jsi 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,4 @@
1
1
  module JSI
2
- # base class for representing an instance of an instance described by a schema
3
2
  class Base
4
3
  class << self
5
4
  def class_comment
@@ -15,7 +14,7 @@ module JSI
15
14
  lines << "#"
16
15
  end
17
16
 
18
- schema.described_hash_property_names.each_with_index do |propname, i|
17
+ schema.described_object_property_names.each_with_index do |propname, i|
19
18
  lines << "#" unless i == 0
20
19
  lines << "# @!attribute [rw] #{propname}"
21
20
 
@@ -73,7 +72,7 @@ module JSI
73
72
  end
74
73
  end
75
74
  lines << "class #{name}"
76
- schema.described_hash_property_names.each_with_index do |propname, i|
75
+ schema.described_object_property_names.each_with_index do |propname, i|
77
76
  lines << "" unless i == 0
78
77
  property_schema = schema['properties'].respond_to?(:to_hash) &&
79
78
  schema['properties'][propname].respond_to?(:to_hash) &&
@@ -84,24 +84,25 @@ module JSON
84
84
  # pointed to by this pointer.
85
85
  def evaluate(document)
86
86
  reference_tokens.inject(document) do |value, token|
87
- if value.is_a?(Array)
87
+ if value.respond_to?(:to_ary)
88
88
  if token.is_a?(String) && token =~ /\A\d|[1-9]\d+\z/
89
89
  token = token.to_i
90
90
  end
91
91
  unless token.is_a?(Integer)
92
92
  raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not an integer and cannot be resolved in array #{value.inspect}")
93
93
  end
94
- unless (0...value.size).include?(token)
94
+ unless (0...(value.respond_to?(:size) ? value : value.to_ary).size).include?(token)
95
95
  raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid index of #{value.inspect}")
96
96
  end
97
- elsif value.is_a?(Hash)
98
- unless value.key?(token)
97
+ (value.respond_to?(:[]) ? value : value.to_ary)[token]
98
+ elsif value.respond_to?(:to_hash)
99
+ unless (value.respond_to?(:key?) ? value : value.to_hash).key?(token)
99
100
  raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid key of #{value.inspect}")
100
101
  end
102
+ (value.respond_to?(:[]) ? value : value.to_hash)[token]
101
103
  else
102
104
  raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} cannot be resolved in #{value.inspect}")
103
105
  end
104
- value[token]
105
106
  end
106
107
  end
107
108
 
@@ -2,25 +2,40 @@ module JSI
2
2
  module JSON
3
3
  # JSI::JSON::Node is an abstraction of a node within a JSON document.
4
4
  # it aims to act like the underlying data type of the node's content
5
- # (Hash or Array, generally) in most cases, defining methods of Hash
6
- # and Array which delegate to the content. However, destructive methods
7
- # are not defined, as modifying the content of a node would change it
8
- # for any other nodes in the document that contain or refer to it.
5
+ # (generally Hash or Array-like) in most cases.
6
+ #
7
+ # the main advantage offered by using a Node over the underlying data
8
+ # is in dereferencing. if a Node consists of a hash with a $ref property
9
+ # pointing within the same document, then the Node will transparently
10
+ # follow the ref and return the referenced data.
11
+ #
12
+ # in most other respects, a Node aims to act like a Hash when the content
13
+ # is Hash-like, an Array when the content is Array-like. methods of Hash
14
+ # and Array are defined and delegated to the node's content.
15
+ #
16
+ # however, destructive methods are for the most part not implemented.
17
+ # at the moment only #[]= is implemented. since Node thinly wraps the
18
+ # underlying data, you can change the data and it will be reflected in
19
+ # the node. implementations of destructive methods are planned.
9
20
  #
10
21
  # methods that return a modified copy such as #merge are defined, and
11
22
  # return a copy of the document with the content of the node modified.
12
23
  # the original node's document and content are untouched.
13
24
  class Node
14
- # if the content of the document at the given path is a Hash, returns
15
- # a HashNode; if an Array, returns ArrayNode. otherwise returns a
16
- # regular Node, though, for the most part this will be called with Hash
17
- # or Array content.
25
+ def self.new_doc(document)
26
+ new_by_type(document, [])
27
+ end
28
+
29
+ # if the content of the document at the given path is Hash-like, returns
30
+ # a HashNode; if Array-like, returns ArrayNode. otherwise returns a
31
+ # regular Node, although Nodes are for the most part instantiated from
32
+ # Hash or Array-like content.
18
33
  def self.new_by_type(document, path)
