jsi 0.3.0 → 0.4.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: 44d076885e1c4e9335d334262468bc6a8e21840801af3cf7a93dde6d449fcaac
4
- data.tar.gz: 9a454e1f36b9fab3b03b1e51fea21c81838483bb515aa1fb109655d7008ba209
3
+ metadata.gz: 42c370e01587ddef37138ffb1aacc4cd6240de94c0bbd9d61e96cafcb6ba829d
4
+ data.tar.gz: 9e26e8e2e8d78a05018302beff7190f33e0243d4f6899f5c3f1d046ccaaa3869
5
5
  SHA512:
6
- metadata.gz: 9e7ddc4b0388d64a99de5b4a79e22bbbcc9067b207e6bbcc056ddcb39c63c574b82b4ceee715d44a89c51c39006587550e60fee01e4b5b6093b70db9206c49b7
7
- data.tar.gz: d16369c5b70ce80240e50d7b7f558a3c6e82099c03d7a8928e4a064489bb08f9878dc356861afbfc798a0c5a302892a8a2d2c2e16e4b220fca05a79846c95eb6
6
+ metadata.gz: 75c5220df82e30b9332fba96d10463ee07cc52ff60cf8dc44963dd70c2472d279180659ab782098b5bcc9066dd000e07f60b1e7fa616215bfe6e6bfa11eddcb4
7
+ data.tar.gz: 848ac231235bebf262502fb7966265ab6c5e477bd6bb4acab038793dd5727a357f70482a3092e161cecc76eff4b126d4cdd7841965747fc2429ee76b5121128c
data/.simplecov CHANGED
@@ -1 +1,3 @@
1
- SimpleCov.start
1
+ SimpleCov.start do
2
+ coverage_dir '{coverage}'
3
+ end
@@ -1,3 +1,9 @@
1
+ # v0.4.0
2
+
3
+ - a JSI::Base has multiple jsi_schemas https://github.com/notEthan/jsi/pull/88
4
+ - JSI.class_for_schemas replaces JSI.class_for_schema
5
+ - fix uri/fragment nomenclature https://github.com/notEthan/jsi/pull/89
6
+
1
7
  # v0.3.0
2
8
 
3
9
  - a schema is a JSI instance of a metaschema
@@ -5,6 +11,7 @@
5
11
  - module JSI::Metaschema
6
12
  - class JSI::MetaschemaNode
7
13
  - JSI::JSON::Node breaking changes
14
+ - module SimpleWrap https://github.com/notEthan/jsi/pull/87
8
15
 
9
16
  # v0.2.1
10
17
 
