summaryrefslogtreecommitdiff
path: root/lib/chef/json_compat.rb
blob: 9db358b5b98c32e45507532178e9622efeea6177 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#
# Author:: Tim Hinderliter (<tim@opscode.com>)
# Copyright:: Copyright (c) 2010 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Wrapper class for interacting with JSON.

require 'ffi_yajl'
require 'chef/exceptions'
# We're requiring this to prevent breaking consumers using Hash.to_json
require 'json'

class Chef
  class JSONCompat
    JSON_MAX_NESTING = 1000

    JSON_CLASS = "json_class".freeze

    CHEF_APICLIENT          = "Chef::ApiClient".freeze
    CHEF_CHECKSUM           = "Chef::Checksum".freeze
    CHEF_COOKBOOKVERSION    = "Chef::CookbookVersion".freeze
    CHEF_DATABAG            = "Chef::DataBag".freeze
    CHEF_DATABAGITEM        = "Chef::DataBagItem".freeze
    CHEF_ENVIRONMENT        = "Chef::Environment".freeze
    CHEF_NODE               = "Chef::Node".freeze
    CHEF_ROLE               = "Chef::Role".freeze
    CHEF_SANDBOX            = "Chef::Sandbox".freeze
    CHEF_RESOURCE           = "Chef::Resource".freeze
    CHEF_RESOURCECOLLECTION = "Chef::ResourceCollection".freeze

    class <<self

      # API to use to avoid create_addtions
      def parse(source, opts = {})
        begin
          FFI_Yajl::Parser.parse(source, opts)
        rescue FFI_Yajl::ParseError => e
          raise Chef::Exceptions::JSON::ParseError, e.message
        end
      end

      # Just call the JSON gem's parse method with a modified :max_nesting field
      def from_json(source, opts = {})
        obj = parse(source, opts)

        # JSON gem requires top level object to be a Hash or Array (otherwise
        # you get the "must contain two octets" error). Yajl doesn't impose the
        # same limitation. For compatibility, we re-impose this condition.
        unless obj.kind_of?(Hash) or obj.kind_of?(Array)
          raise Chef::Exceptions::JSON::ParseError, "Top level JSON object must be a Hash or Array. (actual: #{obj.class})"
        end

        # The old default in the json gem (which we are mimicing because we
        # sadly rely on this misfeature) is to "create additions" i.e., convert
        # JSON objects into ruby objects. Explicit :create_additions => false
        # is required to turn it off.
        if opts[:create_additions].nil? || opts[:create_additions]
          map_to_rb_obj(obj)
        else
          obj
        end
      end

      # Look at an object that's a basic type (from json parse) and convert it
      # to an instance of Chef classes if desired.
      def map_to_rb_obj(json_obj)
        case json_obj
          when Hash
            mapped_hash = map_hash_to_rb_obj(json_obj)
            if json_obj.has_key?(JSON_CLASS) && (class_to_inflate = class_for_json_class(json_obj[JSON_CLASS]))
              class_to_inflate.json_create(mapped_hash)
            else
              mapped_hash
            end
          when Array
            json_obj.map {|e| map_to_rb_obj(e) }
          else
            json_obj
        end
      end

      def map_hash_to_rb_obj(json_hash)
        json_hash.each do |key, value|
          json_hash[key] = map_to_rb_obj(value)
        end
        json_hash
      end

      def to_json(obj, opts = nil)
        begin
          FFI_Yajl::Encoder.encode(obj, opts)
        rescue FFI_Yajl::EncodeError => e
          raise Chef::Exceptions::JSON::EncodeError, e.message
        end
      end

      def to_json_pretty(obj, opts = nil)
        opts ||= {}
        options_map = {}
        options_map[:pretty] = true
        options_map[:indent] = opts[:indent] if opts.has_key?(:indent)
        to_json(obj, options_map).chomp
      end

      # Map +json_class+ to a Class object. We use a +case+ instead of a Hash
      # assigned to a constant because otherwise this file could not be loaded
      # until all the constants were defined, which means you'd have to load
      # the world to get json, which would make knife very slow.
      def class_for_json_class(json_class)
        case json_class
          when CHEF_APICLIENT
            Chef::ApiClient
          when CHEF_CHECKSUM
            Chef::Checksum
          when CHEF_COOKBOOKVERSION
            Chef::CookbookVersion
          when CHEF_DATABAG
            Chef::DataBag
          when CHEF_DATABAGITEM
            Chef::DataBagItem
          when CHEF_ENVIRONMENT
            Chef::Environment
          when CHEF_NODE
            Chef::Node
          when CHEF_ROLE
            Chef::Role
          when CHEF_SANDBOX
            # a falsey return here will disable object inflation/"create
            # additions" in the caller. In Chef 11 this is correct, we just have
            # a dummy Chef::Sandbox class for compat with Chef 10 servers.
            false
          when CHEF_RESOURCE
            Chef::Resource
          when CHEF_RESOURCECOLLECTION
            Chef::ResourceCollection
          when /^Chef::Resource/
            Chef::Resource.find_subclass_by_name(json_class)
          else
            raise Chef::Exceptions::JSON::ParseError, "Unsupported `json_class` type '#{json_class}'"
        end
      end

    end
  end
end