jsi 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|