data/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- Copright © [Ethan](https://github.com/notEthan/)
1
+ Copright © [Ethan](https://github.com/notEthan/) <ethan.jsi@unth.net>
2
2
 
3
3
  [<img align="right" src="https://github.com/notEthan/jsi/raw/master/resources/icons/AGPL-3.0.png">](https://www.gnu.org/licenses/agpl-3.0.html)
4
4
 
data/README.md CHANGED
@@ -226,6 +226,6 @@ Issues and pull requests are welcome on GitHub at https://github.com/notEthan/js
226
226
 
227
227
  [<img align="right" src="https://github.com/notEthan/jsi/raw/master/resources/icons/AGPL-3.0.png">](https://www.gnu.org/licenses/agpl-3.0.html)
228
228
 
229
- JSI is Open Source Software licensed under the terms of the [GNU Affero General Public License version 3](https://www.gnu.org/licenses/agpl-3.0.html).
229
+ JSI is licensed under the terms of the [GNU Affero General Public License version 3](https://www.gnu.org/licenses/agpl-3.0.html).
230
230
 
231
231
  Unlike the MIT or BSD licenses more commonly used with Ruby gems, this license requires that if you modify JSI and propagate your changes, e.g. by including it in a web application, your modified version must be publicly available. The common path of forking on Github should satisfy this requirement.
data/lib/jsi.rb CHANGED
@@ -3,13 +3,21 @@
3
3
  require "jsi/version"
4
4
  require "pp"
5
5
  require "set"
6
+ require "json"
6
7
  require "pathname"
8
+ require "addressable/uri"
9
+
7
10
  require "jsi/json-schema-fragments"
11
+
8
12
  require "jsi/util"
13
+ require "jsi/typelike_modules"
9
14
 
10
15
  module JSI
11
16
  # generally put in code paths that are not expected to be valid control flow paths.
12
17
  # rather a NotImplementedCorrectlyError. but that's too long.
18
+ #
19
+ # if you've found this class because JSI has raised this error, please open an issue with the backtrace
20
+ # and any context you can provide at https://github.com/notEthan/jsi/issues
13
21
  class Bug < NotImplementedError
14
22
  end
15
23
 
@@ -23,8 +31,6 @@ module JSI
23
31
  autoload :Arraylike, 'jsi/typelike_modules'
24
32
  autoload :Schema, 'jsi/schema'
25
33
  autoload :Base, 'jsi/base'
26
- autoload :BaseArray, 'jsi/base'
27
- autoload :BaseHash, 'jsi/base'
28
34
  autoload :Metaschema, 'jsi/metaschema'
29
35
  autoload :MetaschemaNode, 'jsi/metaschema_node'
30
36
  autoload :SchemaClasses, 'jsi/schema_classes'
@@ -35,10 +41,10 @@ module JSI
35
41
 
36
42
  autoload :SimpleWrap, 'jsi/simple_wrap'
37
43
 
38
- # @return [Class subclassing JSI::Base] a JSI class which represents the
39
- # given schema. instances of the class represent JSON Schema instances
40
- # for the given schema.
41
- def self.class_for_schema(*a, &b)
42
- SchemaClasses.class_for_schema(*a, &b)
44
+ # @param schemas [Enumerable<JSI::Schema, #to_hash, Boolean>] schemas to represent with the class
45
+ # @return [Class subclassing JSI::Base] a JSI class which represents the given schemas.
46
+ # an instance of the class represents a JSON Schema instance described by all of the given schemas.
47
+ def self.class_for_schemas(*schemas)
48
+ SchemaClasses.class_for_schemas(*schemas)
43
49
  end
44
50
  end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'jsi/typelike_modules'
5
-
6
3
  module JSI
7
4
  # the base class for representing and instantiating a JSON Schema.
8
5
  #
@@ -13,9 +10,11 @@ module JSI
13
10
  # are dynamically created for schemas using {JSI.class_for_schema}, and these
14
11
  # are what are used to instantiate and represent JSON schema instances.
15
12
  class Base
16
- include Memoize
13
+ include Util::Memoize
17
14
  include Enumerable
18
15
  include PathedNode
16
+ class CannotSubscriptError < StandardError
17
+ end
19
18
 
20
19
  class << self
21
20
  # JSI::Base.new_jsi behaves the same as .new, and is defined for compatibility so you may call #new_jsi
@@ -33,43 +32,71 @@ module JSI
33
32
  @in_schema_classes
34
33
  end
35
34
 
36
- # @return [String] absolute schema_id of the schema this class represents.
37
- # see {Schema#schema_id}.
38
- def schema_id
39
- schema.schema_id
40
- end
41
-
42
- # @return [String] a string representing the class, with schema_id or schema ptr fragment
35
+ # @return [String] a string representing the class, indicating the schemas represented by their module
36
+ # name or a URI
43
37
  def inspect
44
- if !respond_to?(:schema)
38
+ if !respond_to?(:jsi_class_schemas)
45
39
  super
46
40
  else
47
- idfrag = schema_id || schema.node_ptr.fragment
41
+ schema_names = jsi_class_schemas.map do |schema|
42
+ mod = schema.jsi_schema_module
43
+ if mod.name && schema.schema_id
44
+ "#{mod.name} (#{schema.schema_id})"
45
+ elsif mod.name
46
+ mod.name
47
+ elsif schema.schema_id
48
+ schema.schema_id
49
+ else
50
+ schema.node_ptr.uri
51
+ end
52
+ end
53
+
48
54
  if name && !in_schema_classes
49
- "#{name} (#{idfrag})"
55
+ if jsi_class_schemas.empty?
56
+ "#{name} (0 schemas)"
57
+ else
58
+ "#{name} (#{schema_names.join(', ')})"
59
+ end
50
60
  else
51
- "(JSI Schema Class: #{idfrag})"
61
+ if schema_names.empty?
62
+ "(JSI Schema Class for 0 schemas)"
63
+ else
64
+ "(JSI Schema Class: #{schema_names.join(', ')})"
65
+ end
52
66
  end
53
67
  end
54
68
  end
55
69
 
56
70
  alias_method :to_s, :inspect
57
71
 
58
- # @return [String] a name for a constant for this class, generated from the
59
- # schema_id. only used if the class is not assigned to another constant.
72
+ # @return [String, nil] a name for a constant for this class, generated from the constant name
73
+ # or schema id of each schema this class represents. nil if any represented schema has no constant
74
+ # name or schema id.
60
75
  def schema_classes_const_name
61
- if schema_id
62
- 'X' + schema_id.gsub(/[^\w]/, '_')
76
+ if respond_to?(:jsi_class_schemas)
77
+ schema_names = jsi_class_schemas.map do |schema|
78
+ if schema.jsi_schema_module.name
79
+ schema.jsi_schema_module.name
80
+ elsif schema.schema_id
81
+ schema.schema_id
82
+ else
83
+ nil
84
+ end
85
+ end
86
+ if !schema_names.any?(&:nil?) && !schema_names.empty?
87
+ schema_names.sort.map { |n| 'X' + n.gsub(/[^\w]/, '_') }.join('')
88
+ end
63
89
  end
64
90
  end
65
91
 
66
92
  # @return [String] a constant name of this class
67
93
  def name
68
94
  unless instance_variable_defined?(:@in_schema_classes)
69
- if super || !schema_id || SchemaClasses.const_defined?(schema_classes_const_name)
95
+ const_name = schema_classes_const_name
96
+ if super || !const_name || SchemaClasses.const_defined?(const_name)
70
97
  @in_schema_classes = false
71
98
  else
72
- SchemaClasses.const_set(schema_classes_const_name, self)
99
+ SchemaClasses.const_set(const_name, self)
73
100
  @in_schema_classes = true
74
101
  end
75
102
  end
@@ -95,8 +122,8 @@ module JSI
95
122
  # iff `jsi_document` is passed, i.e. when `instance` is `NOINSTANCE`
96
123
  # @param jsi_root_node [JSI::Base] for internal use, specifies the JSI at the root of the document
97
124
  def initialize(instance, jsi_document: nil, jsi_ptr: nil, jsi_root_node: nil)
98
- unless respond_to?(:schema)
99
- raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #schema. please use JSI.class_for_schema")
125
+ unless respond_to?(:jsi_schemas)
126
+ raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #jsi_schemas. it is recommended to instantiate JSIs from a schema using JSI::Schema#new_jsi.")
100
127
  end
101
128
 
102
129
  if instance.is_a?(JSI::Schema)
@@ -126,17 +153,20 @@ module JSI
126
153
  else
127
154
  raise(Bug, 'incorrect usage') if jsi_document || jsi_ptr || jsi_root_node
128
155
  @jsi_document = instance
129
- @jsi_ptr = JSI::JSON::Pointer.new([])
156
+ @jsi_ptr = JSI::JSON::Pointer[]
130
157
  @jsi_root_node = self
131
158
  end
132
159
 
133
160
  if self.jsi_instance.respond_to?(:to_hash)
134
- extend BaseHash
161
+ extend PathedHashNode
135
162
  elsif self.jsi_instance.respond_to?(:to_ary)
136
- extend BaseArray
163
+ extend PathedArrayNode
137
164
  end
138
- if self.schema.describes_schema?
139
- extend JSI::Schema
165
+
166
+ jsi_schemas.each do |schema|
167
+ if schema.describes_schema?
168
+ extend JSI::Schema
169
+ end
140
170
  end
141
171
  end
142
172
 
@@ -153,11 +183,11 @@ module JSI
153
183
  alias_method :node_ptr, :jsi_ptr
154
184
  alias_method :document_root_node, :jsi_root_node
155
185
 
156
- # the instance of the json-schema
186
+ # the instance of the json-schema - the underlying JSON data used to instantiate this JSI
157
187
  alias_method :jsi_instance, :node_content
158
188
  alias_method :instance, :node_content
159
189
 
160
- # each is overridden by BaseHash or BaseArray when appropriate. the base
190
+ # each is overridden by PathedHashNode or PathedArrayNode when appropriate. the base
161
191
  # #each is not actually implemented, along with all the methods of Enumerable.
162
192
  def each
163
193
  raise NoMethodError, "Enumerable methods and #each not implemented for instance that is not like a hash or array: #{jsi_instance.pretty_inspect.chomp}"
@@ -192,8 +222,9 @@ module JSI
192
222
 
193
223
  # @param token [String, Integer, Object] the token to subscript
194
224
  # @return [JSI::Base, Object] the instance's subscript value at the given token.
195
- # if there is a subschema defined for that token on this JSI's schema,
196
- # returns that value as a JSI instantiation of that subschema.
225
+ # if this JSI's schemas define subschemas which apply for the given token, and the value is complex,
226
+ # returns the subscript value as a JSI instantiation of those subschemas. otherwise, the plain instance
227
+ # value is returned.
197
228
  def [](token)
198
229
  if respond_to?(:to_hash)
199
230
  token_in_range = node_content_hash_pubsend(:key?, token)
@@ -202,29 +233,29 @@ module JSI
202
233
  token_in_range = node_content_ary_pubsend(:each_index).include?(token)
203
234
  value = node_content_ary_pubsend(:[], token)
204
235
  else
205
- raise(NoMethodError, "cannot subcript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
236
+ raise(CannotSubscriptError, "cannot subcript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
206
237
  end
207
238
 
208
- jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range|
239
+ result = jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range|
209
240
  if respond_to?(:to_ary)
210
- token_schema = schema.subschema_for_index(token)
241
+ token_schemas = jsi_schemas.map { |schema| schema.subschemas_for_index(token) }.inject(Set.new, &:|)
211
242
  else
212
- token_schema = schema.subschema_for_property(token)
243
+ token_schemas = jsi_schemas.map { |schema| schema.subschemas_for_property_name(token) }.inject(Set.new, &:|)
213
244
  end
214
- token_schema = token_schema && token_schema.match_to_instance(value)
245
+ token_schemas = token_schemas.map { |schema| schema.match_to_instance(value) }.inject(Set.new, &:|)
215
246
 
216
247
  if token_in_range
217
- complex_value = token_schema && (value.respond_to?(:to_hash) || value.respond_to?(:to_ary))
218
- schema_value = token_schema && token_schema.describes_schema?
248
+ complex_value = token_schemas.any? && (value.respond_to?(:to_hash) || value.respond_to?(:to_ary))
249
+ schema_value = token_schemas.any? { |token_schema| token_schema.describes_schema? }
219
250
 
220
251
  if complex_value || schema_value
221
- class_for_schema(token_schema).new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: @jsi_ptr[token], jsi_root_node: @jsi_root_node)
252
+ JSI::SchemaClasses.class_for_schemas(token_schemas).new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: @jsi_ptr[token], jsi_root_node: @jsi_root_node)
222
253
  else
223
254
  value
224
255
  end
225
256
  else
226
257
  defaults = Set.new
227
- if token_schema
258
+ token_schemas.each do |token_schema|
228
259
  if token_schema.respond_to?(:to_hash) && token_schema.key?('default')
229
260
  defaults << token_schema['default']
230
261
  end
@@ -243,6 +274,7 @@ module JSI
243
274
  end
244
275
  end
245
276
  end
277
+ result
246
278
  end
247
279
 
248
280
  # assigns the subscript of the instance identified by the given token to the given value.
@@ -297,12 +329,12 @@ module JSI
297
329
 
298
330
  # @return [Array] array of schema validation errors for this instance
299
331
  def fully_validate(errors_as_objects: false)
300
- schema.fully_validate_instance(jsi_instance, errors_as_objects: errors_as_objects)
332
+ jsi_schemas.map { |schema| schema.fully_validate_instance(jsi_instance, errors_as_objects: errors_as_objects) }.inject([], &:+)
301
333
  end
302
334
 
303
335
  # @return [true, false] whether the instance validates against its schema
304
336
  def validate
305
- schema.validate_instance(jsi_instance)
337
+ jsi_schemas.all? { |schema| schema.validate_instance(jsi_instance) }
306
338
  end
307
339
 
308
340
  # @return [true] if this method does not raise, it returns true to
@@ -310,7 +342,8 @@ module JSI
310
342
  # @raise [::JSON::Schema::ValidationError] raises if the instance has
311
343
  # validation errors
312
344
  def validate!
313
- schema.validate_instance!(jsi_instance)
345
+ jsi_schemas.each { |schema| schema.validate_instance!(jsi_instance) }
346
+ true
314
347
  end
315
348
 
316
349
  def dup
@@ -344,18 +377,18 @@ module JSI
344
377
  class_txt = begin
345
378
  if class_name
346
379
  # ignore ID
347
- schema_name = schema.jsi_schema_module.name
348
- if !schema_name
380
+ schema_module_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name }.compact
381
+ if schema_module_names.empty?
349
382
  class_name
350
383
  else
351
- "#{class_name} (#{schema_name})"
384
+ "#{class_name} (#{schema_module_names.join(', ')})"
352
385
  end
353
386
  else
354
- schema_name = schema.jsi_schema_module.name || schema.schema_id
355
- if !schema_name
387
+ schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name || schema.schema_id }.compact
388
+ if schema_names.empty?
356
389
  "JSI"
357
390
  else
358
- "JSI (#{schema_name})"
391
+ "JSI (#{schema_names.join(', ')})"
359
392
  end
360
393
  end
361
394
  end
@@ -387,24 +420,6 @@ module JSI
387
420
  def jsi_fingerprint
388
421
  {class: jsi_class, jsi_document: jsi_document, jsi_ptr: jsi_ptr}
389
422
  end
390
- include FingerprintHash
391
-
392
- private
393
-
394
- # this is an instance method in order to allow subclasses of JSI classes to
395
- # override it to point to other subclasses corresponding to other schemas.
396
- def class_for_schema(schema)
397
- JSI.class_for_schema(schema)
398
- end
399
- end
400
-
401
- # module extending a {JSI::Base} object when its instance is Hash-like (responds to #to_hash)
402
- module BaseHash
403
- include PathedHashNode
404
- end
405
-
406
- # module extending a {JSI::Base} object when its instance is Array-like (responds to #to_ary)
407
- module BaseArray
408
- include PathedArrayNode
423
+ include Util::FingerprintHash
409
424
  end
410
425
  end
@@ -14,11 +14,11 @@ module JSI
14
14
  # Preferences = JSI.class_for_schema(preferences_json_schema)
15
15
  # class Foo < ActiveRecord::Base
16
16
  # # as a single serializer, loads a Preferences instance from a json column
17
- # serialize 'preferences', JSI::JSICoder.new(Preferences)
17
+ # serialize 'preferences_json', JSI::JSICoder.new(Preferences)
18
18
  #
19
19
  # # for a text column, arms_serialize will go from JSI to JSON-compatible
20
20
  # # objects to a string. the symbol `:jsi` is a shortcut for JSI::JSICoder.
21
- # arms_serialize 'preferences', [:jsi, Preferences], :json
21
+ # arms_serialize 'preferences_txt', [:jsi, Preferences], :json
22
22
  # end
23
23
  #
24
24
  # the column data may be either a single instance of the schema class
@@ -155,7 +155,7 @@ module JSI
155
155
  def object_group_text
156
156
  [
157
157
  self.class.inspect,
158
- "fragment=#{node_ptr.fragment.inspect}",
158
+ node_ptr.uri.to_s,
159
159
  ] + (node_content.respond_to?(:object_group_text) ? node_content.object_group_text : [])
160
160
  end
161
161
 
@@ -185,7 +185,7 @@ module JSI
185
185
  def jsi_fingerprint
186
186
  {class: JSI::JSON::Node, node_document: node_document, node_ptr: node_ptr}
187
187
  end
188
- include FingerprintHash
188
+ include Util::FingerprintHash
189
189
  end
190
190
 
191
191
  # a JSI::JSON::Node whose content is Array-like (responds to #to_ary)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'addressable/uri'
4
-
5
3
  module JSI
6
4
  module JSON
7
5
  # a JSON Pointer, as described by RFC 6901 https://tools.ietf.org/html/rfc6901
@@ -33,28 +31,23 @@ module JSI
33
31
 
34
32
  # parse a URI-escaped fragment and instantiate as a JSI::JSON::Pointer
35
33
  #
36
- # ptr = JSI::JSON::Pointer.from_fragment('#/foo/bar')
37
- # => #<JSI::JSON::Pointer fragment: #/foo/bar>
34
+ # ptr = JSI::JSON::Pointer.from_fragment('/foo/bar')
35
+ # => #<JSI::JSON::Pointer fragment: /foo/bar>
38
36
  # ptr.reference_tokens
39
37
  # => ["foo", "bar"]
40
38
  #
41
39
  # with URI escaping:
42
40
  #
43
- # ptr = JSI::JSON::Pointer.from_fragment('#/foo%20bar')
44
- # => #<JSI::JSON::Pointer fragment: #/foo%20bar>
41
+ # ptr = JSI::JSON::Pointer.from_fragment('/foo%20bar')
42
+ # => #<JSI::JSON::Pointer fragment: /foo%20bar>
45
43
  # ptr.reference_tokens
46
44
  # => ["foo bar"]
47
45
  #
48
46
  # @param fragment [String] a fragment containing a pointer (starting with #)
49
47
  # @return [JSI::JSON::Pointer]
48
+ # @raise [JSI::JSON::Pointer::PointerSyntaxError] when the fragment does not contain a pointer with valid pointer syntax
50
49
  def self.from_fragment(fragment)
51
- fragment = Addressable::URI.unescape(fragment)
52
- match = fragment.match(/\A#/)
53
- if match
54
- from_pointer(match.post_match, type: 'fragment')
55
- else
56
- raise(PointerSyntaxError, "Invalid fragment syntax in #{fragment.inspect}: fragment must begin with #")
57
- end
50
+ from_pointer(Addressable::URI.unescape(fragment), type: 'fragment')
58
51
  end
59
52
 
60
53
  # parse a pointer string and instantiate as a JSI::JSON::Pointer
@@ -72,6 +65,7 @@ module JSI
72
65
  # @param pointer_string [String] a pointer string
73
66
  # @param type (for internal use) indicates the original representation of the pointer
74
67
  # @return [JSI::JSON::Pointer]
68
+ # @raise [JSI::JSON::Pointer::PointerSyntaxError] when the pointer_string does not have valid pointer syntax
75
69
  def self.from_pointer(pointer_string, type: 'pointer')
76
70
  tokens = pointer_string.split('/', -1).map! do |piece|
77
71
  piece.gsub('~1', '/').gsub('~0', '~')
@@ -137,7 +131,12 @@ module JSI
137
131
 
138
132
  # @return [String] the fragment string representation of this Pointer
139
133
  def fragment
140
- '#' + Addressable::URI.escape(pointer)
134
+ Addressable::URI.escape(pointer)
135
+ end
136
+
137
+ # @return [Addressable::URI] a URI consisting only of a pointer fragment
138
+ def uri
139
+ Addressable::URI.new(fragment: fragment)
141
140
  end
142
141
 
143
142
  # @return [Boolean] whether this pointer points to the root (has an empty array of reference_tokens)
@@ -197,102 +196,108 @@ module JSI
197
196
  Pointer.new(reference_tokens + [token], type: @type)
198
197
  end
199
198
 
200
- # given this Pointer points to a schema in the given document, returns a pointer
201
- # to a subschema of that schema for the given property name.
199
+ # given this Pointer points to a schema in the given document, returns a set of pointers
200
+ # to subschemas of that schema for the given property name.
202
201
  #
203
202
  # @param document [#to_hash, #to_ary, Object] document containing the schema this pointer points to
204
203
  # @param property_name [Object] the property name for which to find a subschema
205
- # @return [JSI::JSON::Pointer, nil] a pointer to a subschema in the document for the property_name, or nil
206
- def schema_subschema_ptr_for_property_name(document, property_name)
204
+ # @return [Set<JSI::JSON::Pointer>] pointers to subschemas
205
+ def schema_subschema_ptrs_for_property_name(document, property_name)
207
206
  ptr = self
208
207
  schema = ptr.evaluate(document)
209
- if !schema.respond_to?(:to_hash)
210
- nil
211
- else
212
- if schema.key?('properties') && schema['properties'].respond_to?(:to_hash) && schema['properties'].key?(property_name)
213
- ptr['properties'][property_name]
214
- else
215
- # TODO this is rather incorrect handling of patternProperties and additionalProperties
208
+ Set.new.tap do |ptrs|
209
+ if schema.respond_to?(:to_hash)
210
+ apply_additional = true
211
+ if schema.key?('properties') && schema['properties'].respond_to?(:to_hash) && schema['properties'].key?(property_name)
212
+ apply_additional = false
213
+ ptrs << ptr['properties'][property_name]
214
+ end
216
215
  if schema['patternProperties'].respond_to?(:to_hash)
217
- pattern_schema_name = schema['patternProperties'].keys.detect do |pattern|
218
- property_name.to_s =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
216
+ schema['patternProperties'].each_key do |pattern|
217
+ if property_name.to_s =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
218
+ apply_additional = false
219
+ ptrs << ptr['patternProperties'][pattern]
220
+ end
219
221
  end
220
222
  end
221
- if pattern_schema_name
222
- ptr['patternProperties'][pattern_schema_name]
223
- else
224
- if schema.key?('additionalProperties')
225
- ptr['additionalProperties']
226
- else
227
- nil
228
- end
223
+ if apply_additional && schema.key?('additionalProperties')
224
+ ptrs << ptr['additionalProperties']
229
225
  end
230
226
  end
231
227
  end
232
228
  end
233
229
 
234
- # given this Pointer points to a schema in the given document, returns a pointer
235
- # to a subschema of that schema for the given array index.
230
+ # given this Pointer points to a schema in the given document, returns a set of pointers
231
+ # to subschemas of that schema for the given array index.
236
232
  #
237
233
  # @param document [#to_hash, #to_ary, Object] document containing the schema this pointer points to
238
- # @param idx [Object] the array index for which to find a subschema
239
- # @return [JSI::JSON::Pointer, nil] a pointer to a subschema in the document for array index idx, or nil
240
- def schema_subschema_ptr_for_index(document, idx)
234
+ # @param idx [Object] the array index for which to find subschemas
235
+ # @return [Set<JSI::JSON::Pointer>] pointers to subschemas
236
+ def schema_subschema_ptrs_for_index(document, idx)
241
237
  ptr = self
242
238
  schema = ptr.evaluate(document)
243
- if !schema.respond_to?(:to_hash)
244
- nil
245
- else
246
- if schema.key?('items') || schema.key?('additionalItems')
239
+ Set.new.tap do |ptrs|
240
+ if schema.respond_to?(:to_hash)
247
241
  if schema['items'].respond_to?(:to_ary)
248
242
  if schema['items'].each_index.to_a.include?(idx)
249
- ptr['items'][idx]
243
+ ptrs << ptr['items'][idx]
250
244
  elsif schema.key?('additionalItems')
251
- ptr['additionalItems']
252
- else
253
- nil
245
+ ptrs << ptr['additionalItems']
254
246
  end
255
247
  elsif schema.key?('items')
256
- ptr['items']
257
- else
258
- nil
248
+ ptrs << ptr['items']
259
249
  end
260
- else
261
- nil
262
250
  end
263
251
  end
264
252
  end
265
253
 
266
- # given this Pointer points to a schema in the given document, this matches
267
- # any oneOf or anyOf subschema of the schema which the given instance validates
268
- # against. if a subschema is matched, a pointer to that schema is returned; if not,
269
- # self is returned.
254
+ # given this Pointer points to a schema in the given document, this matches any
255
+ # applicators of the schema (oneOf, anyOf, allOf, $ref) which should be applied
256
+ # and returns them as a set of pointers.
270
257
  #
271
- # @param document [#to_hash, #to_ary, Object] document containing the schema
272
- # this pointer points to
273
- # @param instance [Object] the instance to which to attempt to match *Of subschemas
258
+ # @param document [#to_hash, #to_ary, Object] document containing the schema this pointer points to
259
+ # @param instance [Object] the instance to check any applicators against
274
260
  # @return [JSI::JSON::Pointer] either a pointer to a *Of subschema in the document,
275
261
  # or self if no other subschema was matched
276
- def schema_match_ptr_to_instance(document, instance)
262
+ def schema_match_ptrs_to_instance(document, instance)
277
263
  ptr = self
278
264
  schema = ptr.evaluate(document)
279
- if schema.respond_to?(:to_hash)
280
- # matching oneOf is good here. one schema for one instance.
281
- # matching anyOf is fine. there could be more than one schema matched but it's usually just
282
- # one. if more than one is a match, you just get the first one.
283
- someof_token = %w(oneOf anyOf).detect { |k| schema[k].respond_to?(:to_ary) }
284
- if someof_token
285
- someof_ptr = ptr[someof_token].deref(document)
286
- someof_ptr.evaluate(document).each_index do |i|
287
- someof_schema_ptr = someof_ptr[i].deref(document)
288
- valid = ::JSON::Validator.validate(JSI::Typelike.as_json(document), JSI::Typelike.as_json(instance), fragment: someof_schema_ptr.fragment)
289
- if valid
290
- return someof_schema_ptr.schema_match_ptr_to_instance(document, instance)
265
+
266
+ Set.new.tap do |ptrs|
267
+ if schema.respond_to?(:to_hash)
268
+ if schema['$ref'].respond_to?(:to_str)
269
+ ptr.deref(document) do |deref_ptr|
270
+ ptrs.merge(deref_ptr.schema_match_ptrs_to_instance(document, instance))
271
+ end
272
+ else
273
+ ptrs << ptr
274
+ end
275
+ if schema['allOf'].respond_to?(:to_ary)
276
+ schema['allOf'].each_index do |i|
277
+ ptrs.merge(ptr['allOf'][i].schema_match_ptrs_to_instance(document, instance))
278
+ end
279
+ end
280
+ if schema['anyOf'].respond_to?(:to_ary)
281
+ schema['anyOf'].each_index do |i|
282
+ valid = ::JSON::Validator.validate(JSI::Typelike.as_json(document), JSI::Typelike.as_json(instance), fragment: ptr['anyOf'][i].fragment)
283
+ if valid
284
+ ptrs.merge(ptr['anyOf'][i].schema_match_ptrs_to_instance(document, instance))
285
+ end
286
+ end
287
+ end
288
+ if schema['oneOf'].respond_to?(:to_ary)
289
+ one_i = schema['oneOf'].each_index.detect do |i|
290
+ ::JSON::Validator.validate(JSI::Typelike.as_json(document), JSI::Typelike.as_json(instance), fragment: ptr['oneOf'][i].fragment)
291
+ end
292
+ if one_i
293
+ ptrs.merge(ptr['oneOf'][one_i].schema_match_ptrs_to_instance(document, instance))
291
294
  end
292
295
  end
296
+ # TODO dependencies
297
+ else
298
+ ptrs << ptr
293
299
  end
294
300
  end
295
- return ptr
296
301
  end
297
302
 
298
303
  # takes a document and a block. the block is yielded the content of the given document at this
@@ -378,7 +383,7 @@ module JSI
378
383
  return self unless ref.is_a?(String)
379
384
 
380
385
  if ref[/\A#/]
381
- return Pointer.from_fragment(ref).tap(&block)
386
+ return Pointer.from_fragment(Addressable::URI.parse(ref).fragment).tap(&block)
382
387
  end
383
388
 
384
389
  # HAX for how google does refs and ids
@@ -399,7 +404,7 @@ module JSI
399
404
 
400
405
  # @return [String] string representation of this Pointer
401
406
  def inspect
402
- "#<#{self.class.inspect} #{representation_s}>"
407
+ "#{self.class.name}[#{reference_tokens.map(&:inspect).join(", ")}]"
403
408
  end
404
409
 
405
410
  alias_method :to_s, :inspect
@@ -408,20 +413,7 @@ module JSI
408
413
  def jsi_fingerprint
409
414
  {class: JSI::JSON::Pointer, reference_tokens: reference_tokens}
410
415
  end
411
- include FingerprintHash
412
-
413
- private
414
-
415
- # @return [String] a representation of this pointer based on @type
416
- def representation_s
417
- if @type == 'fragment'
418
- "fragment: #{fragment}"
419
- elsif @type == 'pointer'
420
- "pointer: #{pointer}"
421
- else
422
- "reference_tokens: #{reference_tokens.inspect}"
423
- end
424
- end
416
+ include Util::FingerprintHash
425
417
  end
426
418
  end
427
419
  end