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
|