19
34
  node = Node.new(document, path)
20
35
  content = node.content
21
- if content.is_a?(Hash)
36
+ if content.respond_to?(:to_hash)
22
37
  HashNode.new(document, path)
23
- elsif content.is_a?(Array)
38
+ elsif content.respond_to?(:to_ary)
24
39
  ArrayNode.new(document, path)
25
40
  else
26
41
  node
@@ -29,9 +44,14 @@ module JSI
29
44
 
30
45
  # a Node represents the content of a document at a given path.
31
46
  def initialize(document, path)
32
- raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect.chomp} (#{path.class})") unless path.is_a?(Array)
47
+ unless path.respond_to?(:to_ary)
48
+ raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect.chomp} (#{path.class})")
49
+ end
50
+ if document.is_a?(JSI::JSON::Node)
51
+ raise(TypeError, "document of a Node should not be another JSI::JSON::Node: #{document.inspect}")
52
+ end
33
53
  @document = document
34
- @path = path.dup.freeze
54
+ @path = path.to_ary.dup.freeze
35
55
  @pointer = ::JSON::Schema::Pointer.new(:reference_tokens, path)
36
56
  end
37
57
 
@@ -47,60 +67,88 @@ module JSI
47
67
  pointer.evaluate(document)
48
68
  end
49
69
 
50
- def [](k)
70
+ # returns content at the given subscript - call this the subcontent.
71
+ #
72
+ # if the content cannot be subscripted, raises TypeError.
73
+ #
74
+ # if the subcontent is Hash-like, it is wrapped as a JSI::JSON::HashNode before being returned.
75
+ # if the subcontent is Array-like, it is wrapped as a JSI::JSON::ArrayNode before being returned.
76
+ #
77
+ # if this node's content is a $ref - that is, a hash with a $ref attribute - and the subscript is
78
+ # not a key of the hash, then the $ref is followed before returning the subcontent.
79
+ def [](subscript)
51
80
  node = self
52
81
  content = node.content
53
- if content.is_a?(Hash) && !content.key?(k)
82
+ if content.respond_to?(:to_hash) && !(content.respond_to?(:key?) ? content : content.to_hash).key?(subscript)
54
83
  node = node.deref
55
84
  content = node.content
56
85
  end
86
+ unless content.respond_to?(:[])
87
+ if content.respond_to?(:to_hash)
88
+ content = content.to_hash
89
+ elsif content.respond_to?(:to_ary)
90
+ content = content.to_ary
91
+ else
92
+ raise(NoMethodError, "undefined method `[]`\nsubscripting with #{subscript.pretty_inspect.chomp} (#{subscript.class}) from #{content.class.inspect}. content is: #{content.pretty_inspect.chomp}")
93
+ end
94
+ end
57
95
  begin
58
- el = content[k]
96
+ subcontent = content[subscript]
59
97
  rescue TypeError => e
60
- raise(e.class, e.message + "\nsubscripting with #{k.pretty_inspect.chomp} (#{k.class}) from #{content.class.inspect}. self is: #{pretty_inspect.chomp}", e.backtrace)
98
+ raise(e.class, e.message + "\nsubscripting with #{subscript.pretty_inspect.chomp} (#{subscript.class}) from #{content.class.inspect}. content is: #{content.pretty_inspect.chomp}", e.backtrace)
61
99
  end
62
- if el.is_a?(Hash) || el.is_a?(Array)
63
- self.class.new_by_type(node.document, node.path + [k])
100
+ if subcontent.respond_to?(:to_hash)
101
+ HashNode.new(node.document, node.path + [subscript])
102
+ elsif subcontent.respond_to?(:to_ary)
103
+ ArrayNode.new(node.document, node.path + [subscript])
64
104
  else
65
- el
105
+ subcontent
66
106
  end
67
107
  end
68
108
 
69
- def []=(k, v)
70
- if v.is_a?(Node)
71
- content[k] = v.content
109
+ # assigns the given subscript of the content to the given value. the document is modified in place.
110
+ def []=(subscript, value)
111
+ if value.is_a?(Node)
112
+ content[subscript] = value.content
72
113
  else
73
- content[k] = v
114
+ content[subscript] = value
74
115
  end
75
116
  end
76
117
 
118
+ # returns a Node, dereferencing a $ref attribute if possible. if this node is not hash-like,
119
+ # does not have a $ref, or if what its $ref cannot be found, this node is returned.
120
+ #
121
+ # currently only $refs pointing within the same document are followed.
77
122
  def deref
78
123
  content = self.content
79
124
 
80
- return self unless content.is_a?(Hash) && content['$ref'].is_a?(String)
125
+ if content.respond_to?(:to_hash)
126
+ ref = (content.respond_to?(:[]) ? content : content.to_hash)['$ref']
127
+ end
128
+ return self unless ref.is_a?(String)
81
129
 
82
- if content['$ref'][/\A#/]
83
- return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(content['$ref'])).deref
130
+ if ref[/\A#/]
131
+ return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(ref)).deref
84
132
  end
85
133
 
86
134
  # HAX for how google does refs and ids
87
135
  if document_node['schemas'].respond_to?(:to_hash)
88
- if document_node['schemas'][content['$ref']]
89
- return document_node['schemas'][content['$ref']]
136
+ if document_node['schemas'][ref]
137
+ return document_node['schemas'][ref]
90
138
  end
91
- _, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == content['$ref'] }
139
+ _, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == ref }
92
140
  if deref_by_id
