jsi 0.0.4 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.simplecov +3 -1
- data/CHANGELOG.md +48 -0
- data/LICENSE.md +613 -0
- data/README.md +84 -45
- data/jsi.gemspec +11 -14
- data/lib/jsi.rb +31 -12
- data/lib/jsi/base.rb +310 -344
- data/lib/jsi/base/to_rb.rb +2 -0
- data/lib/jsi/jsi_coder.rb +91 -0
- data/lib/jsi/json-schema-fragments.rb +3 -135
- data/lib/jsi/json.rb +3 -0
- data/lib/jsi/json/node.rb +72 -197
- data/lib/jsi/json/pointer.rb +419 -0
- data/lib/jsi/metaschema.rb +7 -0
- data/lib/jsi/metaschema_node.rb +218 -0
- data/lib/jsi/pathed_node.rb +118 -0
- data/lib/jsi/schema.rb +168 -223
- data/lib/jsi/schema_classes.rb +158 -0
- data/lib/jsi/simple_wrap.rb +12 -0
- data/lib/jsi/typelike_modules.rb +71 -45
- data/lib/jsi/util.rb +47 -57
- data/lib/jsi/version.rb +1 -1
- data/lib/schemas/json-schema.org/draft-04/schema.rb +7 -0
- data/lib/schemas/json-schema.org/draft-06/schema.rb +7 -0
- data/resources/icons/AGPL-3.0.png +0 -0
- data/test/base_array_test.rb +210 -84
- data/test/base_hash_test.rb +201 -58
- data/test/base_test.rb +212 -121
- data/test/jsi_coder_test.rb +85 -0
- data/test/jsi_json_arraynode_test.rb +26 -25
- data/test/jsi_json_hashnode_test.rb +40 -39
- data/test/jsi_json_node_test.rb +95 -126
- data/test/jsi_json_pointer_test.rb +102 -0
- data/test/jsi_typelike_as_json_test.rb +53 -0
- data/test/metaschema_node_test.rb +19 -0
- data/test/schema_module_test.rb +21 -0
- data/test/schema_test.rb +109 -97
- data/test/spreedly_openapi_test.rb +8 -0
- data/test/test_helper.rb +42 -8
- data/test/util_test.rb +14 -14
- metadata +54 -25
- data/LICENSE.txt +0 -21
- data/lib/jsi/schema_instance_json_coder.rb +0 -83
- data/lib/jsi/struct_json_coder.rb +0 -30
- data/test/schema_instance_json_coder_test.rb +0 -121
- data/test/struct_json_coder_test.rb +0 -130
data/lib/jsi/base/to_rb.rb
CHANGED
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
# this is an ActiveRecord serialization coder intended to serialize between
|
5
|
+
# JSON-compatible objects on the database side, and a JSI instance loaded on
|
6
|
+
# the model attribute.
|
7
|
+
#
|
8
|
+
# on its own this coder is useful with a JSON database column. in order to
|
9
|
+
# serialize further to a string of JSON, or to YAML, the gem `arms` allows
|
10
|
+
# coders to be chained together. for example, for a table `foos` and a column
|
11
|
+
# `preferences_json` which is an actual json column, and `preferences_txt`
|
12
|
+
# which is a string:
|
13
|
+
#
|
14
|
+
# Preferences = JSI.class_for_schema(preferences_json_schema)
|
15
|
+
# class Foo < ActiveRecord::Base
|
16
|
+
# # as a single serializer, loads a Preferences instance from a json column
|
17
|
+
# serialize 'preferences_json', JSI::JSICoder.new(Preferences)
|
18
|
+
#
|
19
|
+
# # for a text column, arms_serialize will go from JSI to JSON-compatible
|
20
|
+
# # objects to a string. the symbol `:jsi` is a shortcut for JSI::JSICoder.
|
21
|
+
# arms_serialize 'preferences_txt', [:jsi, Preferences], :json
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# the column data may be either a single instance of the schema class
|
25
|
+
# (represented as one json object) or an array of them (represented as a json
|
26
|
+
# array of json objects), indicated by the keyword argument `array`.
|
27
|
+
class JSICoder
|
28
|
+
# @param schema [JSI::Schema, JSI::SchemaModule, Class < JSI::Base] a schema, a JSI schema class, or
|
29
|
+
# a JSI schema module. #load will instantiate column data using the JSI schema represented.
|
30
|
+
# @param array [Boolean] whether the dumped data represent one instance of the schema,
|
31
|
+
# or an array of them. note that it may be preferable to simply use an array schema.
|
32
|
+
def initialize(schema, array: false)
|
33
|
+
unless schema.respond_to?(:new_jsi)
|
34
|
+
raise(ArgumentError, "not a JSI schema, class, or module: #{schema.inspect}")
|
35
|
+
end
|
36
|
+
@schema = schema
|
37
|
+
@array = array
|
38
|
+
end
|
39
|
+
|
40
|
+
# loads the database column to JSI instances of our schema
|
41
|
+
#
|
42
|
+
# @param data [Object, Array, nil] the dumped schema instance(s) of the JSI(s)
|
43
|
+
# @return [JSI::Base, Array<JSI::Base>, nil] the JSI or JSIs containing the schema
|
44
|
+
# instance(s), or nil if data is nil
|
45
|
+
def load(data)
|
46
|
+
return nil if data.nil?
|
47
|
+
object = if @array
|
48
|
+
unless data.respond_to?(:to_ary)
|
49
|
+
raise TypeError, "expected array-like column data; got: #{data.class}: #{data.inspect}"
|
50
|
+
end
|
51
|
+
data.map { |el| load_object(el) }
|
52
|
+
else
|
53
|
+
load_object(data)
|
54
|
+
end
|
55
|
+
object
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param object [JSI::Base, Array<JSI::Base>, nil] the JSI or array of JSIs containing
|
59
|
+
# the schema instance(s)
|
60
|
+
# @return [Object, Array, nil] the schema instance(s) of the JSI(s), or nil if object is nil
|
61
|
+
def dump(object)
|
62
|
+
return nil if object.nil?
|
63
|
+
jsonifiable = begin
|
64
|
+
if @array
|
65
|
+
unless object.respond_to?(:to_ary)
|
66
|
+
raise(TypeError, "expected array-like attribute; got: #{object.class}: #{object.inspect}")
|
67
|
+
end
|
68
|
+
object.map do |el|
|
69
|
+
dump_object(el)
|
70
|
+
end
|
71
|
+
else
|
72
|
+
dump_object(object)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
jsonifiable
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
# @param data [Object]
|
80
|
+
# @return [JSI::Base]
|
81
|
+
def load_object(data)
|
82
|
+
@schema.new_jsi(data)
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param object [JSI::Base, Object]
|
86
|
+
# @return [Object]
|
87
|
+
def dump_object(object)
|
88
|
+
JSI::Typelike.as_json(object)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -1,141 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "json-schema"
|
2
4
|
|
3
5
|
# apply the changes from https://github.com/ruby-json-schema/json-schema/pull/382
|
4
6
|
|
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
|
-
res = reference_tokens.inject(document) do |value, token|
|
87
|
-
if value.respond_to?(:to_ary)
|
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.respond_to?(:size) ? value : value.to_ary).size).include?(token)
|
95
|
-
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid index of #{value.inspect}")
|
96
|
-
end
|
97
|
-
(value.respond_to?(:[]) ? value : value.to_ary)[token]
|
98
|
-
elsif value.respond_to?(:to_hash)
|
99
|
-
unless (value.respond_to?(:key?) ? value : value.to_hash).key?(token)
|
100
|
-
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid key of #{value.inspect}")
|
101
|
-
end
|
102
|
-
(value.respond_to?(:[]) ? value : value.to_hash)[token]
|
103
|
-
else
|
104
|
-
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} cannot be resolved in #{value.inspect}")
|
105
|
-
end
|
106
|
-
end
|
107
|
-
res
|
108
|
-
end
|
109
|
-
|
110
|
-
# the pointer string representation of this Pointer
|
111
|
-
def pointer
|
112
|
-
reference_tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('')
|
113
|
-
end
|
114
|
-
|
115
|
-
# the fragment string representation of this Pointer
|
116
|
-
def fragment
|
117
|
-
'#' + Addressable::URI.escape(pointer)
|
118
|
-
end
|
119
|
-
|
120
|
-
def to_s
|
121
|
-
"#<#{self.class.inspect} #{@type} = #{representation_s}>"
|
122
|
-
end
|
123
|
-
|
124
|
-
private
|
125
|
-
|
126
|
-
def representation_s
|
127
|
-
if @type == :fragment
|
128
|
-
fragment
|
129
|
-
elsif @type == :pointer
|
130
|
-
pointer
|
131
|
-
else
|
132
|
-
reference_tokens.inspect
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
7
|
# json-schema/validator.rb
|
140
8
|
|
141
9
|
module JSON
|
@@ -177,7 +45,7 @@ module JSON
|
|
177
45
|
def schema_from_fragment(base_schema, fragment)
|
178
46
|
schema_uri = base_schema.uri
|
179
47
|
|
180
|
-
pointer = JSON::
|
48
|
+
pointer = JSI::JSON::Pointer.from_fragment(fragment)
|
181
49
|
|
182
50
|
base_schema = JSON::Schema.new(pointer.evaluate(base_schema.schema), schema_uri, @options[:version])
|
183
51
|
|
data/lib/jsi/json.rb
CHANGED
data/lib/jsi/json/node.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSI
|
2
4
|
module JSON
|
3
5
|
# JSI::JSON::Node is an abstraction of a node within a JSON document.
|
@@ -22,51 +24,45 @@ module JSI
|
|
22
24
|
# return a copy of the document with the content of the node modified.
|
23
25
|
# the original node's document and content are untouched.
|
24
26
|
class Node
|
25
|
-
|
26
|
-
|
27
|
+
include Enumerable
|
28
|
+
include PathedNode
|
29
|
+
|
30
|
+
def self.new_doc(node_document)
|
31
|
+
new_by_type(node_document, JSI::JSON::Pointer.new([]))
|
27
32
|
end
|
28
33
|
|
29
|
-
# if the content of the document at the given
|
34
|
+
# if the content of the document at the given pointer is Hash-like, returns
|
30
35
|
# a HashNode; if Array-like, returns ArrayNode. otherwise returns a
|
31
36
|
# regular Node, although Nodes are for the most part instantiated from
|
32
37
|
# Hash or Array-like content.
|
33
|
-
def self.new_by_type(
|
34
|
-
|
35
|
-
content = node.content
|
38
|
+
def self.new_by_type(node_document, node_ptr)
|
39
|
+
content = node_ptr.evaluate(node_document)
|
36
40
|
if content.respond_to?(:to_hash)
|
37
|
-
HashNode.new(
|
41
|
+
HashNode.new(node_document, node_ptr)
|
38
42
|
elsif content.respond_to?(:to_ary)
|
39
|
-
ArrayNode.new(
|
43
|
+
ArrayNode.new(node_document, node_ptr)
|
40
44
|
else
|
41
|
-
|
45
|
+
Node.new(node_document, node_ptr)
|
42
46
|
end
|
43
47
|
end
|
44
48
|
|
45
|
-
# a Node represents the content of a document at a given
|
46
|
-
def initialize(
|
47
|
-
unless
|
48
|
-
raise(
|
49
|
+
# a Node represents the content of a document at a given pointer.
|
50
|
+
def initialize(node_document, node_ptr)
|
51
|
+
unless node_ptr.is_a?(JSI::JSON::Pointer)
|
52
|
+
raise(TypeError, "node_ptr must be a JSI::JSON::Pointer. got: #{node_ptr.pretty_inspect.chomp} (#{node_ptr.class})")
|
49
53
|
end
|
50
|
-
if
|
51
|
-
raise(TypeError, "
|
54
|
+
if node_document.is_a?(JSI::JSON::Node)
|
55
|
+
raise(TypeError, "node_document of a Node should not be another JSI::JSON::Node: #{node_document.inspect}")
|
52
56
|
end
|
53
|
-
@
|
54
|
-
@
|
55
|
-
@pointer = ::JSON::Schema::Pointer.new(:reference_tokens, path)
|
57
|
+
@node_document = node_document
|
58
|
+
@node_ptr = node_ptr
|
56
59
|
end
|
57
60
|
|
58
|
-
# the
|
59
|
-
attr_reader :
|
60
|
-
# the document containing this Node at is path
|
61
|
-
attr_reader :document
|
62
|
-
# ::JSON::Schema::Pointer representing the path to this node within its document
|
63
|
-
attr_reader :pointer
|
61
|
+
# the document containing this Node at our pointer
|
62
|
+
attr_reader :node_document
|
64
63
|
|
65
|
-
#
|
66
|
-
|
67
|
-
content = pointer.evaluate(document)
|
68
|
-
content
|
69
|
-
end
|
64
|
+
# JSI::JSON::Pointer pointing to this node within its document
|
65
|
+
attr_reader :node_ptr
|
70
66
|
|
71
67
|
# returns content at the given subscript - call this the subcontent.
|
72
68
|
#
|
@@ -78,12 +74,8 @@ module JSI
|
|
78
74
|
# if this node's content is a $ref - that is, a hash with a $ref attribute - and the subscript is
|
79
75
|
# not a key of the hash, then the $ref is followed before returning the subcontent.
|
80
76
|
def [](subscript)
|
81
|
-
|
82
|
-
content =
|
83
|
-
if content.respond_to?(:to_hash) && !(content.respond_to?(:key?) ? content : content.to_hash).key?(subscript)
|
84
|
-
node = node.deref
|
85
|
-
content = node.content
|
86
|
-
end
|
77
|
+
ptr = self.node_ptr
|
78
|
+
content = self.node_content
|
87
79
|
unless content.respond_to?(:[])
|
88
80
|
if content.respond_to?(:to_hash)
|
89
81
|
content = content.to_hash
|
@@ -99,9 +91,9 @@ module JSI
|
|
99
91
|
raise(e.class, e.message + "\nsubscripting with #{subscript.pretty_inspect.chomp} (#{subscript.class}) from #{content.class.inspect}. content is: #{content.pretty_inspect.chomp}", e.backtrace)
|
100
92
|
end
|
101
93
|
if subcontent.respond_to?(:to_hash)
|
102
|
-
HashNode.new(
|
94
|
+
HashNode.new(node_document, ptr[subscript])
|
103
95
|
elsif subcontent.respond_to?(:to_ary)
|
104
|
-
ArrayNode.new(
|
96
|
+
ArrayNode.new(node_document, ptr[subscript])
|
105
97
|
else
|
106
98
|
subcontent
|
107
99
|
end
|
@@ -110,9 +102,9 @@ module JSI
|
|
110
102
|
# assigns the given subscript of the content to the given value. the document is modified in place.
|
111
103
|
def []=(subscript, value)
|
112
104
|
if value.is_a?(Node)
|
113
|
-
|
105
|
+
node_content[subscript] = value.node_content
|
114
106
|
else
|
115
|
-
|
107
|
+
node_content[subscript] = value
|
116
108
|
end
|
117
109
|
end
|
118
110
|
|
@@ -120,209 +112,92 @@ module JSI
|
|
120
112
|
# does not have a $ref, or if what its $ref cannot be found, this node is returned.
|
121
113
|
#
|
122
114
|
# currently only $refs pointing within the same document are followed.
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
if ref[/\A#/]
|
132
|
-
return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(ref)).deref
|
133
|
-
end
|
134
|
-
|
135
|
-
# HAX for how google does refs and ids
|
136
|
-
if document_node['schemas'].respond_to?(:to_hash)
|
137
|
-
if document_node['schemas'][ref]
|
138
|
-
return document_node['schemas'][ref]
|
139
|
-
end
|
140
|
-
_, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == ref }
|
141
|
-
if deref_by_id
|
142
|
-
return deref_by_id
|
143
|
-
end
|
115
|
+
#
|
116
|
+
# @yield [Node] if a block is given (optional), this will yield a deref'd node. if this
|
117
|
+
# node is not a $ref object, the block is not called. if we are a $ref which cannot be followed
|
118
|
+
# (e.g. a $ref to an external document, which is not yet supported), the block is not called.
|
119
|
+
# @return [JSI::JSON::Node] dereferenced node, or this node
|
120
|
+
def deref(&block)
|
121
|
+
node_ptr_deref do |deref_ptr|
|
122
|
+
return Node.new_by_type(node_document, deref_ptr).tap(&(block || Util::NOOP))
|
144
123
|
end
|
145
|
-
|
146
|
-
#raise(NotImplementedError, "cannot dereference #{ref}") # TODO
|
147
124
|
return self
|
148
125
|
end
|
149
126
|
|
150
127
|
# a Node at the root of the document
|
151
|
-
def
|
152
|
-
Node.new_doc(
|
128
|
+
def document_root_node
|
129
|
+
Node.new_doc(node_document)
|
153
130
|
end
|
154
131
|
|
155
|
-
# the parent of this node. if this node is the document root
|
156
|
-
# ::JSON::
|
132
|
+
# the parent of this node. if this node is the document root, raises
|
133
|
+
# JSI::JSON::Pointer::ReferenceError.
|
157
134
|
def parent_node
|
158
|
-
|
159
|
-
raise(::JSON::Schema::Pointer::ReferenceError, "cannot access parent of root node: #{pretty_inspect.chomp}")
|
160
|
-
else
|
161
|
-
Node.new_by_type(document, path[0...-1])
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
# the pointer path to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
|
166
|
-
def pointer_path
|
167
|
-
pointer.pointer
|
168
|
-
end
|
169
|
-
|
170
|
-
# the pointer fragment to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
|
171
|
-
def fragment
|
172
|
-
pointer.fragment
|
135
|
+
Node.new_by_type(node_document, node_ptr.parent)
|
173
136
|
end
|
174
137
|
|
175
138
|
# returns a jsonifiable representation of this node's content
|
176
139
|
def as_json(*opt)
|
177
|
-
Typelike.as_json(
|
140
|
+
Typelike.as_json(node_content, *opt)
|
178
141
|
end
|
179
142
|
|
180
143
|
# takes a block. the block is yielded the content of this node. the block MUST return a modified
|
181
144
|
# copy of that content (and NOT modify the object it is given).
|
182
|
-
def modified_copy
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
# that is recursively merged up to the document root. the recursion is done with a
|
189
|
-
# y combinator, for no other reason than that was a fun way to implement it.
|
190
|
-
modified_document = JSI::Util.ycomb do |rec|
|
191
|
-
proc do |subdocument, subpath|
|
192
|
-
if subpath == []
|
193
|
-
yield(subdocument)
|
194
|
-
else
|
195
|
-
car = subpath[0]
|
196
|
-
cdr = subpath[1..-1]
|
197
|
-
if subdocument.respond_to?(:to_hash)
|
198
|
-
subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_hash)[car]
|
199
|
-
car_object = rec.call(subdocument_car, cdr)
|
200
|
-
if car_object.object_id == subdocument_car.object_id
|
201
|
-
subdocument
|
202
|
-
else
|
203
|
-
(subdocument.respond_to?(:merge) ? subdocument : subdocument.to_hash).merge({car => car_object})
|
204
|
-
end
|
205
|
-
elsif subdocument.respond_to?(:to_ary)
|
206
|
-
if car.is_a?(String) && car =~ /\A\d+\z/
|
207
|
-
car = car.to_i
|
208
|
-
end
|
209
|
-
unless car.is_a?(Integer)
|
210
|
-
raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
|
211
|
-
end
|
212
|
-
subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_ary)[car]
|
213
|
-
car_object = rec.call(subdocument_car, cdr)
|
214
|
-
if car_object.object_id == subdocument_car.object_id
|
215
|
-
subdocument
|
216
|
-
else
|
217
|
-
(subdocument.respond_to?(:[]=) ? subdocument : subdocument.to_ary).dup.tap do |arr|
|
218
|
-
arr[car] = car_object
|
219
|
-
end
|
220
|
-
end
|
221
|
-
else
|
222
|
-
raise(TypeError, "bad subscript: #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect.chomp}")
|
223
|
-
end
|
224
|
-
end
|
225
|
-
end
|
226
|
-
end.call(document, path)
|
227
|
-
Node.new_by_type(modified_document, path)
|
145
|
+
def modified_copy(&block)
|
146
|
+
Node.new_by_type(node_ptr.modified_document_copy(node_document, &block), node_ptr)
|
147
|
+
end
|
148
|
+
|
149
|
+
def dup
|
150
|
+
modified_copy(&:dup)
|
228
151
|
end
|
229
152
|
|
230
153
|
# meta-information about the object, outside the content. used by #inspect / #pretty_print
|
154
|
+
# @return [Array<String>]
|
231
155
|
def object_group_text
|
232
|
-
|
156
|
+
[
|
157
|
+
self.class.inspect,
|
158
|
+
node_ptr.uri.to_s,
|
159
|
+
] + (node_content.respond_to?(:object_group_text) ? node_content.object_group_text : [])
|
233
160
|
end
|
234
161
|
|
235
162
|
# a string representing this node
|
236
163
|
def inspect
|
237
|
-
"\#<#{
|
164
|
+
"\#<#{object_group_text.join(' ')} #{node_content.inspect}>"
|
238
165
|
end
|
239
166
|
|
240
167
|
# pretty-prints a representation this node to the given printer
|
241
168
|
def pretty_print(q)
|
242
|
-
q.
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
}
|
169
|
+
q.text '#<'
|
170
|
+
q.text object_group_text.join(' ')
|
171
|
+
q.group_sub {
|
172
|
+
q.nest(2) {
|
173
|
+
q.breakable ' '
|
174
|
+
q.pp node_content
|
249
175
|
}
|
250
|
-
|
251
|
-
|
252
|
-
|
176
|
+
}
|
177
|
+
q.breakable ''
|
178
|
+
q.text '>'
|
253
179
|
end
|
254
180
|
|
255
181
|
# fingerprint for equality (see FingerprintHash). two nodes are equal if they are both nodes
|
256
182
|
# (regardless of type, e.g. one may be a Node and the other may be a HashNode) within equal
|
257
|
-
# documents at equal
|
183
|
+
# documents at equal pointers. note that this means two nodes with the same content may not be
|
258
184
|
# considered equal.
|
259
|
-
def
|
260
|
-
{
|
185
|
+
def jsi_fingerprint
|
186
|
+
{class: JSI::JSON::Node, node_document: node_document, node_ptr: node_ptr}
|
261
187
|
end
|
262
|
-
include FingerprintHash
|
188
|
+
include Util::FingerprintHash
|
263
189
|
end
|
264
190
|
|
265
191
|
# a JSI::JSON::Node whose content is Array-like (responds to #to_ary)
|
266
192
|
# and includes Array methods from Arraylike
|
267
193
|
class ArrayNode < Node
|
268
|
-
|
269
|
-
def each
|
270
|
-
return to_enum(__method__) { (content.respond_to?(:size) ? content : content.to_ary).size } unless block_given?
|
271
|
-
(content.respond_to?(:each_index) ? content : content.to_ary).each_index { |i| yield self[i] }
|
272
|
-
self
|
273
|
-
end
|
274
|
-
|
275
|
-
# the content of this ArrayNode, as an Array
|
276
|
-
def to_ary
|
277
|
-
to_a
|
278
|
-
end
|
279
|
-
|
280
|
-
include Enumerable
|
281
|
-
include Arraylike
|
282
|
-
|
283
|
-
# returns a jsonifiable representation of this node's content
|
284
|
-
def as_json(*opt) # needs redefined after including Enumerable
|
285
|
-
Typelike.as_json(content, *opt)
|
286
|
-
end
|
287
|
-
|
288
|
-
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
|
289
|
-
# we override these methods from Arraylike
|
290
|
-
SAFE_INDEX_ONLY_METHODS.each do |method_name|
|
291
|
-
define_method(method_name) { |*a, &b| (content.respond_to?(method_name) ? content : content.to_ary).public_send(method_name, *a, &b) }
|
292
|
-
end
|
194
|
+
include PathedArrayNode
|
293
195
|
end
|
294
196
|
|
295
197
|
# a JSI::JSON::Node whose content is Hash-like (responds to #to_hash)
|
296
198
|
# and includes Hash methods from Hashlike
|
297
199
|
class HashNode < Node
|
298
|
-
|
299
|
-
def each(&block)
|
300
|
-
return to_enum(__method__) { content.respond_to?(:size) ? content.size : content.to_ary.size } unless block_given?
|
301
|
-
if block.arity > 1
|
302
|
-
(content.respond_to?(:each_key) ? content : content.to_hash).each_key { |k| yield k, self[k] }
|
303
|
-
else
|
304
|
-
(content.respond_to?(:each_key) ? content : content.to_hash).each_key { |k| yield [k, self[k]] }
|
305
|
-
end
|
306
|
-
self
|
307
|
-
end
|
308
|
-
|
309
|
-
# the content of this HashNode, as a Hash
|
310
|
-
def to_hash
|
311
|
-
inject({}) { |h, (k, v)| h[k] = v; h }
|
312
|
-
end
|
313
|
-
|
314
|
-
include Enumerable
|
315
|
-
include Hashlike
|
316
|
-
|
317
|
-
# returns a jsonifiable representation of this node's content
|
318
|
-
def as_json(*opt) # needs redefined after including Enumerable
|
319
|
-
Typelike.as_json(content, *opt)
|
320
|
-
end
|
321
|
-
|
322
|
-
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
|
323
|
-
SAFE_KEY_ONLY_METHODS.each do |method_name|
|
324
|
-
define_method(method_name) { |*a, &b| (content.respond_to?(method_name) ? content : content.to_hash).public_send(method_name, *a, &b) }
|
325
|
-
end
|
200
|
+
include PathedHashNode
|
326
201
|
end
|
327
202
|
end
|
328
203
|
end
|