summaryrefslogtreecommitdiff
path: root/lib/chef/json_compat.rb
blob: f8f05a0074ee8a6661ab946234d542b939744a2a (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
159
160
161
162
163
164
#
# Author:: Tim Hinderliter (<tim@chef.io>)
# Copyright:: Copyright 2010-2016, Chef Software 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
    CHEF_RESOURCESET        = "Chef::ResourceCollection::ResourceSet".freeze
    CHEF_RESOURCELIST       = "Chef::ResourceCollection::ResourceList".freeze
    CHEF_RUNLISTEXPANSION   = "Chef::RunListExpansion".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) || 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_RESOURCESET
          Chef::ResourceCollection::ResourceSet
        when CHEF_RESOURCELIST
          Chef::ResourceCollection::ResourceList
        when /^Chef::Resource/
          Chef::Resource.find_descendants_by_name(json_class)
        else
          raise Chef::Exceptions::JSON::ParseError, "Unsupported `json_class` type '#{json_class}'"
        end
      end

    end
  end
end