93
141
  return deref_by_id
94
142
  end
95
143
  end
96
144
 
97
- #raise(NotImplementedError, "cannot dereference #{content['$ref']}") # TODO
145
+ #raise(NotImplementedError, "cannot dereference #{ref}") # TODO
98
146
  return self
99
147
  end
100
148
 
101
149
  # a Node at the root of the document
102
150
  def document_node
103
- Node.new_by_type(document, [])
151
+ Node.new_doc(document)
104
152
  end
105
153
 
106
154
  # the parent of this node. if this node is the document root (its path is empty), raises
@@ -117,11 +165,13 @@ module JSI
117
165
  def pointer_path
118
166
  pointer.pointer
119
167
  end
168
+
120
169
  # the pointer fragment to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
121
170
  def fragment
122
171
  pointer.fragment
123
172
  end
124
173
 
174
+ # returns a jsonifiable representation of this node's content
125
175
  def as_json(*opt)
126
176
  Typelike.as_json(content, *opt)
127
177
  end
@@ -136,7 +186,7 @@ module JSI
136
186
  # or hash in the path above this node. this node's content is modified by the caller, and
137
187
  # that is recursively merged up to the document root. the recursion is done with a
138
188
  # y combinator, for no other reason than that was a fun way to implement it.
139
- modified_document = JSI.ycomb do |rec|
189
+ modified_document = JSI::Util.ycomb do |rec|
140
190
  proc do |subdocument, subpath|
141
191
  if subpath == []
142
192
  yield(subdocument)
@@ -144,11 +194,12 @@ module JSI
144
194
  car = subpath[0]
145
195
  cdr = subpath[1..-1]
146
196
  if subdocument.respond_to?(:to_hash)
147
- car_object = rec.call(subdocument[car], cdr)
148
- if car_object.object_id == subdocument[car].object_id
197
+ subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_hash)[car]
198
+ car_object = rec.call(subdocument_car, cdr)
199
+ if car_object.object_id == subdocument_car.object_id
149
200
  subdocument
150
201
  else
151
- subdocument.merge({car => car_object})
202
+ (subdocument.respond_to?(:merge) ? subdocument : subdocument.to_hash).merge({car => car_object})
152
203
  end
153
204
  elsif subdocument.respond_to?(:to_ary)
154
205
  if car.is_a?(String) && car =~ /\A\d+\z/
@@ -157,11 +208,12 @@ module JSI
157
208
  unless car.is_a?(Integer)
158
209
  raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
159
210
  end
160
- car_object = rec.call(subdocument[car], cdr)
161
- if car_object.object_id == subdocument[car].object_id
211
+ subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_ary)[car]
212
+ car_object = rec.call(subdocument_car, cdr)
213
+ if car_object.object_id == subdocument_car.object_id
162
214
  subdocument
163
215
  else
164
- subdocument.dup.tap do |arr|
216
+ (subdocument.respond_to?(:[]=) ? subdocument : subdocument.to_ary).dup.tap do |arr|
165
217
  arr[car] = car_object
166
218
  end
