jsi 0.0.4 → 0.4.0
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 +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
|