jsi 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.simplecov +1 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +160 -0
- data/Rakefile.rb +9 -0
- data/jsi.gemspec +31 -0
- data/lib/jsi.rb +28 -0
- data/lib/jsi/base.rb +325 -0
- data/lib/jsi/base/to_rb.rb +127 -0
- data/lib/jsi/json-schema-fragments.rb +191 -0
- data/lib/jsi/json.rb +7 -0
- data/lib/jsi/json/node.rb +256 -0
- data/lib/jsi/schema.rb +249 -0
- data/lib/jsi/schema_instance_json_coder.rb +83 -0
- data/lib/jsi/struct_json_coder.rb +30 -0
- data/lib/jsi/typelike_modules.rb +164 -0
- data/lib/jsi/util.rb +103 -0
- data/lib/jsi/version.rb +3 -0
- data/test/base_array_test.rb +142 -0
- data/test/base_hash_test.rb +135 -0
- data/test/base_test.rb +395 -0
- data/test/jsi_json_arraynode_test.rb +133 -0
- data/test/jsi_json_hashnode_test.rb +117 -0
- data/test/jsi_json_node_test.rb +288 -0
- data/test/jsi_test.rb +11 -0
- data/test/schema_instance_json_coder_test.rb +122 -0
- data/test/struct_json_coder_test.rb +130 -0
- data/test/test_helper.rb +29 -0
- data/test/util_test.rb +62 -0
- metadata +155 -0
@@ -0,0 +1,127 @@
|
|
1
|
+
module JSI
|
2
|
+
# base class for representing an instance of an instance described by a schema
|
3
|
+
class Base
|
4
|
+
class << self
|
5
|
+
def class_comment
|
6
|
+
lines = []
|
7
|
+
|
8
|
+
description = schema &&
|
9
|
+
schema['description'].respond_to?(:to_str) &&
|
10
|
+
schema['description'].to_str
|
11
|
+
if description
|
12
|
+
description.split("\n", -1).each do |descline|
|
13
|
+
lines << "# " + descline
|
14
|
+
end
|
15
|
+
lines << "#"
|
16
|
+
end
|
17
|
+
|
18
|
+
schema.described_hash_property_names.each_with_index do |propname, i|
|
19
|
+
lines << "#" unless i == 0
|
20
|
+
lines << "# @!attribute [rw] #{propname}"
|
21
|
+
|
22
|
+
property_schema = schema['properties'].respond_to?(:to_hash) &&
|
23
|
+
schema['properties'][propname].respond_to?(:to_hash) &&
|
24
|
+
schema['properties'][propname]
|
25
|
+
|
26
|
+
required = property_schema && property_schema['required']
|
27
|
+
required ||= schema['required'].respond_to?(:to_ary) && schema['required'].include?(propname)
|
28
|
+
lines << "# @required" if required
|
29
|
+
|
30
|
+
type = property_schema &&
|
31
|
+
property_schema['type'].respond_to?(:to_str) &&
|
32
|
+
property_schema['type'].to_str
|
33
|
+
simple = {'string' => 'String', 'number' => 'Numeric', 'boolean' => 'Boolean', 'null' => 'nil'}
|
34
|
+
rettypes = []
|
35
|
+
if simple.key?(type)
|
36
|
+
rettypes << simple[type]
|
37
|
+
elsif type == 'object' || type == 'array'
|
38
|
+
rettypes = []
|
39
|
+
schema_class = JSI.class_for_schema(property_schema)
|
40
|
+
unless schema_class.name =~ /\AJSI::SchemaClasses::/
|
41
|
+
rettypes << schema_class.name
|
42
|
+
end
|
43
|
+
rettypes << {'object' => '#to_hash', 'array' => '#to_ary'}[type]
|
44
|
+
elsif type
|
45
|
+
# not really valid, but there's some information in there. whatever it is.
|
46
|
+
rettypes << type
|
47
|
+
end
|
48
|
+
# we'll add Object to all because the accessor methods have no enforcement that their value is
|
49
|
+
# of the specified type, and may return anything really. TODO: consider if this is of any value?
|
50
|
+
rettypes << 'Object'
|
51
|
+
lines << "# @return [#{rettypes.join(', ')}]"
|
52
|
+
|
53
|
+
description = property_schema &&
|
54
|
+
property_schema['description'].respond_to?(:to_str) &&
|
55
|
+
property_schema['description'].to_str
|
56
|
+
if description
|
57
|
+
description.split("\n", -1).each do |descline|
|
58
|
+
lines << "# " + descline
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
lines.join("\n")
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_rb
|
66
|
+
lines = []
|
67
|
+
description = schema &&
|
68
|
+
schema['description'].respond_to?(:to_str) &&
|
69
|
+
schema['description'].to_str
|
70
|
+
if description
|
71
|
+
description.split("\n", -1).each do |descline|
|
72
|
+
lines << "# " + descline
|
73
|
+
end
|
74
|
+
end
|
75
|
+
lines << "class #{name}"
|
76
|
+
schema.described_hash_property_names.each_with_index do |propname, i|
|
77
|
+
lines << "" unless i == 0
|
78
|
+
property_schema = schema['properties'].respond_to?(:to_hash) &&
|
79
|
+
schema['properties'][propname].respond_to?(:to_hash) &&
|
80
|
+
schema['properties'][propname]
|
81
|
+
description = property_schema &&
|
82
|
+
property_schema['description'].respond_to?(:to_str) &&
|
83
|
+
property_schema['description'].to_str
|
84
|
+
if description
|
85
|
+
description.split("\n", -1).each do |descline|
|
86
|
+
lines << " # " + descline
|
87
|
+
end
|
88
|
+
lines << " #" # blank comment line between description and @return
|
89
|
+
end
|
90
|
+
|
91
|
+
required = property_schema && property_schema['required']
|
92
|
+
required ||= schema['required'].respond_to?(:to_ary) && schema['required'].include?(propname)
|
93
|
+
lines << " # @required" if required
|
94
|
+
|
95
|
+
type = property_schema &&
|
96
|
+
property_schema['type'].respond_to?(:to_str) &&
|
97
|
+
property_schema['type'].to_str
|
98
|
+
simple = {'string' => 'String', 'number' => 'Numeric', 'boolean' => 'Boolean', 'null' => 'nil'}
|
99
|
+
rettypes = []
|
100
|
+
if simple.key?(type)
|
101
|
+
rettypes << simple[type]
|
102
|
+
elsif type == 'object' || type == 'array'
|
103
|
+
rettypes = []
|
104
|
+
schema_class = JSI.class_for_schema(property_schema)
|
105
|
+
unless schema_class.name =~ /\AJSI::SchemaClasses::/
|
106
|
+
rettypes << schema_class.name
|
107
|
+
end
|
108
|
+
rettypes << {'object' => '#to_hash', 'array' => '#to_ary'}[type]
|
109
|
+
elsif type
|
110
|
+
# not really valid, but there's some information in there. whatever it is.
|
111
|
+
rettypes << type
|
112
|
+
end
|
113
|
+
# we'll add Object to all because the accessor methods have no enforcement that their value is
|
114
|
+
# of the specified type, and may return anything really. TODO: consider if this is of any value?
|
115
|
+
rettypes << 'Object'
|
116
|
+
lines << " # @return [#{rettypes.join(', ')}]"
|
117
|
+
|
118
|
+
lines << " def #{propname}"
|
119
|
+
lines << " super"
|
120
|
+
lines << " end"
|
121
|
+
end
|
122
|
+
lines << "end"
|
123
|
+
lines.join("\n")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
require "json-schema"
|
2
|
+
|
3
|
+
# apply the changes from https://github.com/ruby-json-schema/json-schema/pull/382
|
4
|
+
|
5
|
+
# json-schema/pointer.rb
|
6
|
+
require 'addressable/uri'
|
7
|
+
|
8
|
+
module JSON
|
9
|
+
class Schema
|
10
|
+
# a JSON Pointer, as described by RFC 6901 https://tools.ietf.org/html/rfc6901
|
11
|
+
class Pointer
|
12
|
+
class Error < JSON::Schema::SchemaError
|
13
|
+
end
|
14
|
+
class PointerSyntaxError < Error
|
15
|
+
end
|
16
|
+
class ReferenceError < Error
|
17
|
+
end
|
18
|
+
|
19
|
+
# parse a fragment to an array of reference tokens
|
20
|
+
#
|
21
|
+
# #/foo/bar
|
22
|
+
#
|
23
|
+
# => ['foo', 'bar']
|
24
|
+
#
|
25
|
+
# #/foo%20bar
|
26
|
+
#
|
27
|
+
# => ['foo bar']
|
28
|
+
def self.parse_fragment(fragment)
|
29
|
+
fragment = Addressable::URI.unescape(fragment)
|
30
|
+
match = fragment.match(/\A#/)
|
31
|
+
if match
|
32
|
+
parse_pointer(match.post_match)
|
33
|
+
else
|
34
|
+
raise(PointerSyntaxError, "Invalid fragment syntax in #{fragment.inspect}: fragment must begin with #")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# parse a pointer to an array of reference tokens
|
39
|
+
#
|
40
|
+
# /foo
|
41
|
+
#
|
42
|
+
# => ['foo']
|
43
|
+
#
|
44
|
+
# /foo~0bar/baz~1qux
|
45
|
+
#
|
46
|
+
# => ['foo~bar', 'baz/qux']
|
47
|
+
def self.parse_pointer(pointer_string)
|
48
|
+
tokens = pointer_string.split('/', -1).map! do |piece|
|
49
|
+
piece.gsub('~1', '/').gsub('~0', '~')
|
50
|
+
end
|
51
|
+
if tokens[0] == ''
|
52
|
+
tokens[1..-1]
|
53
|
+
elsif tokens.empty?
|
54
|
+
tokens
|
55
|
+
else
|
56
|
+
raise(PointerSyntaxError, "Invalid pointer syntax in #{pointer_string.inspect}: pointer must begin with /")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# initializes a JSON::Schema::Pointer from the given representation.
|
61
|
+
#
|
62
|
+
# type may be one of:
|
63
|
+
#
|
64
|
+
# - :fragment - the representation is a fragment containing a pointer (starting with #)
|
65
|
+
# - :pointer - the representation is a pointer (starting with /)
|
66
|
+
# - :reference_tokens - the representation is an array of tokens referencing a path in a document
|
67
|
+
def initialize(type, representation)
|
68
|
+
@type = type
|
69
|
+
if type == :reference_tokens
|
70
|
+
reference_tokens = representation
|
71
|
+
elsif type == :fragment
|
72
|
+
reference_tokens = self.class.parse_fragment(representation)
|
73
|
+
elsif type == :pointer
|
74
|
+
reference_tokens = self.class.parse_pointer(representation)
|
75
|
+
else
|
76
|
+
raise ArgumentError, "invalid initialization type: #{type.inspect} with representation #{representation.inspect}"
|
77
|
+
end
|
78
|
+
@reference_tokens = reference_tokens.map(&:freeze).freeze
|
79
|
+
end
|
80
|
+
|
81
|
+
attr_reader :reference_tokens
|
82
|
+
|
83
|
+
# takes a root json document and evaluates this pointer through the document, returning the value
|
84
|
+
# pointed to by this pointer.
|
85
|
+
def evaluate(document)
|
86
|
+
reference_tokens.inject(document) do |value, token|
|
87
|
+
if value.is_a?(Array)
|
88
|
+
if token.is_a?(String) && token =~ /\A\d|[1-9]\d+\z/
|
89
|
+
token = token.to_i
|
90
|
+
end
|
91
|
+
unless token.is_a?(Integer)
|
92
|
+
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not an integer and cannot be resolved in array #{value.inspect}")
|
93
|
+
end
|
94
|
+
unless (0...value.size).include?(token)
|
95
|
+
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid index of #{value.inspect}")
|
96
|
+
end
|
97
|
+
elsif value.is_a?(Hash)
|
98
|
+
unless value.key?(token)
|
99
|
+
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid key of #{value.inspect}")
|
100
|
+
end
|
101
|
+
else
|
102
|
+
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} cannot be resolved in #{value.inspect}")
|
103
|
+
end
|
104
|
+
value[token]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# the pointer string representation of this Pointer
|
109
|
+
def pointer
|
110
|
+
reference_tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('')
|
111
|
+
end
|
112
|
+
|
113
|
+
# the fragment string representation of this Pointer
|
114
|
+
def fragment
|
115
|
+
'#' + Addressable::URI.escape(pointer)
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_s
|
119
|
+
"#<#{self.class.inspect} #{@type} = #{representation_s}>"
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def representation_s
|
125
|
+
if @type == :fragment
|
126
|
+
fragment
|
127
|
+
elsif @type == :pointer
|
128
|
+
pointer
|
129
|
+
else
|
130
|
+
reference_tokens.inspect
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# json-schema/validator.rb
|
138
|
+
|
139
|
+
module JSON
|
140
|
+
class Validator
|
141
|
+
def initialize(schema_data, data, opts={})
|
142
|
+
@options = @@default_opts.clone.merge(opts)
|
143
|
+
@errors = []
|
144
|
+
|
145
|
+
validator = self.class.validator_for_name(@options[:version])
|
146
|
+
@options[:version] = validator
|
147
|
+
@options[:schema_reader] ||= self.class.schema_reader
|
148
|
+
|
149
|
+
@validation_options = @options[:record_errors] ? {:record_errors => true} : {}
|
150
|
+
@validation_options[:insert_defaults] = true if @options[:insert_defaults]
|
151
|
+
@validation_options[:strict] = true if @options[:strict] == true
|
152
|
+
@validation_options[:clear_cache] = true if !@@cache_schemas || @options[:clear_cache]
|
153
|
+
|
154
|
+
@@mutex.synchronize { @base_schema = initialize_schema(schema_data) }
|
155
|
+
@original_data = data
|
156
|
+
@data = initialize_data(data)
|
157
|
+
@@mutex.synchronize { build_schemas(@base_schema) }
|
158
|
+
|
159
|
+
# If the :fragment option is set, try and validate against the fragment
|
160
|
+
if opts[:fragment]
|
161
|
+
@base_schema = schema_from_fragment(@base_schema, opts[:fragment])
|
162
|
+
end
|
163
|
+
|
164
|
+
# validate the schema, if requested
|
165
|
+
if @options[:validate_schema]
|
166
|
+
if @base_schema.schema["$schema"]
|
167
|
+
base_validator = self.class.validator_for_name(@base_schema.schema["$schema"])
|
168
|
+
end
|
169
|
+
metaschema = base_validator ? base_validator.metaschema : validator.metaschema
|
170
|
+
# Don't clear the cache during metaschema validation!
|
171
|
+
self.class.validate!(metaschema, @base_schema.schema, {:clear_cache => false})
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def schema_from_fragment(base_schema, fragment)
|
176
|
+
schema_uri = base_schema.uri
|
177
|
+
|
178
|
+
pointer = JSON::Schema::Pointer.new(:fragment, fragment)
|
179
|
+
|
180
|
+
base_schema = JSON::Schema.new(pointer.evaluate(base_schema.schema), schema_uri, @options[:version])
|
181
|
+
|
182
|
+
if @options[:list]
|
183
|
+
base_schema.to_array_schema
|
184
|
+
elsif base_schema.is_a?(Hash)
|
185
|
+
JSON::Schema.new(base_schema, schema_uri, @options[:version])
|
186
|
+
else
|
187
|
+
base_schema
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
data/lib/jsi/json.rb
ADDED
@@ -0,0 +1,256 @@
|
|
1
|
+
module JSI
|
2
|
+
module JSON
|
3
|
+
# JSI::JSON::Node is an abstraction of a node within a JSON document.
|
4
|
+
# it aims to act like the underlying data type of the node's content
|
5
|
+
# (Hash or Array, generally) in most cases, defining methods of Hash
|
6
|
+
# and Array which delegate to the content. However, destructive methods
|
7
|
+
# are not defined, as modifying the content of a node would change it
|
8
|
+
# for any other nodes in the document that contain or refer to it.
|
9
|
+
#
|
10
|
+
# methods that return a modified copy such as #merge are defined, and
|
11
|
+
# return a copy of the document with the content of the node modified.
|
12
|
+
# the original node's document and content are untouched.
|
13
|
+
class Node
|
14
|
+
# if the content of the document at the given path is a Hash, returns
|
15
|
+
# a HashNode; if an Array, returns ArrayNode. otherwise returns a
|
16
|
+
# regular Node, though, for the most part this will be called with Hash
|
17
|
+
# or Array content.
|
18
|
+
def self.new_by_type(document, path)
|
19
|
+
node = Node.new(document, path)
|
20
|
+
content = node.content
|
21
|
+
if content.is_a?(Hash)
|
22
|
+
HashNode.new(document, path)
|
23
|
+
elsif content.is_a?(Array)
|
24
|
+
ArrayNode.new(document, path)
|
25
|
+
else
|
26
|
+
node
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# a Node represents the content of a document at a given path.
|
31
|
+
def initialize(document, path)
|
32
|
+
raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect.chomp} (#{path.class})") unless path.is_a?(Array)
|
33
|
+
@document = document
|
34
|
+
@path = path.dup.freeze
|
35
|
+
@pointer = ::JSON::Schema::Pointer.new(:reference_tokens, path)
|
36
|
+
end
|
37
|
+
|
38
|
+
# the path of this Node within its document
|
39
|
+
attr_reader :path
|
40
|
+
# the document containing this Node at is path
|
41
|
+
attr_reader :document
|
42
|
+
# ::JSON::Schema::Pointer representing the path to this node within its document
|
43
|
+
attr_reader :pointer
|
44
|
+
|
45
|
+
# the raw content of this Node from the underlying document at this Node's path.
|
46
|
+
def content
|
47
|
+
pointer.evaluate(document)
|
48
|
+
end
|
49
|
+
|
50
|
+
def [](k)
|
51
|
+
node = self
|
52
|
+
content = node.content
|
53
|
+
if content.is_a?(Hash) && !content.key?(k)
|
54
|
+
node = node.deref
|
55
|
+
content = node.content
|
56
|
+
end
|
57
|
+
begin
|
58
|
+
el = content[k]
|
59
|
+
rescue TypeError => e
|
60
|
+
raise(e.class, e.message + "\nsubscripting with #{k.pretty_inspect.chomp} (#{k.class}) from #{content.class.inspect}. self is: #{pretty_inspect.chomp}", e.backtrace)
|
61
|
+
end
|
62
|
+
if el.is_a?(Hash) || el.is_a?(Array)
|
63
|
+
self.class.new_by_type(node.document, node.path + [k])
|
64
|
+
else
|
65
|
+
el
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def []=(k, v)
|
70
|
+
if v.is_a?(Node)
|
71
|
+
content[k] = v.content
|
72
|
+
else
|
73
|
+
content[k] = v
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def deref
|
78
|
+
content = self.content
|
79
|
+
|
80
|
+
return self unless content.is_a?(Hash) && content['$ref'].is_a?(String)
|
81
|
+
|
82
|
+
if content['$ref'][/\A#/]
|
83
|
+
return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(content['$ref'])).deref
|
84
|
+
end
|
85
|
+
|
86
|
+
# HAX for how google does refs and ids
|
87
|
+
if document_node['schemas'].respond_to?(:to_hash)
|
88
|
+
if document_node['schemas'][content['$ref']]
|
89
|
+
return document_node['schemas'][content['$ref']]
|
90
|
+
end
|
91
|
+
_, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == content['$ref'] }
|
92
|
+
if deref_by_id
|
93
|
+
return deref_by_id
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
#raise(NotImplementedError, "cannot dereference #{content['$ref']}") # TODO
|
98
|
+
return self
|
99
|
+
end
|
100
|
+
|
101
|
+
# a Node at the root of the document
|
102
|
+
def document_node
|
103
|
+
Node.new_by_type(document, [])
|
104
|
+
end
|
105
|
+
|
106
|
+
# the parent of this node. if this node is the document root (its path is empty), raises
|
107
|
+
# ::JSON::Schema::Pointer::ReferenceError.
|
108
|
+
def parent_node
|
109
|
+
if path.empty?
|
110
|
+
raise(::JSON::Schema::Pointer::ReferenceError, "cannot access parent of root node: #{pretty_inspect.chomp}")
|
111
|
+
else
|
112
|
+
Node.new_by_type(document, path[0...-1])
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# the pointer path to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
|
117
|
+
def pointer_path
|
118
|
+
pointer.pointer
|
119
|
+
end
|
120
|
+
# the pointer fragment to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
|
121
|
+
def fragment
|
122
|
+
pointer.fragment
|
123
|
+
end
|
124
|
+
|
125
|
+
def as_json(*opt)
|
126
|
+
Typelike.as_json(content, *opt)
|
127
|
+
end
|
128
|
+
|
129
|
+
# takes a block. the block is yielded the content of this node. the block MUST return a modified
|
130
|
+
# copy of that content (and NOT modify the object it is given).
|
131
|
+
def modified_copy
|
132
|
+
# we need to preserve the rest of the document, but modify the content at our path.
|
133
|
+
#
|
134
|
+
# this is actually a bit tricky. we can't modify the original document, obviously.
|
135
|
+
# we could do a deep copy, but that's expensive. instead, we make a copy of each array
|
136
|
+
# or hash in the path above this node. this node's content is modified by the caller, and
|
137
|
+
# that is recursively merged up to the document root. the recursion is done with a
|
138
|
+
# y combinator, for no other reason than that was a fun way to implement it.
|
139
|
+
modified_document = JSI.ycomb do |rec|
|
140
|
+
proc do |subdocument, subpath|
|
141
|
+
if subpath == []
|
142
|
+
yield(subdocument)
|
143
|
+
else
|
144
|
+
car = subpath[0]
|
145
|
+
cdr = subpath[1..-1]
|
146
|
+
if subdocument.respond_to?(:to_hash)
|
147
|
+
car_object = rec.call(subdocument[car], cdr)
|
148
|
+
if car_object.object_id == subdocument[car].object_id
|
149
|
+
subdocument
|
150
|
+
else
|
151
|
+
subdocument.merge({car => car_object})
|
152
|
+
end
|
153
|
+
elsif subdocument.respond_to?(:to_ary)
|
154
|
+
if car.is_a?(String) && car =~ /\A\d+\z/
|
155
|
+
car = car.to_i
|
156
|
+
end
|
157
|
+
unless car.is_a?(Integer)
|
158
|
+
raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
|
159
|
+
end
|
160
|
+
car_object = rec.call(subdocument[car], cdr)
|
161
|
+
if car_object.object_id == subdocument[car].object_id
|
162
|
+
subdocument
|
163
|
+
else
|
164
|
+
subdocument.dup.tap do |arr|
|
165
|
+
arr[car] = car_object
|
166
|
+
end
|
167
|
+
end
|
168
|
+
else
|
169
|
+
raise(TypeError, "bad subscript: #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect.chomp}")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end.call(document, path)
|
174
|
+
Node.new_by_type(modified_document, path)
|
175
|
+
end
|
176
|
+
|
177
|
+
def object_group_text
|
178
|
+
"fragment=#{fragment.inspect}"
|
179
|
+
end
|
180
|
+
def inspect
|
181
|
+
"\#<#{self.class.inspect} #{object_group_text} #{content.inspect}>"
|
182
|
+
end
|
183
|
+
def pretty_print(q)
|
184
|
+
q.instance_exec(self) do |obj|
|
185
|
+
text "\#<#{obj.class.inspect} #{obj.object_group_text}"
|
186
|
+
group_sub {
|
187
|
+
nest(2) {
|
188
|
+
breakable ' '
|
189
|
+
pp obj.content
|
190
|
+
}
|
191
|
+
}
|
192
|
+
breakable ''
|
193
|
+
text '>'
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def fingerprint
|
198
|
+
{is_node: self.is_a?(JSI::JSON::Node), document: document, path: path}
|
199
|
+
end
|
200
|
+
include FingerprintHash
|
201
|
+
end
|
202
|
+
|
203
|
+
class ArrayNode < Node
|
204
|
+
def each
|
205
|
+
return to_enum(__method__) { content.size } unless block_given?
|
206
|
+
content.each_index { |i| yield self[i] }
|
207
|
+
self
|
208
|
+
end
|
209
|
+
|
210
|
+
def to_ary
|
211
|
+
to_a
|
212
|
+
end
|
213
|
+
|
214
|
+
include Enumerable
|
215
|
+
include Arraylike
|
216
|
+
|
217
|
+
def as_json(*opt) # needs redefined after including Enumerable
|
218
|
+
Typelike.as_json(content, *opt)
|
219
|
+
end
|
220
|
+
|
221
|
+
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
|
222
|
+
# we override these methods from Arraylike
|
223
|
+
SAFE_INDEX_ONLY_METHODS.each do |method_name|
|
224
|
+
define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
class HashNode < Node
|
229
|
+
def each(&block)
|
230
|
+
return to_enum(__method__) { content.size } unless block_given?
|
231
|
+
if block.arity > 1
|
232
|
+
content.each_key { |k| yield k, self[k] }
|
233
|
+
else
|
234
|
+
content.each_key { |k| yield [k, self[k]] }
|
235
|
+
end
|
236
|
+
self
|
237
|
+
end
|
238
|
+
|
239
|
+
def to_hash
|
240
|
+
inject({}) { |h, (k, v)| h[k] = v; h }
|
241
|
+
end
|
242
|
+
|
243
|
+
include Enumerable
|
244
|
+
include Hashlike
|
245
|
+
|
246
|
+
def as_json(*opt) # needs redefined after including Enumerable
|
247
|
+
Typelike.as_json(content, *opt)
|
248
|
+
end
|
249
|
+
|
250
|
+
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
|
251
|
+
SAFE_KEY_ONLY_METHODS.each do |method_name|
|
252
|
+
define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|