167
219
  end
@@ -174,12 +226,17 @@ module JSI
174
226
  Node.new_by_type(modified_document, path)
175
227
  end
176
228
 
229
+ # meta-information about the object, outside the content. used by #inspect / #pretty_print
177
230
  def object_group_text
178
- "fragment=#{fragment.inspect}"
231
+ "fragment=#{fragment.inspect}" + (content.respond_to?(:object_group_text) ? ' ' + content.object_group_text : '')
179
232
  end
233
+
234
+ # a string representing this node
180
235
  def inspect
181
236
  "\#<#{self.class.inspect} #{object_group_text} #{content.inspect}>"
182
237
  end
238
+
239
+ # pretty-prints a representation this node to the given printer
183
240
  def pretty_print(q)
184
241
  q.instance_exec(self) do |obj|
185
242
  text "\#<#{obj.class.inspect} #{obj.object_group_text}"
@@ -194,19 +251,27 @@ module JSI
194
251
  end
195
252
  end
196
253
 
254
+ # fingerprint for equality (see FingerprintHash). two nodes are equal if they are both nodes
255
+ # (regardless of type, e.g. one may be a Node and the other may be a HashNode) within equal
256
+ # documents at equal paths. note that this means two nodes with the same content may not be
257
+ # considered equal.
197
258
  def fingerprint
198
259
  {is_node: self.is_a?(JSI::JSON::Node), document: document, path: path}
199
260
  end
200
261
  include FingerprintHash
201
262
  end
202
263
 
264
+ # a JSI::JSON::Node whose content is Array-like (responds to #to_ary)
265
+ # and includes Array methods from Arraylike
203
266
  class ArrayNode < Node
267
+ # iterates over each element in the same manner as Array#each
204
268
  def each
205
- return to_enum(__method__) { content.size } unless block_given?
206
- content.each_index { |i| yield self[i] }
269
+ return to_enum(__method__) { (content.respond_to?(:size) ? content : content.to_ary).size } unless block_given?
270
+ (content.respond_to?(:each_index) ? content : content.to_ary).each_index { |i| yield self[i] }
207
271
  self
208
272
  end
209
273
 
274
+ # the content of this ArrayNode, as an Array
210
275
  def to_ary
211
276
  to_a
212
277
  end
@@ -214,6 +279,7 @@ module JSI
214
279
  include Enumerable
215
280
  include Arraylike
216
281
 
282
+ # returns a jsonifiable representation of this node's content
217
283
  def as_json(*opt) # needs redefined after including Enumerable
218
284
  Typelike.as_json(content, *opt)
219
285
  end
@@ -221,21 +287,25 @@ module JSI
221
287
  # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
222
288
  # we override these methods from Arraylike
223
289
  SAFE_INDEX_ONLY_METHODS.each do |method_name|
224
- define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
290
+ define_method(method_name) { |*a, &b| (content.respond_to?(method_name) ? content : content.to_ary).public_send(method_name, *a, &b) }
225
291
  end
226
292
  end
227
293
 
294
+ # a JSI::JSON::Node whose content is Hash-like (responds to #to_hash)
295
+ # and includes Hash methods from Hashlike
228
296
  class HashNode < Node
297
+ # iterates over each element in the same manner as Array#each
229
298
  def each(&block)
230
- return to_enum(__method__) { content.size } unless block_given?
299
+ return to_enum(__method__) { content.respond_to?(:size) ? content.size : content.to_ary.size } unless block_given?
231
300
  if block.arity > 1
232
- content.each_key { |k| yield k, self[k] }
301
+ (content.respond_to?(:each_key) ? content : content.to_hash).each_key { |k| yield k, self[k] }
233
302
  else
234
- content.each_key { |k| yield [k, self[k]] }
303
+ (content.respond_to?(:each_key) ? content : content.to_hash).each_key { |k| yield [k, self[k]] }
235
304
  end
236
305
  self
237
306
  end
238
307
 
308
+ # the content of this HashNode, as a Hash
239
309
  def to_hash
240
310
  inject({}) { |h, (k, v)| h[k] = v; h }
241
311
  end
@@ -243,13 +313,14 @@ module JSI
243
313
  include Enumerable
244
314
  include Hashlike
245
315
 
316
+ # returns a jsonifiable representation of this node's content
246
317
  def as_json(*opt) # needs redefined after including Enumerable
