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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9899a9ae6a661558974001eba14223c9e497849d3bf3d8e3c6ee9fffd4fcc6d
4
- data.tar.gz: 8cfcc01a0167bc251235d7376e793e50bdba92e39f706edbb7bd05386a035352
3
+ metadata.gz: 4f38a4e48bd546b340af428489a4cb5361ddf0d4b322c598cd56a7d6e69e1949
4
+ data.tar.gz: 4028ff03d8f935fc5b9f328e1adf9f094dab5621183c0d3b2b6152105b672f5d
5
5
  SHA512:
6
- metadata.gz: e4ed68443ce9a25db4ddeb18c80f4aea51b4dbb819600afc98c30ab6453ddf8dbc7589079cb7e6743802a31cc4ec53bc32b1da6796a10334e8cc2b6182d37cd4
7
- data.tar.gz: 64e8c93a243bc54e0226b8020e20b475c151d349d0b2769bf745b0ba6019f31cf6de63c4edaa36baea89da790814654e92c68154c5a63ade881bef4a18fac133
6
+ metadata.gz: a7bc8a8ac76c4deaf859681f763e031f570cb04a557549e7b77b42055bea61132792347c2037ceed11c824f7968bda55db0ff69a317c425cf9576335ebc17af3
7
+ data.tar.gz: 04d7f8d2132c20051f9780b92990d1b0b65b6a5f1fda19572b01effbdfbff22b93c8ffa1267bbaca367279fa8547feaa326d9f861763509a799aff0ecf8dc534
@@ -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 represents JSON-schemas as ruby classes, and schema instances as instances of those classes.
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
- A JSI class aims to be a fairly unobtrusive wrapper around its instance. It adds accessors for known property names, validation methods, and a few 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.
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
- # => #{<Contact fragment="#">
41
- # #{<Contact fragment="#">
42
- # "phone" => #[<JSI::SchemaClasses["1f97#/properties/phone"] fragment="#/phone">
43
- # #{<JSI::SchemaClasses["1f97#/properties/phone/items"] fragment="#/phone/0"> "location" => "home", "number" => "555"}
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 fragment="#">
79
- # "phone" => #[<JSI::SchemaClasses["1f97#/properties/phone"] fragment="#/phone">
80
- # #{<JSI::SchemaClasses["1f97#/properties/phone/items"] fragment="#/phone/0">
81
- # "number" => #[<JSI::SchemaClasses["1f97#/properties/phone/items/properties/number"] fragment="#/phone/0/number"> 5, 5, 5]
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 1f97"]
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 from which other classes representing JSON-Schemas inherit.
105
- - a JSI class refers to a class representing a schema, a subclass of JSI::Base.
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 json-schema.
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 json-schema. a JSI::Schema represents such a json-schema. a JSI class allows instantiation of such a schema.
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
 
@@ -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/base'
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.
@@ -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-Z_]/]
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
- # initializes this JSI from the given instance. the instance will be
72
- # wrapped as a {JSI::JSON::Node JSI::JSON::Node} (unless what you pass is
73
- # a Node already).
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 ancestor [JSI::Base] for internal use, specifies an ancestor
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, ancestor: nil)
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
- @ancestor = ancestor || self
84
- self.instance = instance
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 @instance.is_a?(JSI::JSON::HashNode)
131
+ if self.jsi_instance.respond_to?(:to_hash)
87
132
  extend BaseHash
88
- elsif @instance.is_a?(JSI::JSON::ArrayNode)
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 the json-schema. this is a JSI::JSON::Node.
94
- attr_reader :instance
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 ancestor of this
97
- attr_reader :ancestor
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 is at the root or was instantiated from a source that does not have
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 parents
111
- parent = @ancestor
112
- (@ancestor.instance.path.size...self.instance.path.size).map do |i|
113
- parent.tap do
114
- parent = parent[self.instance.path[i]]
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(s) are known.
182
+ # the immediate parent of this JSI. nil if there is no parent.
120
183
  #
121
184
  # @return [JSI::Base, nil]
122
- def parent
123
- parents.first
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
- derefed = instance.deref
132
- if derefed.object_id == instance.object_id
133
- self
134
- else
135
- self.class.new(derefed, ancestor: @ancestor)
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
- modified_instance = instance.modified_copy(&block)
148
- self.class.new(modified_instance, ancestor: @ancestor)
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
- instance.fragment
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.fully_validate(instance)
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.validate(instance)
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.validate!(instance)
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: self.class, instance: instance}
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 @instance to the given thing, raising if the thing is not appropriate for a JSI instance
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(:[], subscript)
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
- # this module is just a namespace for schema classes.
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
- # yields each key and value of this JSI.
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
- # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
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 [](property_name_)
370
- memoize(:[], property_name_) do |property_name|
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(property_name)
373
- property_schema = property_schema && property_schema.match_to_instance(instance[property_name])
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 !instance.key?(property_name) && property_schema && property_schema.schema_object.key?('default')
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
- class_for_schema(property_schema).new(default, ancestor: @ancestor)
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 && (instance[property_name].respond_to?(:to_hash) || instance[property_name].respond_to?(:to_ary))
384
- class_for_schema(property_schema).new(instance[property_name], ancestor: @ancestor)
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
- instance[property_name]
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
- # module extending a {JSI::Base} object when its schema instance is Array-like (responds to #to_ary)
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
- # @return [Array] an array, the same size as the instance, in which the
417
- # element at each index is the result of self[index] (see #[])
418
- def to_ary
419
- to_a
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
- include Arraylike
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
- # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
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
- # @return [Object] returns the instance's subscript value at the given index
431
- # i_. if there is a subschema defined for that index on this JSI's schema,
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
- # @param i_ the array index to subscript
434
- def [](i_)
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(i)
438
- index_schema = index_schema && index_schema.match_to_instance(instance[i])
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 !instance.each_index.to_a.include?(i) && index_schema && index_schema.schema_object.key?('default')
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
- class_for_schema(index_schema).new(default, ancestor: @ancestor)
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 && (instance[i].respond_to?(:to_hash) || instance[i].respond_to?(:to_ary))
449
- class_for_schema(index_schema).new(instance[i], ancestor: @ancestor)
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
- instance[i]
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