jsi 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +18 -15
- data/jsi.gemspec +1 -1
- data/lib/jsi.rb +5 -1
- data/lib/jsi/base.rb +222 -215
- data/lib/jsi/json-schema-fragments.rb +1 -1
- data/lib/jsi/json/node.rb +61 -146
- data/lib/jsi/json/pointer.rb +235 -41
- data/lib/jsi/pathed_node.rb +113 -0
- data/lib/jsi/schema.rb +46 -40
- data/lib/jsi/schema_classes.rb +86 -0
- data/lib/jsi/simple_wrap.rb +7 -0
- data/lib/jsi/typelike_modules.rb +39 -11
- data/lib/jsi/util.rb +3 -0
- data/lib/jsi/version.rb +1 -1
- data/test/base_array_test.rb +63 -51
- data/test/base_hash_test.rb +38 -28
- data/test/base_test.rb +54 -27
- data/test/jsi_json_arraynode_test.rb +19 -18
- data/test/jsi_json_hashnode_test.rb +29 -28
- data/test/jsi_json_node_test.rb +50 -28
- data/test/jsi_json_pointer_test.rb +13 -5
- data/test/schema_test.rb +13 -13
- data/test/spreedly_openapi_test.rb +8 -0
- data/test/test_helper.rb +3 -3
- data/test/util_test.rb +10 -10
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f38a4e48bd546b340af428489a4cb5361ddf0d4b322c598cd56a7d6e69e1949
|
4
|
+
data.tar.gz: 4028ff03d8f935fc5b9f328e1adf9f094dab5621183c0d3b2b6152105b672f5d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a7bc8a8ac76c4deaf859681f763e031f570cb04a557549e7b77b42055bea61132792347c2037ceed11c824f7968bda55db0ff69a317c425cf9576335ebc17af3
|
7
|
+
data.tar.gz: 04d7f8d2132c20051f9780b92990d1b0b65b6a5f1fda19572b01effbdfbff22b93c8ffa1267bbaca367279fa8547feaa326d9f861763509a799aff0ecf8dc534
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,23 @@
|
|
1
|
+
# v0.2.0
|
2
|
+
|
3
|
+
- JSI::PathedNode unifies interfaces of JSI::Base, JSI::JSON::Node
|
4
|
+
- JSI::Base does not (generally) wrap a JSI::JSON::Node
|
5
|
+
- some method renames to try to better indicate what a method applies to, and unreserve common names
|
6
|
+
- JSI::Base
|
7
|
+
- #instance -> #jsi_instance
|
8
|
+
- #parents -> #parent_jsis, #parent -> #parent_jsi
|
9
|
+
- JSI::Schema
|
10
|
+
- #fully_validate -> #fully_validate_instance
|
11
|
+
- #validate -> #validate_instance
|
12
|
+
- #validate! -> #validate_instance!
|
13
|
+
- improvements to methods which use a modified copy - #dup, #update/#merge
|
14
|
+
- #deref on PathedNode classes uses a block form
|
15
|
+
- JSI::PathedArrayNode, PathedHashNode
|
16
|
+
- JSI::JSON::Pointer refactoring and improvement
|
17
|
+
- Schema#new_jsi
|
18
|
+
- JSI::SimpleWrap
|
19
|
+
- more
|
20
|
+
|
1
21
|
# v0.1.0
|
2
22
|
|
3
23
|
- JSI::JSON::Pointer replaces monkey-patched-in ::JSON::Schema::Pointer
|
data/README.md
CHANGED
@@ -3,9 +3,11 @@
|
|
3
3
|
[![Build Status](https://travis-ci.org/notEthan/jsi.svg?branch=master)](https://travis-ci.org/notEthan/jsi)
|
4
4
|
[![Coverage Status](https://coveralls.io/repos/github/notEthan/jsi/badge.svg)](https://coveralls.io/github/notEthan/jsi)
|
5
5
|
|
6
|
-
JSI
|
6
|
+
JSI offers an Object-Oriented representation for JSON data using JSON Schemas. Given your JSON Schemas, JSI constructs Ruby classes which are used to instantiate your JSON data. These classes let you use JSON with all the niceties of OOP such as property accessors and application-defined instance methods.
|
7
7
|
|
8
|
-
|
8
|
+
To learn more about JSON Schema see [https://json-schema.org/]().
|
9
|
+
|
10
|
+
A JSI class aims to be a fairly unobtrusive wrapper around its instance - "instance" here meaning the JSON data which instantiate the JSON Schema. The instance is usually a Hash or an Array but may be basic types, or in fact any object. A JSI class adds accessors for property names described by its schema, schema validation, and other nice things. Mostly though, you use a JSI as you would use its underlying data, calling the same methods (e.g. `#[]`, `#map`, `#repeated_permutation`) and passing it to anything that duck-types expecting #to_ary or #to_hash.
|
9
11
|
|
10
12
|
## Example
|
11
13
|
|
@@ -37,10 +39,11 @@ This definition gives you not just the Contact class, but classes for the whole
|
|
37
39
|
|
38
40
|
```ruby
|
39
41
|
bill = Contact.new('name' => 'bill', 'phone' => [{'location' => 'home', 'number' => '555'}], 'nickname' => 'big b')
|
40
|
-
|
41
|
-
# #{<Contact
|
42
|
-
# "
|
43
|
-
#
|
42
|
+
|
43
|
+
# => #{<Contact Hash>
|
44
|
+
# "name" => "bill",
|
45
|
+
# "phone" => #[<JSI::SchemaClasses["23d8#/properties/phone"] Array>
|
46
|
+
# #{<JSI::SchemaClasses["23d8#/properties/phone/items"] Hash> "location" => "home", "number" => "555"}
|
44
47
|
# ],
|
45
48
|
# "nickname" => "big b"
|
46
49
|
# }
|
@@ -75,15 +78,15 @@ bill.validate
|
|
75
78
|
|
76
79
|
```ruby
|
77
80
|
bad = Contact.new('phone' => [{'number' => [5, 5, 5]}])
|
78
|
-
# => #{<Contact
|
79
|
-
# "phone" => #[<JSI::SchemaClasses["
|
80
|
-
# #{<JSI::SchemaClasses["
|
81
|
-
# "number" => #[<JSI::SchemaClasses["
|
81
|
+
# => #{<Contact Hash>
|
82
|
+
# "phone" => #[<JSI::SchemaClasses["23d8#/properties/phone"] Array>
|
83
|
+
# #{<JSI::SchemaClasses["23d8#/properties/phone/items"] Hash>
|
84
|
+
# "number" => #[<JSI::SchemaClasses["23d8#/properties/phone/items/properties/number"] Array> 5, 5, 5]
|
82
85
|
# }
|
83
86
|
# ]
|
84
87
|
# }
|
85
88
|
bad.phone.fully_validate
|
86
|
-
# => ["The property '#/0/number' of type array did not match the following type: string in schema
|
89
|
+
# => ["The property '#/0/number' of type array did not match the following type: string in schema 23d8"]
|
87
90
|
```
|
88
91
|
|
89
92
|
These validations are done by the [`json-schema` gem](https://github.com/ruby-json-schema/json-schema) - JSI does not do validations on its own.
|
@@ -101,12 +104,12 @@ There's plenty more JSI has to offer, but this should give you a pretty good ide
|
|
101
104
|
|
102
105
|
## Terminology and Concepts
|
103
106
|
|
104
|
-
- JSI::Base is the base class
|
105
|
-
- a JSI class
|
107
|
+
- `JSI::Base` is the base class for each JSI class representing a JSON Schema.
|
108
|
+
- a "JSI class" is a subclass of `JSI::Base` representing a JSON schema.
|
106
109
|
- "instance" is a term that is significantly overloaded in this space, so documentation will attempt to be clear what kind of instance is meant:
|
107
|
-
- a schema instance refers broadly to a data structure that is described by a
|
110
|
+
- a schema instance refers broadly to a data structure that is described by a JSON schema.
|
108
111
|
- a JSI instance (or just "a JSI") is a ruby object instantiating a JSI class. it has a method #instance which contains the underlying data.
|
109
|
-
- a schema refers to a
|
112
|
+
- a schema refers to a JSON schema. a `JSI::Schema` represents such a schema. a JSI class allows instantiation of a schema as a JSI instance.
|
110
113
|
|
111
114
|
## JSI classes
|
112
115
|
|
data/jsi.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = "jsi"
|
7
7
|
spec.version = JSI::VERSION
|
8
8
|
spec.authors = ["Ethan"]
|
9
|
-
spec.email = ["ethan@unth"]
|
9
|
+
spec.email = ["ethan@unth.net"]
|
10
10
|
|
11
11
|
spec.summary = "JSI: JSON-Schema instantiation"
|
12
12
|
spec.description = "JSI represents json-schemas as ruby classes and json-schema instances as instances of those classes"
|
data/lib/jsi.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "jsi/version"
|
2
2
|
require "pp"
|
3
|
+
require "set"
|
3
4
|
require "jsi/json-schema-fragments"
|
4
5
|
require "jsi/util"
|
5
6
|
|
@@ -10,6 +11,7 @@ module JSI
|
|
10
11
|
end
|
11
12
|
|
12
13
|
autoload :JSON, 'jsi/json'
|
14
|
+
autoload :PathedNode, 'jsi/pathed_node'
|
13
15
|
autoload :Typelike, 'jsi/typelike_modules'
|
14
16
|
autoload :Hashlike, 'jsi/typelike_modules'
|
15
17
|
autoload :Arraylike, 'jsi/typelike_modules'
|
@@ -17,9 +19,11 @@ module JSI
|
|
17
19
|
autoload :Base, 'jsi/base'
|
18
20
|
autoload :BaseArray, 'jsi/base'
|
19
21
|
autoload :BaseHash, 'jsi/base'
|
20
|
-
autoload :SchemaClasses, 'jsi/
|
22
|
+
autoload :SchemaClasses, 'jsi/schema_classes'
|
21
23
|
autoload :JSICoder, 'jsi/jsi_coder'
|
22
24
|
|
25
|
+
autoload :SimpleWrap, 'jsi/simple_wrap'
|
26
|
+
|
23
27
|
# @return [Class subclassing JSI::Base] a JSI class which represents the
|
24
28
|
# given schema. instances of the class represent JSON Schema instances
|
25
29
|
# for the given schema.
|
data/lib/jsi/base.rb
CHANGED
@@ -13,8 +13,11 @@ module JSI
|
|
13
13
|
class Base
|
14
14
|
include Memoize
|
15
15
|
include Enumerable
|
16
|
+
include PathedNode
|
16
17
|
|
17
18
|
class << self
|
19
|
+
# is the constant JSI::SchemaClasses::{self.schema_classes_const_name} defined?
|
20
|
+
# (if so, we will prefer to use something more human-readable than that ugly mess.)
|
18
21
|
attr_accessor :in_schema_classes
|
19
22
|
|
20
23
|
# @return [String] absolute schema_id of the schema this class represents.
|
@@ -53,7 +56,7 @@ module JSI
|
|
53
56
|
# schema_id. only used if the class is not assigned to another constant.
|
54
57
|
def schema_classes_const_name
|
55
58
|
name = schema.schema_id.gsub(/[^\w]/, '_')
|
56
|
-
name = 'X' + name unless name[/\A[a-zA-
|
59
|
+
name = 'X' + name unless name[/\A[a-zA-Z]/]
|
57
60
|
name = name[0].upcase + name[1..-1]
|
58
61
|
name
|
59
62
|
end
|
@@ -68,33 +71,85 @@ module JSI
|
|
68
71
|
end
|
69
72
|
end
|
70
73
|
|
71
|
-
#
|
72
|
-
#
|
73
|
-
|
74
|
+
# NOINSTANCE is a magic value passed to #initialize when instantiating a JSI
|
75
|
+
# from a document and JSON Pointer.
|
76
|
+
NOINSTANCE = Object.new.tap { |o| [:inspect, :to_s].each(&(-> (s, m) { o.define_singleton_method(m) { s } }.curry.([JSI::Base.name, 'NOINSTANCE'].join('::')))) }
|
77
|
+
|
78
|
+
# initializes this JSI from the given instance - instance is most commonly
|
79
|
+
# a parsed JSON document consisting of Hash, Array, or sometimes a basic
|
80
|
+
# type, but this is in no way enforced and a JSI may wrap any object.
|
74
81
|
#
|
75
82
|
# @param instance [Object] the JSON Schema instance being represented
|
76
|
-
# @param
|
83
|
+
# @param jsi_document [Object] for internal use. the instance may be specified as a
|
84
|
+
# node in the `jsi_document` param, pointed to by `jsi_ptr`. the param `instance`
|
85
|
+
# MUST be `NOINSTANCE` to use the jsi_document + jsi_ptr form. `jsi_document` MUST
|
86
|
+
# NOT be passed if `instance` is anything other than `NOINSTANCE`.
|
87
|
+
# @param jsi_ptr [JSI::JSON::Pointer] for internal use. a JSON pointer specifying
|
88
|
+
# the path of this instance in the `jsi_document` param. `jsi_ptr` must be passed
|
89
|
+
# iff `jsi_document` is passed, i.e. when `instance` is `NOINSTANCE`
|
90
|
+
# @param ancestor_jsi [JSI::Base] for internal use, specifies an ancestor_jsi
|
77
91
|
# from which this JSI originated to calculate #parents
|
78
|
-
def initialize(instance,
|
92
|
+
def initialize(instance, jsi_document: nil, jsi_ptr: nil, ancestor_jsi: nil)
|
79
93
|
unless respond_to?(:schema)
|
80
94
|
raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #schema. please use JSI.class_for_schema")
|
81
95
|
end
|
82
96
|
|
83
|
-
|
84
|
-
|
97
|
+
if instance.is_a?(JSI::Base)
|
98
|
+
raise(TypeError, "assigning another JSI::Base instance to #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
|
99
|
+
elsif instance.is_a?(JSI::Schema)
|
100
|
+
raise(TypeError, "assigning a schema to #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
|
101
|
+
end
|
102
|
+
|
103
|
+
if instance == NOINSTANCE
|
104
|
+
@jsi_document = jsi_document
|
105
|
+
unless jsi_ptr.is_a?(JSI::JSON::Pointer)
|
106
|
+
raise(TypeError, "jsi_ptr must be a JSI::JSON::Pointer; got: #{jsi_ptr.inspect}")
|
107
|
+
end
|
108
|
+
@jsi_ptr = jsi_ptr
|
109
|
+
else
|
110
|
+
raise(Bug, 'incorrect usage') if jsi_document || jsi_ptr || ancestor_jsi
|
111
|
+
if instance.is_a?(PathedNode)
|
112
|
+
@jsi_document = instance.document_root_node
|
113
|
+
# this can result in the unusual situation where ancestor_jsi is nil, though jsi_ptr is not root.
|
114
|
+
# #document_root_node will then return a JSI::JSON::Pointer instead of a root JSI.
|
115
|
+
@jsi_ptr = instance.node_ptr
|
116
|
+
else
|
117
|
+
@jsi_document = instance
|
118
|
+
@jsi_ptr = JSI::JSON::Pointer.new([])
|
119
|
+
end
|
120
|
+
end
|
121
|
+
if ancestor_jsi
|
122
|
+
if !ancestor_jsi.is_a?(JSI::Base)
|
123
|
+
raise(TypeError, "ancestor_jsi must be a JSI::Base; got: #{ancestor_jsi.inspect}")
|
124
|
+
end
|
125
|
+
if !ancestor_jsi.jsi_ptr.contains?(@jsi_ptr)
|
126
|
+
raise(Bug, "ancestor_jsi ptr #{ancestor_jsi.jsi_ptr.inspect} is not ancestor of #{@jsi_ptr.inspect}")
|
127
|
+
end
|
128
|
+
end
|
129
|
+
@ancestor_jsi = ancestor_jsi
|
85
130
|
|
86
|
-
if
|
131
|
+
if self.jsi_instance.respond_to?(:to_hash)
|
87
132
|
extend BaseHash
|
88
|
-
elsif
|
133
|
+
elsif self.jsi_instance.respond_to?(:to_ary)
|
89
134
|
extend BaseArray
|
90
135
|
end
|
91
136
|
end
|
92
137
|
|
93
|
-
# the instance of
|
94
|
-
attr_reader :
|
138
|
+
# document containing the instance of this JSI
|
139
|
+
attr_reader :jsi_document
|
140
|
+
|
141
|
+
# JSI::JSON::Pointer pointing to this JSI's instance within the jsi_document
|
142
|
+
attr_reader :jsi_ptr
|
95
143
|
|
96
|
-
# a JSI which is an
|
97
|
-
attr_reader :
|
144
|
+
# a JSI which is an ancestor_jsi of this
|
145
|
+
attr_reader :ancestor_jsi
|
146
|
+
|
147
|
+
alias_method :node_document, :jsi_document
|
148
|
+
alias_method :node_ptr, :jsi_ptr
|
149
|
+
|
150
|
+
# the instance of the json-schema
|
151
|
+
alias_method :jsi_instance, :node_content
|
152
|
+
alias_method :instance, :node_content
|
98
153
|
|
99
154
|
# each is overridden by BaseHash or BaseArray when appropriate. the base
|
100
155
|
# #each is not actually implemented, along with all the methods of Enumerable.
|
@@ -103,37 +158,93 @@ module JSI
|
|
103
158
|
end
|
104
159
|
|
105
160
|
# an array of JSI instances above this one in the document. empty if this
|
106
|
-
# JSI
|
107
|
-
# a document (e.g. a plain hash or array).
|
161
|
+
# JSI does not have a known ancestor.
|
108
162
|
#
|
109
163
|
# @return [Array<JSI::Base>]
|
110
|
-
def
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
164
|
+
def parent_jsis
|
165
|
+
ancestor_jsi = @ancestor_jsi || self
|
166
|
+
parent = ancestor_jsi
|
167
|
+
|
168
|
+
(ancestor_jsi.jsi_ptr.reference_tokens.size...self.jsi_ptr.reference_tokens.size).map do |i|
|
169
|
+
current = parent
|
170
|
+
parent = parent[self.jsi_ptr.reference_tokens[i]]
|
171
|
+
if current.is_a?(JSI::Base)
|
172
|
+
current
|
173
|
+
else
|
174
|
+
# sometimes after a deref, we may end up with parents whose schema we do not know.
|
175
|
+
# TODO this is kinda crap; hopefully we can remove it along with deref instantiating
|
176
|
+
# a deref ptr as the same JSI class it is
|
177
|
+
SimpleWrap.new(NOINSTANCE, jsi_document: jsi_document, jsi_ptr: jsi_ptr.take(i), ancestor_jsi: @ancestor_jsi)
|
115
178
|
end
|
116
179
|
end.reverse
|
117
180
|
end
|
118
181
|
|
119
|
-
# the immediate parent of this JSI. nil if no parent
|
182
|
+
# the immediate parent of this JSI. nil if there is no parent.
|
120
183
|
#
|
121
184
|
# @return [JSI::Base, nil]
|
122
|
-
def
|
123
|
-
|
185
|
+
def parent_jsi
|
186
|
+
parent_jsis.first
|
187
|
+
end
|
188
|
+
|
189
|
+
# @return [JSI::PathedNode] a pathed node at the root of the document. this is generally a JSI::Base
|
190
|
+
# but may be a JSI::JSON::Node in unusual circumstances.
|
191
|
+
def document_root_node
|
192
|
+
if @jsi_ptr.root?
|
193
|
+
self
|
194
|
+
elsif @ancestor_jsi
|
195
|
+
@ancestor_jsi.document_root_node
|
196
|
+
elsif instance.is_a?(PathedNode)
|
197
|
+
instance.document_root_node
|
198
|
+
else
|
199
|
+
JSI::JSON::Node.new_doc(@jsi_document)
|
200
|
+
end
|
124
201
|
end
|
125
202
|
|
203
|
+
# @return [JSI::PathedNode]
|
204
|
+
def parent_node
|
205
|
+
if @jsi_ptr.root?
|
206
|
+
nil
|
207
|
+
elsif @ancestor_jsi
|
208
|
+
parent_jsis.first.tap do |parent_node|
|
209
|
+
raise(Bug, 'is @ancestor_jsi == self? it should not be') if parent_node.nil?
|
210
|
+
raise(Bug, "parent_node not PathedNode: #{parent_node.pretty_inspect.chomp}") unless parent_node.is_a?(JSI::PathedNode)
|
211
|
+
end
|
212
|
+
elsif instance.is_a?(PathedNode)
|
213
|
+
instance.parent_node
|
214
|
+
else
|
215
|
+
JSI::JSON::Node.new_by_type(@jsi_document, @jsi_ptr.parent)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# @deprecated
|
220
|
+
alias_method :parents, :parent_jsis
|
221
|
+
# @deprecated
|
222
|
+
alias_method :parent, :parent_jsi
|
223
|
+
|
126
224
|
# if this JSI is a $ref then the $ref is followed. otherwise this JSI
|
127
225
|
# is returned.
|
128
226
|
#
|
227
|
+
# @yield [JSI::Base] if a block is given (optional), this will yield a deref'd JSI. if this
|
228
|
+
# JSI is not a $ref object, the block is not called. if we are a $ref which cannot be followed
|
229
|
+
# (e.g. a $ref to an external document, which is not yet supported), the block is not called.
|
129
230
|
# @return [JSI::Base, self]
|
130
|
-
def deref
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
231
|
+
def deref(&block)
|
232
|
+
node_ptr_deref do |deref_ptr|
|
233
|
+
jsi_from_root = deref_ptr.evaluate(document_root_node)
|
234
|
+
if jsi_from_root.is_a?(JSI::Base)
|
235
|
+
return jsi_from_root.tap(&(block || Util::NOOP))
|
236
|
+
else
|
237
|
+
# TODO I want to get rid of this ... just return jsi_from_root whatever it is
|
238
|
+
# NOTE when I get rid of this, simplify #parent_jsis too
|
239
|
+
if @ancestor_jsi && @ancestor_jsi.jsi_ptr.contains?(deref_ptr)
|
240
|
+
derefed = self.class.new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: deref_ptr, ancestor_jsi: @ancestor_jsi)
|
241
|
+
else
|
242
|
+
derefed = self.class.new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: deref_ptr)
|
243
|
+
end
|
244
|
+
return derefed.tap(&(block || Util::NOOP))
|
245
|
+
end
|
136
246
|
end
|
247
|
+
return self
|
137
248
|
end
|
138
249
|
|
139
250
|
# yields the content of the underlying instance. the block must result in
|
@@ -144,22 +255,33 @@ module JSI
|
|
144
255
|
# in a (nondestructively) modified copy of this.
|
145
256
|
# @return [JSI::Base subclass the same as self] the modified copy of self
|
146
257
|
def modified_copy(&block)
|
147
|
-
|
148
|
-
|
258
|
+
if @ancestor_jsi
|
259
|
+
raise(Bug, 'bad @ancestor_jsi') if @ancestor_jsi.object_id == self.object_id
|
260
|
+
|
261
|
+
modified_ancestor = @ancestor_jsi.modified_copy do |anc|
|
262
|
+
mod_anc = @jsi_ptr.ptr_relative_to(@ancestor_jsi.jsi_ptr).modified_document_copy(anc, &block)
|
263
|
+
mod_anc
|
264
|
+
end
|
265
|
+
self.class.new(Base::NOINSTANCE, jsi_document: modified_ancestor.jsi_document, jsi_ptr: @jsi_ptr, ancestor_jsi: modified_ancestor)
|
266
|
+
else
|
267
|
+
modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
|
268
|
+
self.class.new(Base::NOINSTANCE, jsi_document: modified_document, jsi_ptr: @jsi_ptr)
|
269
|
+
end
|
149
270
|
end
|
150
271
|
|
272
|
+
# @return [String] the fragment representation of a pointer to this JSI's instance within its document
|
151
273
|
def fragment
|
152
|
-
|
274
|
+
@jsi_ptr.fragment
|
153
275
|
end
|
154
276
|
|
155
277
|
# @return [Array<String>] array of schema validation error messages for this instance
|
156
278
|
def fully_validate
|
157
|
-
schema.
|
279
|
+
schema.fully_validate_instance(instance)
|
158
280
|
end
|
159
281
|
|
160
282
|
# @return [true, false] whether the instance validates against its schema
|
161
283
|
def validate
|
162
|
-
schema.
|
284
|
+
schema.validate_instance(instance)
|
163
285
|
end
|
164
286
|
|
165
287
|
# @return [true] if this method does not raise, it returns true to
|
@@ -167,7 +289,11 @@ module JSI
|
|
167
289
|
# @raise [::JSON::Schema::ValidationError] raises if the instance has
|
168
290
|
# validation errors
|
169
291
|
def validate!
|
170
|
-
schema.
|
292
|
+
schema.validate_instance!(instance)
|
293
|
+
end
|
294
|
+
|
295
|
+
def dup
|
296
|
+
modified_copy(&:dup)
|
171
297
|
end
|
172
298
|
|
173
299
|
# @return [String] a string representing this JSI, indicating its class
|
@@ -194,7 +320,7 @@ module JSI
|
|
194
320
|
|
195
321
|
# @return [String] the instance's object_group_text
|
196
322
|
def object_group_text
|
197
|
-
instance.object_group_text
|
323
|
+
instance.respond_to?(:object_group_text) ? instance.object_group_text : instance.class.inspect
|
198
324
|
end
|
199
325
|
|
200
326
|
# @return [Object] a jsonifiable representation of the instance
|
@@ -202,35 +328,20 @@ module JSI
|
|
202
328
|
Typelike.as_json(instance, *opt)
|
203
329
|
end
|
204
330
|
|
205
|
-
# @return [Object] an opaque fingerprint of this JSI for FingerprintHash
|
331
|
+
# @return [Object] an opaque fingerprint of this JSI for FingerprintHash. JSIs are equal
|
332
|
+
# if their instances are equal, and if the JSIs are of the same JSI class or subclass.
|
206
333
|
def fingerprint
|
207
|
-
{class:
|
334
|
+
{class: jsi_class, jsi_document: jsi_document, jsi_ptr: jsi_ptr}
|
208
335
|
end
|
209
336
|
include FingerprintHash
|
210
337
|
|
211
338
|
private
|
212
339
|
|
213
|
-
# assigns
|
214
|
-
# @param thing [Object] a JSON schema instance for this class's schema
|
215
|
-
def instance=(thing)
|
216
|
-
if instance_variable_defined?(:@instance)
|
217
|
-
raise(JSI::Bug, "overwriting instance is not supported")
|
218
|
-
end
|
219
|
-
if thing.is_a?(Base)
|
220
|
-
warn "assigning instance to a Base instance is incorrect. received: #{thing.pretty_inspect.chomp}"
|
221
|
-
@instance = thing.instance
|
222
|
-
elsif thing.is_a?(JSI::JSON::Node)
|
223
|
-
@instance = thing
|
224
|
-
else
|
225
|
-
@instance = JSI::JSON::Node.new_doc(thing)
|
226
|
-
end
|
227
|
-
end
|
228
|
-
|
229
|
-
# assigns a subscript, taking care of memoization and unwrapping a JSI if given.
|
340
|
+
# assigns a subscript, unwrapping a JSI if given.
|
230
341
|
# @param subscript [Object] the bit between the [ and ]
|
231
342
|
# @param value [JSI::Base, Object] the value to be assigned
|
232
343
|
def subscript_assign(subscript, value)
|
233
|
-
clear_memo(:[]
|
344
|
+
clear_memo(:[])
|
234
345
|
if value.is_a?(Base)
|
235
346
|
instance[subscript] = value.instance
|
236
347
|
else
|
@@ -245,145 +356,44 @@ module JSI
|
|
245
356
|
end
|
246
357
|
end
|
247
358
|
|
248
|
-
#
|
249
|
-
module SchemaClasses
|
250
|
-
extend Memoize
|
251
|
-
|
252
|
-
# JSI::SchemaClasses[schema_id] returns a class for the schema with the
|
253
|
-
# given id, the same class as returned from JSI.class_for_schema.
|
254
|
-
#
|
255
|
-
# @param schema_id [String] absolute schema id as returned by {JSI::Schema#schema_id}
|
256
|
-
# @return [Class subclassing JSI::Base] the class for that schema
|
257
|
-
def self.[](schema_id)
|
258
|
-
@classes_by_id[schema_id]
|
259
|
-
end
|
260
|
-
@classes_by_id = {}
|
261
|
-
end
|
262
|
-
|
263
|
-
# see {JSI.class_for_schema}
|
264
|
-
def SchemaClasses.class_for_schema(schema_object)
|
265
|
-
memoize(:class_for_schema, JSI::Schema.from_object(schema_object)) do |schema_|
|
266
|
-
begin
|
267
|
-
begin
|
268
|
-
Class.new(Base).instance_exec(schema_) do |schema|
|
269
|
-
begin
|
270
|
-
include(JSI::SchemaClasses.module_for_schema(schema))
|
271
|
-
|
272
|
-
SchemaClasses.instance_exec(self) { |klass| @classes_by_id[klass.schema_id] = klass }
|
273
|
-
|
274
|
-
self
|
275
|
-
end
|
276
|
-
end
|
277
|
-
end
|
278
|
-
end
|
279
|
-
end
|
280
|
-
end
|
281
|
-
|
282
|
-
# a module for the given schema, with accessor methods for any object
|
283
|
-
# property names the schema identifies. also has class and instance
|
284
|
-
# methods called #schema to access the {JSI::Schema} this module
|
285
|
-
# represents.
|
286
|
-
#
|
287
|
-
# accessor methods are defined on these modules so that methods can be
|
288
|
-
# defined on {JSI.class_for_schema} classes without method redefinition
|
289
|
-
# warnings. additionally, these overriding instance methods can call
|
290
|
-
# `super` to invoke the normal accessor behavior.
|
291
|
-
#
|
292
|
-
# no property names that are the same as existing method names on the JSI
|
293
|
-
# class will be defined. users should use #[] and #[]= to access properties
|
294
|
-
# whose names conflict with existing methods.
|
295
|
-
def SchemaClasses.module_for_schema(schema_object)
|
296
|
-
memoize(:module_for_schema, JSI::Schema.from_object(schema_object)) do |schema_|
|
297
|
-
Module.new.tap do |m|
|
298
|
-
m.instance_exec(schema_) do |schema|
|
299
|
-
define_method(:schema) { schema }
|
300
|
-
define_singleton_method(:schema) { schema }
|
301
|
-
define_singleton_method(:included) do |includer|
|
302
|
-
includer.send(:define_singleton_method, :schema) { schema }
|
303
|
-
end
|
304
|
-
|
305
|
-
define_singleton_method(:schema_id) do
|
306
|
-
schema.schema_id
|
307
|
-
end
|
308
|
-
define_singleton_method(:inspect) do
|
309
|
-
%Q(#<Module for Schema: #{schema_id}>)
|
310
|
-
end
|
311
|
-
|
312
|
-
instance_method_modules = [m, Base, BaseArray, BaseHash]
|
313
|
-
instance_methods = instance_method_modules.map do |mod|
|
314
|
-
mod.instance_methods + mod.private_instance_methods
|
315
|
-
end.inject(Set.new, &:|)
|
316
|
-
accessors_to_define = schema.described_object_property_names.map(&:to_s) - instance_methods.map(&:to_s)
|
317
|
-
accessors_to_define.each do |property_name|
|
318
|
-
define_method(property_name) do
|
319
|
-
if respond_to?(:[])
|
320
|
-
self[property_name]
|
321
|
-
else
|
322
|
-
raise(NoMethodError, "instance does not respond to []; cannot call reader `#{property_name}' for: #{pretty_inspect.chomp}")
|
323
|
-
end
|
324
|
-
end
|
325
|
-
define_method("#{property_name}=") do |value|
|
326
|
-
if respond_to?(:[]=)
|
327
|
-
self[property_name] = value
|
328
|
-
else
|
329
|
-
raise(NoMethodError, "instance does not respond to []=; cannot call writer `#{property_name}=' for: #{pretty_inspect.chomp}")
|
330
|
-
end
|
331
|
-
end
|
332
|
-
end
|
333
|
-
end
|
334
|
-
end
|
335
|
-
end
|
336
|
-
end
|
337
|
-
|
338
|
-
# module extending a {JSI::Base} object when its schema instance is Hash-like (responds to #to_hash)
|
359
|
+
# module extending a {JSI::Base} object when its instance is Hash-like (responds to #to_hash)
|
339
360
|
module BaseHash
|
340
|
-
|
341
|
-
# each yielded key is the same as a key of the instance, and each yielded
|
342
|
-
# value is the result of self[key] (see #[]).
|
343
|
-
# returns an Enumerator if no block is given.
|
344
|
-
# @yield [Object, Object] each key and value of this JSI hash
|
345
|
-
# @return [self, Enumerator]
|
346
|
-
def each
|
347
|
-
return to_enum(__method__) { instance.size } unless block_given?
|
348
|
-
instance.each_key { |k| yield(k, self[k]) }
|
349
|
-
self
|
350
|
-
end
|
351
|
-
|
352
|
-
# @return [Hash] a hash in which each key is a key of the instance hash and
|
353
|
-
# each value is the result of self[key] (see #[]).
|
354
|
-
def to_hash
|
355
|
-
inject({}) { |h, (k, v)| h[k] = v; h }
|
356
|
-
end
|
357
|
-
|
358
|
-
include Hashlike
|
361
|
+
include PathedHashNode
|
359
362
|
|
360
|
-
|
361
|
-
SAFE_KEY_ONLY_METHODS.each do |method_name|
|
362
|
-
define_method(method_name) { |*a, &b| instance.public_send(method_name, *a, &b) }
|
363
|
-
end
|
363
|
+
alias_method :jsi_instance_hash_pubsend, :node_content_hash_pubsend
|
364
364
|
|
365
|
+
# @param property_name [String, Object] the property name to subscript
|
365
366
|
# @return [JSI::Base, Object] the instance's subscript value at the given
|
366
367
|
# key property_name_. if there is a subschema defined for that property
|
367
368
|
# on this JSI's schema, returns the instance's subscript as a JSI
|
368
369
|
# instiation of that subschema.
|
369
|
-
def [](
|
370
|
-
|
370
|
+
def [](property_name)
|
371
|
+
instance_property_key_ = jsi_instance_hash_pubsend(:key?, property_name)
|
372
|
+
if !instance_property_key_
|
373
|
+
deref do |deref_jsi|
|
374
|
+
return deref_jsi[property_name]
|
375
|
+
end
|
376
|
+
end
|
377
|
+
instance_property_value_ = jsi_instance_sub(property_name)
|
378
|
+
memoize(:[], property_name, instance_property_value_, instance_property_key_) do |property_name_, instance_property_value, instance_property_key|
|
371
379
|
begin
|
372
|
-
property_schema = schema.subschema_for_property(
|
373
|
-
property_schema = property_schema && property_schema.match_to_instance(
|
380
|
+
property_schema = schema.subschema_for_property(property_name_)
|
381
|
+
property_schema = property_schema && property_schema.match_to_instance(instance_property_value)
|
374
382
|
|
375
|
-
if !
|
383
|
+
if !instance_property_key && property_schema && property_schema.schema_object.key?('default')
|
376
384
|
# use the default value
|
377
385
|
default = property_schema.schema_object['default']
|
378
386
|
if default.respond_to?(:to_hash) || default.respond_to?(:to_ary)
|
379
|
-
|
387
|
+
# we are using #dup so that we get a modified copy of self, in which we set dup[property_name_]=default.
|
388
|
+
# this avoids duplication of code with #modified_copy and below in #[] to handle pathing and such.
|
389
|
+
dup.tap { |o| o[property_name_] = default }[property_name_]
|
380
390
|
else
|
381
391
|
default
|
382
392
|
end
|
383
|
-
elsif property_schema && (
|
384
|
-
class_for_schema(property_schema).new(
|
393
|
+
elsif property_schema && (instance_property_value.respond_to?(:to_hash) || instance_property_value.respond_to?(:to_ary))
|
394
|
+
class_for_schema(property_schema).new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: @jsi_ptr[property_name_], ancestor_jsi: @ancestor_jsi || self)
|
385
395
|
else
|
386
|
-
|
396
|
+
instance_property_value
|
387
397
|
end
|
388
398
|
end
|
389
399
|
end
|
@@ -398,57 +408,46 @@ module JSI
|
|
398
408
|
def []=(property_name, value)
|
399
409
|
subscript_assign(property_name, value)
|
400
410
|
end
|
401
|
-
end
|
402
411
|
|
403
|
-
|
404
|
-
module BaseArray
|
405
|
-
# yields each element. each yielded element is the result of self[index]
|
406
|
-
# for each index of the instance (see #[]).
|
407
|
-
# returns an Enumerator if no block is given.
|
408
|
-
# @yield [Object] each element of this JSI array
|
409
|
-
# @return [self, Enumerator]
|
410
|
-
def each
|
411
|
-
return to_enum(__method__) { instance.size } unless block_given?
|
412
|
-
instance.each_index { |i| yield(self[i]) }
|
413
|
-
self
|
414
|
-
end
|
412
|
+
private
|
415
413
|
|
416
|
-
# @
|
417
|
-
#
|
418
|
-
def
|
419
|
-
|
414
|
+
# @param token [String, Object]
|
415
|
+
# @return [Object]
|
416
|
+
def jsi_instance_sub(token)
|
417
|
+
jsi_instance_hash_pubsend(:[], token)
|
420
418
|
end
|
419
|
+
end
|
421
420
|
|
422
|
-
|
421
|
+
# module extending a {JSI::Base} object when its instance is Array-like (responds to #to_ary)
|
422
|
+
module BaseArray
|
423
|
+
include PathedArrayNode
|
423
424
|
|
424
|
-
|
425
|
-
# we override these methods from Arraylike
|
426
|
-
SAFE_INDEX_ONLY_METHODS.each do |method_name|
|
427
|
-
define_method(method_name) { |*a, &b| instance.public_send(method_name, *a, &b) }
|
428
|
-
end
|
425
|
+
alias_method :jsi_instance_ary_pubsend, :node_content_ary_pubsend
|
429
426
|
|
430
|
-
# @
|
431
|
-
#
|
427
|
+
# @param i [Integer] the array index to subscript
|
428
|
+
# @return [JSI::Base, Object] the instance's subscript value at the given index
|
429
|
+
# i. if there is a subschema defined for that index on this JSI's schema,
|
432
430
|
# returns the instance's subscript as a JSI instiation of that subschema.
|
433
|
-
|
434
|
-
|
435
|
-
memoize(:[], i_) do |i|
|
431
|
+
def [](i)
|
432
|
+
memoize(:[], i, jsi_instance_sub(i), jsi_instance_ary_pubsend(:each_index).to_a.include?(i)) do |i_, instance_idx_value, i_in_range|
|
436
433
|
begin
|
437
|
-
index_schema = schema.subschema_for_index(
|
438
|
-
index_schema = index_schema && index_schema.match_to_instance(
|
434
|
+
index_schema = schema.subschema_for_index(i_)
|
435
|
+
index_schema = index_schema && index_schema.match_to_instance(instance_idx_value)
|
439
436
|
|
440
|
-
if !
|
437
|
+
if !i_in_range && index_schema && index_schema.schema_object.key?('default')
|
441
438
|
# use the default value
|
442
439
|
default = index_schema.schema_object['default']
|
443
440
|
if default.respond_to?(:to_hash) || default.respond_to?(:to_ary)
|
444
|
-
|
441
|
+
# we are using #dup so that we get a modified copy of self, in which we set dup[i]=default.
|
442
|
+
# this avoids duplication of code with #modified_copy and below in #[] to handle pathing and such.
|
443
|
+
dup.tap { |o| o[i_] = default }[i_]
|
445
444
|
else
|
446
445
|
default
|
447
446
|
end
|
448
|
-
elsif index_schema && (
|
449
|
-
class_for_schema(index_schema).new(
|
447
|
+
elsif index_schema && (instance_idx_value.respond_to?(:to_hash) || instance_idx_value.respond_to?(:to_ary))
|
448
|
+
class_for_schema(index_schema).new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: @jsi_ptr[i_], ancestor_jsi: @ancestor_jsi || self)
|
450
449
|
else
|
451
|
-
|
450
|
+
instance_idx_value
|
452
451
|
end
|
453
452
|
end
|
454
453
|
end
|
@@ -461,5 +460,13 @@ module JSI
|
|
461
460
|
def []=(i, value)
|
462
461
|
subscript_assign(i, value)
|
463
462
|
end
|
463
|
+
|
464
|
+
private
|
465
|
+
|
466
|
+
# @param token [Integer]
|
467
|
+
# @return [Object]
|
468
|
+
def jsi_instance_sub(token)
|
469
|
+
jsi_instance_ary_pubsend(:[], token)
|
470
|
+
end
|
464
471
|
end
|
465
472
|
end
|