247
318
  Typelike.as_json(content, *opt)
248
319
  end
249
320
 
250
321
  # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
251
322
  SAFE_KEY_ONLY_METHODS.each do |method_name|
252
- define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
323
+ define_method(method_name) { |*a, &b| (content.respond_to?(method_name) ? content : content.to_hash).public_send(method_name, *a, &b) }
253
324
  end
254
325
  end
255
326
  end
@@ -1,37 +1,50 @@
1
1
  require 'jsi/json/node'
2
2
 
3
3
  module JSI
4
+ # JSI::Schema represents a JSON Schema. initialized from a Hash-like schema
5
+ # object, JSI::Schema is a relatively simple class to abstract useful methods
6
+ # applied to a JSON Schema.
4
7
  class Schema
5
8
  include Memoize
6
9
 
10
+ # initializes a schema from a given JSI::Base, JSI::JSON::Node, or hash.
11
+ # @param schema_object [JSI::Base, #to_hash] the schema
7
12
  def initialize(schema_object)
8
13
  if schema_object.is_a?(JSI::Schema)
9
14
  raise(TypeError, "will not instantiate Schema from another Schema: #{schema_object.pretty_inspect.chomp}")
10
15
  elsif schema_object.is_a?(JSI::Base)
11
- @schema_object = JSI.deep_stringify_symbol_keys(schema_object.deref)
12
- @schema_node = @schema_object.instance
16
+ @schema_jsi = JSI.deep_stringify_symbol_keys(schema_object.deref)
17
+ @schema_node = @schema_jsi.instance
13
18
  elsif schema_object.is_a?(JSI::JSON::HashNode)
14
- @schema_object = nil
19
+ @schema_jsi = nil
15
20
  @schema_node = JSI.deep_stringify_symbol_keys(schema_object.deref)
16
21
  elsif schema_object.respond_to?(:to_hash)
17
- @schema_object = nil
18
- @schema_node = JSI::JSON::Node.new_by_type(JSI.deep_stringify_symbol_keys(schema_object), [])
22
+ @schema_jsi = nil
23
+ @schema_node = JSI::JSON::Node.new_doc(JSI.deep_stringify_symbol_keys(schema_object))
19
24
  else
20
25
  raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
21
26
  end
22
- if @schema_object
23
- define_singleton_method(:instance) { schema_node } # aka schema_object.instance
24
- define_singleton_method(:schema) { schema_object.schema }
25
- extend BaseHash
26
- else
27
- define_singleton_method(:[]) { |*a, &b| schema_node.public_send(:[], *a, &b) }
28
- end
29
27
  end
28
+
29
+ # @return [JSI::JSON::Node] a JSI::JSON::Node for the schema
30
30
  attr_reader :schema_node
31
+
32
+ # @return [JSI::Base, nil] a JSI for this schema, if a metaschema is known; otherwise nil
33
+ attr_reader :schema_jsi
34
+
35
+ # @return [JSI::Base, JSI::JSON::Node] either a JSI::Base subclass or a
36
+ # JSI::JSON::Node for the schema
31
37
  def schema_object
32
- @schema_object || @schema_node
38
+ @schema_jsi || @schema_node
39
+ end
40
+
41
+ # @return [JSI::Base, JSI::JSON::Node, Object] property value from the schema_object
42
+ # @param property_name [String, Object] property name to access from the schema_object
43
+ def [](property_name)
44
+ schema_object[property_name]
33
45
  end
34
46
 
47
+ # @return [String] an absolute id for the schema, with a json pointer fragment
35
48
  def schema_id
36
49
  @schema_id ||= begin
37
50
  # start from schema_node and ascend parents looking for an 'id' property.
@@ -87,10 +100,17 @@ module JSI
87
100
  end
88
101
  end
89
102
 
103
+ # @return [Class subclassing JSI::Base] shortcut for JSI.class_for_schema(schema)
90
104
  def schema_class
91
105
  JSI.class_for_schema(self)
92
106
  end
93
107
 
108
+ # if this schema is a oneOf, allOf, anyOf schema, #match_to_instance finds
109
+ # one of the subschemas that matches the given instance and returns it. if
110
+ # there are no matching *Of schemas, this schema is returned.
111
+ #
112
+ # @param instance [Object] the instance to which to attempt to match *Of subschemas
113
+ # @return [JSI::Schema] a matched subschema, or this schema (self)
94
114
  def match_to_instance(instance)
95
115
  # matching oneOf is good here. one schema for one instance.
96
116
  # matching anyOf is okay. there could be more than one schema matched. it's often just one. if more
@@ -109,6 +129,10 @@ module JSI
109
129
  return self
110
130
  end
111
131
 
132
+ # @param property_name_ [String] the property for which to find a subschema
133
+ # @return [JSI::Schema, nil] a subschema from `properties`,
134
+ # `patternProperties`, or `additionalProperties` for the given
135
+ # property_name
112
136
  def subschema_for_property(property_name_)
113
137
  memoize(:subschema_for_property, property_name_) do |property_name|
114
138
  if schema_object['properties'].respond_to?(:to_hash) && schema_object['properties'][property_name].respond_to?(:to_hash)
@@ -132,6 +156,9 @@ module JSI
132
156
  end
133
157
  end
134
158
 
159
+ # @param index_ [Integer] the index for which to find a subschema
160
+ # @return [JSI::Schema, nil] a subschema from `items` or
161
+ # `additionalItems` for the given index
135
162
  def subschema_for_index(index_)
136
163
  memoize(:subschema_for_index, index_) do |index|
137
164
  if schema_object['items'].respond_to?(:to_ary)
@@ -148,44 +175,14 @@ module JSI
148
175
  end
149
176
  end
150
177
 
151
- def describes_array?
152
- memoize(:describes_array?) do
153
- schema_node['type'] == 'array' ||
154
- schema_node['items'] ||
155
- schema_node['additionalItems'] ||
156
- schema_node['default'].respond_to?(:to_ary) || # TODO make sure this is right
157
- (schema_node['enum'].respond_to?(:to_ary) && schema_node['enum'].all? { |enum| enum.respond_to?(:to_ary) }) ||
158
- schema_node['maxItems'] ||
159
- schema_node['minItems'] ||
160
- schema_node.key?('uniqueItems') ||
161
- schema_node['oneOf'].respond_to?(:to_ary) &&
162
- schema_node['oneOf'].all? { |someof_node| self.class.new(someof_node).describes_array? } ||
163
- schema_node['allOf'].respond_to?(:to_ary) &&
164
- schema_node['allOf'].all? { |someof_node| self.class.new(someof_node).describes_array? } ||
165
- schema_node['anyOf'].respond_to?(:to_ary) &&
166
- schema_node['anyOf'].all? { |someof_node| self.class.new(someof_node).describes_array? }
167
- end
168
- end
169
- def describes_hash?
170
- memoize(:describes_hash?) do
171
- schema_node['type'] == 'object' ||
172
- schema_node['required'].respond_to?(:to_ary) ||
173
- schema_node['properties'].respond_to?(:to_hash) ||
174
- schema_node['additionalProperties'] ||
175
- schema_node['patternProperties'] ||
176
- schema_node['default'].respond_to?(:to_hash) ||
177
- (schema_node['enum'].respond_to?(:to_ary) && schema_node['enum'].all? { |enum| enum.respond_to?(:to_hash) }) ||
178
- schema_node['oneOf'].respond_to?(:to_ary) &&
179
- schema_node['oneOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? } ||
180
- schema_node['allOf'].respond_to?(:to_ary) &&
181
- schema_node['allOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? } ||
182
- schema_node['anyOf'].respond_to?(:to_ary) &&
183
- schema_node['anyOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? }
184
- end
185
- end
186
-
187
- def described_hash_property_names
188
- memoize(:described_hash_property_names) do
178
+ # @return [Set] any object property names this schema indicates may be
179
+ # present on its instances. this includes, if present: keys of this
180
+ # schema's "properties" object; entries of this schema's array of
181
+ # "required" property keys. if this schema has oneOf/allOf/anyOf
182
+ # subschemas, those schemas are checked (recursively) for their
183
+ # described object property names.
184
+ def described_object_property_names
185
+ memoize(:described_object_property_names) do
189
186
  Set.new.tap do |property_names|
190
187
  if schema_node['properties'].respond_to?(:to_hash)
191
188
  property_names.merge(schema_node['properties'].keys)
@@ -197,30 +194,65 @@ module JSI
197
194
  # we should look at dependencies (TODO).
198
195
  %w(oneOf allOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |schemas_key|
199
196
  schema_node[schemas_key].map(&:deref).map do |someof_node|
200
- property_names.merge(self.class.new(someof_node).described_hash_property_names)
197
+ property_names.merge(self.class.new(someof_node).described_object_property_names)
201
198
  end
202
199
  end
203
200
  end
204
201
  end
205
202
  end
206
203
 
204
+ # @return [Array<String>] array of schema validation error messages for
205
+ # the given instance against this schema
207
206
  def fully_validate(instance)
208
- ::JSON::Validator.fully_validate(schema_node.document, object_to_content(instance), fragment: schema_node.fragment)
207
+ ::JSON::Validator.fully_validate(JSI::Typelike.as_json(schema_node.document), JSI::Typelike.as_json(instance), fragment: schema_node.fragment)
209
208
  end
209
+
210
+ # @return [true, false] whether the given instance validates against this schema
210
211
  def validate(instance)
211
- ::JSON::Validator.validate(schema_node.document, object_to_content(instance), fragment: schema_node.fragment)
212
+ ::JSON::Validator.validate(JSI::Typelike.as_json(schema_node.document), JSI::Typelike.as_json(instance), fragment: schema_node.fragment)
212
213
  end
214
+
215
+ # @return [true] if this method does not raise, it returns true to
216
+ # indicate the instance is valid against this schema
217
+ # @raise [::JSON::Schema::ValidationError] raises if the instance has
218
+ # validation errors against this schema
213
219
  def validate!(instance)
214
- ::JSON::Validator.validate!(schema_node.document, object_to_content(instance), fragment: schema_node.fragment)
220
+ ::JSON::Validator.validate!(JSI::Typelike.as_json(schema_node.document), JSI::Typelike.as_json(instance), fragment: schema_node.fragment)
215
221
  end
216
222
 
223
+ # @return [Array<String>] array of schema validation error messages for
224
+ # this schema, validated against its metaschema. a default metaschema
225
+ # is assumed if the schema does not specify a $schema.
226
+ def fully_validate_schema
227
+ ::JSON::Validator.fully_validate(JSI::Typelike.as_json(schema_node.document), [], fragment: schema_node.fragment, validate_schema: true, list: true)
228
+ end
229
+
230
+ # @return [true, false] whether this schema validates against its metaschema
231
+ def validate_schema
232
+ ::JSON::Validator.validate(JSI::Typelike.as_json(schema_node.document), [], fragment: schema_node.fragment, validate_schema: true, list: true)
233
+ end
234
+
235
+ # @return [true] if this method does not raise, it returns true to
236
+ # indicate this schema is valid against its metaschema
237
+ # @raise [::JSON::Schema::ValidationError] raises if this schema has
238
+ # validation errors against its metaschema
239
+ def validate_schema!
240
+ ::JSON::Validator.validate!(JSI::Typelike.as_json(schema_node.document), [], fragment: schema_node.fragment, validate_schema: true, list: true)
241
+ end
242
+
243
+ # @return [String] a string for #instance and #pretty_print including the schema_id
217
244
  def object_group_text
218
245
  "schema_id=#{schema_id}"
219
246
  end
247
+
248
+ # @return [String] a string representing this Schema
220
249
  def inspect
221
250
  "\#<#{self.class.inspect} #{object_group_text} #{schema_object.inspect}>"
222
251
  end
223
252
  alias_method :to_s, :inspect
253
+
254
+ # pretty-prints a representation this Schema to the given printer
255
+ # @return [void]
224
256
  def pretty_print(q)
225
257
  q.instance_exec(self) do |obj|
226
258
  text "\#<#{obj.class.inspect} #{obj.object_group_text}"
@@ -234,16 +266,16 @@ module JSI
234
266
  text '>'
235
267
  end
236
268
  end
269
+
270
+ # @return [Object] returns a jsonifiable representation of this schema
271
+ def as_json(*opt)
272
+ Typelike.as_json(schema_object, *opt)
273
+ end
274
+
275
+ # @return [Object] an opaque fingerprint of this Schema for FingerprintHash
237
276
  def fingerprint
238
277
  {class: self.class, schema_node: schema_node}
239
278
  end
240
279
  include FingerprintHash
241
-
242
- private
243
- def object_to_content(object)
244
- object = object.instance if object.is_a?(JSI::Base)
245
- object = object.content if object.is_a?(JSI::JSON::Node)
246
- object
247
- end
248
280
  end
249
281
  end