jsi 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Metaschema
5
+ include JSI::Schema::DescribesSchema
6
+ end
7
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ # a MetaschemaNode is a PathedNode whose node_document contains a metaschema.
5
+ # as with any PathedNode the node_ptr points to the content of a node.
6
+ # the root of the metaschema is pointed to by metaschema_root_ptr.
7
+ # the schema of the root of the document is pointed to by root_schema_ptr.
8
+ #
9
+ # like JSI::Base, this class represents an instance of a schema, an instance
10
+ # which may itself be a schema. unlike JSI::Base, the document containing the
11
+ # schema and the instance is the same, and a schema may be an instance of itself.
12
+ #
13
+ # the document containing the metaschema, its subschemas, and instances of those
14
+ # subschemas is the node_document.
15
+ #
16
+ # the schema instance is the content in the document pointed to by the MetaschemaNode's node_ptr.
17
+ #
18
+ # unlike with JSI::Base, the schema is not part of the class, since a metaschema
19
+ # needs the ability to have its schema be the instance itself.
20
+ #
21
+ # if the MetaschemaNode's schema is its self, it will be extended with JSI::Metaschema.
22
+ #
23
+ # a MetaschemaNode is extended with JSI::Schema when it represents a schema - this is the case when
24
+ # its schema is the metaschema.
25
+ class MetaschemaNode
26
+ include PathedNode
27
+ include Memoize
28
+
29
+ # not every MetaschemaNode is actually an Enumerable, but it's better to include Enumerable on
30
+ # the class than to conditionally extend the instance.
31
+ include Enumerable
32
+
33
+ # @param node_document the document containing the metaschema
34
+ # @param node_ptr [JSI::JSON::Pointer] ptr to this MetaschemaNode in node_document
35
+ # @param metaschema_root_ptr [JSI::JSON::Pointer] ptr to the root of the metaschema in node_document
36
+ # @param root_schema_ptr [JSI::JSON::Pointer] ptr to the schema of the root of the node_document
37
+ def initialize(node_document, node_ptr: JSI::JSON::Pointer[], metaschema_root_ptr: JSI::JSON::Pointer[], root_schema_ptr: JSI::JSON::Pointer[])
38
+ @node_document = node_document
39
+ @node_ptr = node_ptr
40
+ @metaschema_root_ptr = metaschema_root_ptr
41
+ @root_schema_ptr = root_schema_ptr
42
+
43
+ node_content = self.node_content
44
+
45
+ if node_content.respond_to?(:to_hash)
46
+ extend PathedHashNode
47
+ elsif node_content.respond_to?(:to_ary)
48
+ extend PathedArrayNode
49
+ end
50
+
51
+ instance_for_schema = node_document
52
+ schema_ptr = node_ptr.reference_tokens.inject(root_schema_ptr) do |ptr0, tok|
53
+ if instance_for_schema.respond_to?(:to_ary)
54
+ ptr1 = ptr0 && ptr0.schema_subschema_ptr_for_index(node_document, tok)
55
+ else
56
+ ptr1 = ptr0 && ptr0.schema_subschema_ptr_for_property_name(node_document, tok)
57
+ end
58
+ instance_for_schema = instance_for_schema[tok]
59
+ ptr2 = ptr1 && ptr1.deref(node_document)
60
+ ptr3 = ptr2 && ptr2.schema_match_ptr_to_instance(node_document, instance_for_schema)
61
+ ptr3 # only God may judge my variable names
62
+ end
63
+
64
+ @schema = if schema_ptr
65
+ if schema_ptr == node_ptr
66
+ self
67
+ else
68
+ new_node(node_ptr: schema_ptr)
69
+ end
70
+ else
71
+ nil
72
+ end
73
+
74
+ if schema_ptr
75
+ if schema_ptr == metaschema_root_ptr
76
+ extend JSI::Schema
77
+ end
78
+ if schema_ptr == node_ptr
79
+ extend Metaschema
80
+ end
81
+ end
82
+
83
+ if @schema
84
+ extend(JSI::SchemaClasses.accessor_module_for_schema(@schema, conflicting_modules: [Metaschema, Schema, MetaschemaNode, PathedArrayNode, PathedHashNode]))
85
+ end
86
+
87
+ # workarounds
88
+ begin # draft 4 boolean schema workaround
89
+ # in draft 4, boolean schemas are not described in the root, but on anyOf schemas on
90
+ # properties/additionalProperties and properties/additionalItems.
91
+ # we need to extend those as DescribesSchema.
92
+ addtlPropsanyOf = metaschema_root_ptr["properties"]["additionalProperties"]["anyOf"]
93
+ addtlItemsanyOf = metaschema_root_ptr["properties"]["additionalItems"]["anyOf"]
94
+
95
+ if !node_ptr.root? && [addtlPropsanyOf, addtlItemsanyOf].include?(node_ptr.parent)
96
+ extend JSI::Schema::DescribesSchema
97
+ end
98
+ end
99
+ end
100
+
101
+ # document containing the metaschema. see PathedNode#node_document.
102
+ attr_reader :node_document
103
+ # ptr to this metaschema node. see PathedNode#node_ptr.
104
+ attr_reader :node_ptr
105
+ # ptr to the root of the metaschema in the node_document
106
+ attr_reader :metaschema_root_ptr
107
+ # ptr to the schema of the root of the node_document
108
+ attr_reader :root_schema_ptr
109
+ # a JSI::Schema describing this MetaschemaNode
110
+ attr_reader :schema
111
+
112
+ # @return [MetaschemaNode] document root MetaschemaNode
113
+ def document_root_node
114
+ new_node(node_ptr: JSI::JSON::Pointer[])
115
+ end
116
+
117
+ # @return [MetaschemaNode] parent MetaschemaNode
118
+ def parent_node
119
+ new_node(node_ptr: node_ptr.parent)
120
+ end
121
+
122
+ # @param token [String, Integer, Object] the token to subscript
123
+ # @return [MetaschemaNode, Object] the node content's subscript value at the given token.
124
+ # if there is a subschema defined for that token on this MetaschemaNode's schema,
125
+ # returns that value as a MetaschemaNode instantiation of that subschema.
126
+ def [](token)
127
+ if respond_to?(:to_hash)
128
+ token_in_range = node_content_hash_pubsend(:key?, token)
129
+ value = node_content_hash_pubsend(:[], token)
130
+ elsif respond_to?(:to_ary)
131
+ token_in_range = node_content_ary_pubsend(:each_index).include?(token)
132
+ value = node_content_ary_pubsend(:[], token)
133
+ else
134
+ raise(NoMethodError, "cannot subcript (using token: #{token.inspect}) from content: #{node_content.pretty_inspect.chomp}")
135
+ end
136
+
137
+ jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range|
138
+ if token_in_range
139
+ value_node = new_node(node_ptr: node_ptr[token])
140
+
141
+ if value_node.is_a?(Schema) || value.respond_to?(:to_hash) || value.respond_to?(:to_ary)
142
+ value_node
143
+ else
144
+ value
145
+ end
146
+ else
147
+ # I think I will not support Hash#default/#default_proc in this case.
148
+ nil
149
+ end
150
+ end
151
+ end
152
+
153
+ # if this MetaschemaNode is a $ref then the $ref is followed. otherwise this MetaschemaNode is returned.
154
+ # @return [MetaschemaNode]
155
+ def deref(&block)
156
+ node_ptr_deref do |deref_ptr|
157
+ return new_node(node_ptr: deref_ptr).tap(&(block || Util::NOOP))
158
+ end
159
+ return self
160
+ end
161
+
162
+ # @yield [Object] the node content of the instance. the block should result
163
+ # in a (nondestructively) modified copy of this.
164
+ # @return [MetaschemaNode] modified copy of self
165
+ def modified_copy(&block)
166
+ MetaschemaNode.new(node_ptr.modified_document_copy(node_document, &block), our_initialize_params)
167
+ end
168
+
169
+ # @return [String]
170
+ def inspect
171
+ "\#<#{object_group_text.join(' ')} #{node_content.inspect}>"
172
+ end
173
+
174
+ def pretty_print(q)
175
+ q.text '#<'
176
+ q.text object_group_text.join(' ')
177
+ q.group_sub {
178
+ q.nest(2) {
179
+ q.breakable ' '
180
+ q.pp node_content
181
+ }
182
+ }
183
+ q.breakable ''
184
+ q.text '>'
185
+ end
186
+
187
+ # @return [Array<String>]
188
+ def object_group_text
189
+ if schema
190
+ class_n_schema = "#{self.class} (#{schema.node_ptr.fragment})"
191
+ else
192
+ class_n_schema = self.class.to_s
193
+ end
194
+ [
195
+ class_n_schema,
196
+ is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
197
+ *(node_content.respond_to?(:object_group_text) ? node_content.object_group_text : []),
198
+ ].compact
199
+ end
200
+
201
+ # @return [Object] an opaque fingerprint of this MetaschemaNode for FingerprintHash
202
+ def jsi_fingerprint
203
+ {class: self.class, node_document: node_document}.merge(our_initialize_params)
204
+ end
205
+ include FingerprintHash
206
+
207
+ private
208
+
209
+ def our_initialize_params
210
+ {node_ptr: node_ptr, metaschema_root_ptr: metaschema_root_ptr, root_schema_ptr: root_schema_ptr}
211
+ end
212
+
213
+ def new_node(params)
214
+ MetaschemaNode.new(node_document, our_initialize_params.merge(params))
215
+ end
216
+ end
217
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
2
4
  # including class MUST define
3
5
  # - #node_document [Object] returning the document
@@ -10,11 +12,14 @@ module JSI
10
12
  #
11
13
  # the node content (#node_content) is the result of evaluating the node document at the path.
12
14
  module PathedNode
15
+ # @return [Object] the content of this node
13
16
  def node_content
14
17
  content = node_ptr.evaluate(node_document)
15
18
  content
16
19
  end
17
20
 
21
+ # @yield [JSI::JSON::Pointer] if a block is given (optional), this will yield a deref'd pointer
22
+ # @return [JSI::JSON::Pointer] our node_ptr, derefed against our node_document
18
23
  def node_ptr_deref(&block)
19
24
  node_ptr.deref(node_document, &block)
20
25
  end
@@ -1,80 +1,101 @@
1
- require 'jsi/json/node'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module JSI
4
4
  # JSI::Schema represents a JSON Schema. initialized from a Hash-like schema
5
5
  # object, JSI::Schema is a relatively simple class to abstract useful methods
6
6
  # applied to a JSON Schema.
7
- class Schema
7
+ module Schema
8
+ class Error < StandardError
9
+ end
10
+
11
+ # an exception raised when a thing is expected to be a JSI::Schema, but is not
12
+ class NotASchemaError < Error
13
+ end
14
+
8
15
  include Memoize
9
16
 
17
+ # JSI::Schema::DescribesSchema: a schema which describes another schema. this module
18
+ # extends a JSI::Schema instance and indicates that JSIs which instantiate the schema
19
+ # are themselves also schemas.
20
+ #
21
+ # examples of a schema which describes a schema include the draft JSON Schema metaschemas and
22
+ # the OpenAPI schema definition which describes "A deterministic version of a JSON Schema object."
23
+ module DescribesSchema
24
+ end
25
+
10
26
  class << self
27
+ # @return [JSI::Schema] the default metaschema
28
+ def default_metaschema
29
+ JSI::JSONSchemaOrgDraft06.schema
30
+ end
31
+
32
+ # @return [Array<JSI::Schema>] supported metaschemas
33
+ def supported_metaschemas
34
+ [
35
+ JSI::JSONSchemaOrgDraft04.schema,
36
+ JSI::JSONSchemaOrgDraft06.schema,
37
+ ]
38
+ end
39
+
40
+ # instantiates a given schema object as a JSI::Schema.
41
+ #
42
+ # schemas are instantiated according to their '$schema' property if specified. otherwise their schema
43
+ # will be the {JSI::Schema.default_metaschema}.
44
+ #
45
+ # if the given schema_object is a JSI::Base but not already a JSI::Schema, an error
46
+ # will be raised. JSI::Base _should_ already extend a given instance with JSI::Schema
47
+ # when its schema describes a schema (by extending with JSI::Schema::DescribesSchema).
48
+ #
11
49
  # @param schema_object [#to_hash, Boolean, JSI::Schema] an object to be instantiated as a schema.
12
50
  # if it's already a schema, it is returned as-is.
13
- # @return [JSI::Schema]
51
+ # @return [JSI::Schema] a JSI::Schema representing the given schema_object
14
52
  def from_object(schema_object)
15
53
  if schema_object.is_a?(Schema)
16
54
  schema_object
55
+ elsif schema_object.is_a?(JSI::Base)
56
+ raise(NotASchemaError, "the given schema_object is a JSI::Base, but is not a JSI::Schema: #{schema_object.pretty_inspect.chomp}")
57
+ elsif schema_object.respond_to?(:to_hash)
58
+ schema_object = JSI.deep_stringify_symbol_keys(schema_object)
59
+ if schema_object.key?('$schema') && schema_object['$schema'].respond_to?(:to_str)
60
+ if schema_object['$schema'] == schema_object['$id'] || schema_object['$schema'] == schema_object['id']
61
+ MetaschemaNode.new(schema_object)
62
+ else
63
+ metaschema = supported_metaschemas.detect { |ms| schema_object['$schema'] == ms['$id'] || schema_object['$schema'] == ms['id'] }
64
+ unless metaschema
65
+ raise(NotImplementedError, "metaschema not supported: #{schema_object['$schema']}")
66
+ end
67
+ metaschema.new_jsi(schema_object)
68
+ end
69
+ else
70
+ default_metaschema.new_jsi(schema_object)
71
+ end
72
+ elsif [true, false].include?(schema_object)
73
+ default_metaschema.new_jsi(schema_object)
17
74
  else
18
- new(schema_object)
75
+ raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
19
76
  end
20
77
  end
21
- end
22
-
23
- # initializes a schema from a given JSI::Base, JSI::JSON::Node, or hash. Boolean schemas are
24
- # instantiated as their equivalent hash ({} for true and {"not" => {}} for false).
25
- #
26
- # @param schema_object [JSI::Base, #to_hash, Boolean] the schema
27
- def initialize(schema_object)
28
- if schema_object.is_a?(JSI::Schema)
29
- raise(TypeError, "will not instantiate Schema from another Schema: #{schema_object.pretty_inspect.chomp}")
30
- elsif schema_object.is_a?(JSI::PathedNode)
31
- @schema_node = JSI.deep_stringify_symbol_keys(schema_object.deref)
32
- elsif schema_object.respond_to?(:to_hash)
33
- @schema_node = JSI::JSON::Node.new_doc(JSI.deep_stringify_symbol_keys(schema_object))
34
- elsif schema_object == true
35
- @schema_node = JSI::JSON::Node.new_doc({})
36
- elsif schema_object == false
37
- @schema_node = JSI::JSON::Node.new_doc({"not" => {}})
38
- else
39
- raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
40
- end
41
- end
42
78
 
43
- # @return [JSI::PathedNode] a JSI::PathedNode (JSI::JSON::Node or JSI::Base) for the schema
44
- attr_reader :schema_node
45
-
46
- alias_method :schema_object, :schema_node
47
-
48
- # @return [JSI::Base, JSI::JSON::Node, Object] property value from the schema_object
49
- # @param property_name [String, Object] property name to access from the schema_object
50
- def [](property_name)
51
- schema_object[property_name]
79
+ alias_method :new, :from_object
52
80
  end
53
81
 
54
- # @return [String] an absolute id for the schema, with a json pointer fragment
82
+ # @return [String, nil] an absolute id for the schema, with a json pointer fragment. nil if
83
+ # no parent of this schema defines an id.
55
84
  def schema_id
56
- @schema_id ||= begin
57
- # start from schema_node and ascend parents looking for an 'id' property.
85
+ return @schema_id if instance_variable_defined?(:@schema_id)
86
+ @schema_id = begin
87
+ # start from self and ascend parents looking for an 'id' property.
58
88
  # append a fragment to that id (appending to an existing fragment if there
59
89
  # is one) consisting of the path from that parent to our schema_node.
60
- node_for_id = schema_node
90
+ node_for_id = self
61
91
  path_from_id_node = []
62
92
  done = false
63
93
 
64
94
  while !done
65
- # TODO: track what parents are schemas. somehow.
66
- # look at 'id' if node_for_id is a schema, or the document root.
67
- # decide whether to look at '$id' for all parent nodes or also just schemas.
68
- if node_for_id.respond_to?(:to_hash)
69
- if node_for_id.node_ptr.root? || node_for_id.object_id == schema_node.object_id
70
- # I'm only looking at 'id' for the document root and the schema node
71
- # until I track what parents are schemas.
72
- parent_id = node_for_id['$id'] || node_for_id['id']
73
- else
74
- # will look at '$id' everywhere since it is less likely to show up outside schemas than
75
- # 'id', but it will be better to only look at parents that are schemas for this too.
76
- parent_id = node_for_id['$id']
77
- end
95
+ node_content_for_id = node_for_id.node_content
96
+ if node_for_id.is_a?(JSI::Schema) && node_content_for_id.respond_to?(:to_hash)
97
+ parent_id = node_content_for_id.key?('$id') && node_content_for_id['$id'].respond_to?(:to_str) ? node_content_for_id['$id'].to_str :
98
+ node_content_for_id.key?('id') && node_content_for_id['id'].respond_to?(:to_str) ? node_content_for_id['id'].to_str : nil
78
99
  end
79
100
 
80
101
  if parent_id || node_for_id.node_ptr.root?
@@ -86,40 +107,45 @@ module JSI
86
107
  end
87
108
  if parent_id
88
109
  parent_auri = Addressable::URI.parse(parent_id)
89
- else
90
- node_for_id = schema_node.document_root_node
91
- validator = ::JSON::Validator.new(Typelike.as_json(node_for_id), nil)
92
- # TODO not good instance_exec'ing into another library's ivars
93
- parent_auri = validator.instance_exec { @base_schema }.uri
94
- end
95
- if parent_auri.fragment
96
- # add onto the fragment
97
- parent_id_path = JSI::JSON::Pointer.from_fragment('#' + parent_auri.fragment).reference_tokens
98
- path_from_id_node = parent_id_path + path_from_id_node
99
- parent_auri.fragment = nil
100
- #else: no fragment so parent_id good as is
101
- end
110
+ if parent_auri.fragment
111
+ # add onto the fragment
112
+ parent_id_path = JSI::JSON::Pointer.from_fragment('#' + parent_auri.fragment).reference_tokens
113
+ path_from_id_node = parent_id_path + path_from_id_node
114
+ parent_auri.fragment = nil
115
+ #else: no fragment so parent_id good as is
116
+ end
102
117
 
103
- fragment = JSI::JSON::Pointer.new(path_from_id_node).fragment
104
- schema_id = parent_auri.to_s + fragment
118
+ fragment = JSI::JSON::Pointer.new(path_from_id_node).fragment
119
+ schema_id = parent_auri.to_s + fragment
105
120
 
106
- schema_id
121
+ schema_id
122
+ else
123
+ nil
124
+ end
107
125
  end
108
126
  end
109
127
 
128
+ # @return [Module] a module representing this schema. see {JSI::SchemaClasses.module_for_schema}.
129
+ def jsi_schema_module
130
+ JSI::SchemaClasses.module_for_schema(self)
131
+ end
132
+
110
133
  # @return [Class subclassing JSI::Base] shortcut for JSI.class_for_schema(schema)
111
134
  def jsi_schema_class
112
135
  JSI.class_for_schema(self)
113
136
  end
114
137
 
115
- alias_method :schema_class, :jsi_schema_class
116
-
117
138
  # calls #new on the class for this schema with the given arguments. for parameters,
118
139
  # see JSI::Base#initialize documentation.
119
140
  #
120
141
  # @return [JSI::Base] a JSI whose schema is this schema and whose instance is the given instance
121
142
  def new_jsi(other_instance, *a, &b)
122
- jsi_schema_class.new(other_instance, *a, &b)
143
+ JSI.class_for_schema(match_to_instance(other_instance)).new(other_instance, *a, &b)
144
+ end
145
+
146
+ # @return [Boolean] does this schema itself describe a schema?
147
+ def describes_schema?
148
+ is_a?(JSI::Schema::DescribesSchema)
123
149
  end
124
150
 
125
151
  # if this schema is a oneOf, allOf, anyOf schema, #match_to_instance finds
@@ -129,60 +155,42 @@ module JSI
129
155
  # @param other_instance [Object] the instance to which to attempt to match *Of subschemas
130
156
  # @return [JSI::Schema] a matched subschema, or this schema (self)
131
157
  def match_to_instance(other_instance)
132
- # matching oneOf is good here. one schema for one instance.
133
- # matching anyOf is okay. there could be more than one schema matched. it's often just one. if more
134
- # than one is a match, you just get the first one.
135
- %w(oneOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |someof_key|
136
- schema_node[someof_key].map(&:deref).map do |someof_node|
137
- someof_schema = self.class.new(someof_node)
138
- if someof_schema.validate_instance(other_instance)
139
- return someof_schema.match_to_instance(other_instance)
140
- end
141
- end
158
+ ptr = node_ptr
159
+ ptr = ptr.deref(node_document)
160
+ ptr = ptr.schema_match_ptr_to_instance(node_document, other_instance)
161
+ if ptr
162
+ ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
163
+ else
164
+ self
142
165
  end
143
- return self
144
166
  end
145
167
 
146
- # @param property_name_ [String] the property for which to find a subschema
147
- # @return [JSI::Schema, nil] a subschema from `properties`,
148
- # `patternProperties`, or `additionalProperties` for the given
149
- # property_name
150
- def subschema_for_property(property_name_)
151
- memoize(:subschema_for_property, property_name_) do |property_name|
152
- if schema_object['properties'].respond_to?(:to_hash) && schema_object['properties'][property_name].respond_to?(:to_hash)
153
- self.class.new(schema_object['properties'][property_name])
168
+ # @param property_name [String] the property name for which to find a subschema
169
+ # @return [JSI::Schema, nil] a subschema from `properties`, `patternProperties`, or `additionalProperties` for the given token
170
+ def subschema_for_property(property_name)
171
+ jsi_memoize(:subschema_for_property, property_name) do |property_name|
172
+ ptr = node_ptr
173
+ ptr = ptr.deref(node_document)
174
+ ptr = ptr.schema_subschema_ptr_for_property_name(node_document, property_name)
175
+ if ptr
176
+ ptr = ptr.deref(node_document)
177
+ ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
154
178
  else
155
- if schema_object['patternProperties'].respond_to?(:to_hash)
156
- _, pattern_schema_object = schema_object['patternProperties'].detect do |pattern, _|
157
- property_name.to_s =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
158
- end
159
- end
160
- if pattern_schema_object
161
- self.class.new(pattern_schema_object)
162
- else
163
- if schema_object['additionalProperties'].respond_to?(:to_hash)
164
- self.class.new(schema_object['additionalProperties'])
165
- else
166
- nil
167
- end
168
- end
179
+ nil
169
180
  end
170
181
  end
171
182
  end
172
183
 
173
- # @param index_ [Integer] the index for which to find a subschema
174
- # @return [JSI::Schema, nil] a subschema from `items` or
175
- # `additionalItems` for the given index
176
- def subschema_for_index(index_)
177
- memoize(:subschema_for_index, index_) do |index|
178
- if schema_object['items'].respond_to?(:to_ary)
179
- if index < schema_object['items'].size
180
- self.class.new(schema_object['items'][index])
181
- elsif schema_object['additionalItems'].respond_to?(:to_hash)
182
- self.class.new(schema_object['additionalItems'])
183
- end
184
- elsif schema_object['items'].respond_to?(:to_hash)
185
- self.class.new(schema_object['items'])
184
+ # @param index [Integer] the array index for which to find a subschema
185
+ # @return [JSI::Schema, nil] a subschema from `items` or `additionalItems` for the given token
186
+ def subschema_for_index(index)
187
+ jsi_memoize(:subschema_for_index, index) do |index|
188
+ ptr = node_ptr
189
+ ptr = ptr.deref(node_document)
190
+ ptr = ptr.schema_subschema_ptr_for_index(node_document, index)
191
+ if ptr
192
+ ptr = ptr.deref(node_document)
193
+ ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
186
194
  else
187
195
  nil
188
196
  end
@@ -195,33 +203,33 @@ module JSI
195
203
  # "required" property keys. if this schema has allOf subschemas, those
196
204
  # schemas are checked (recursively) for their described object property names.
197
205
  def described_object_property_names
198
- memoize(:described_object_property_names) do
206
+ jsi_memoize(:described_object_property_names) do
199
207
  Set.new.tap do |property_names|
200
- if schema_node['properties'].respond_to?(:to_hash)
201
- property_names.merge(schema_node['properties'].keys)
208
+ if node_content.respond_to?(:to_hash) && node_content['properties'].respond_to?(:to_hash)
209
+ property_names.merge(node_content['properties'].keys)
202
210
  end
203
- if schema_node['required'].respond_to?(:to_ary)
204
- property_names.merge(schema_node['required'].to_ary)
211
+ if node_content.respond_to?(:to_hash) && node_content['required'].respond_to?(:to_ary)
212
+ property_names.merge(node_content['required'].to_ary)
205
213
  end
206
214
  # we should look at dependencies (TODO).
207
- if schema_node['allOf'].respond_to?(:to_ary)
208
- schema_node['allOf'].map(&:deref).map do |allOf_node|
209
- property_names.merge(self.class.new(allOf_node).described_object_property_names)
215
+ if respond_to?(:to_hash) && self['allOf'].respond_to?(:to_ary)
216
+ self['allOf'].select{ |s| s.is_a?(JSI::Schema) }.map(&:deref).map do |allOf_schema|
217
+ property_names.merge(allOf_schema.described_object_property_names)
210
218
  end
211
219
  end
212
220
  end
213
221
  end
214
222
  end
215
223
 
216
- # @return [Array<String>] array of schema validation error messages for
224
+ # @return [Array] array of schema validation errors for
217
225
  # the given instance against this schema
218
- def fully_validate_instance(other_instance)
219
- ::JSON::Validator.fully_validate(JSI::Typelike.as_json(schema_node.node_document), JSI::Typelike.as_json(other_instance), fragment: schema_node.node_ptr.fragment)
226
+ def fully_validate_instance(other_instance, errors_as_objects: false)
227
+ ::JSON::Validator.fully_validate(JSI::Typelike.as_json(node_document), JSI::Typelike.as_json(other_instance), fragment: node_ptr.fragment, errors_as_objects: errors_as_objects)
220
228
  end
221
229
 
222
230
  # @return [true, false] whether the given instance validates against this schema
223
231
  def validate_instance(other_instance)
224
- ::JSON::Validator.validate(JSI::Typelike.as_json(schema_node.node_document), JSI::Typelike.as_json(other_instance), fragment: schema_node.node_ptr.fragment)
232
+ ::JSON::Validator.validate(JSI::Typelike.as_json(node_document), JSI::Typelike.as_json(other_instance), fragment: node_ptr.fragment)
225
233
  end
226
234
 
227
235
  # @return [true] if this method does not raise, it returns true to
@@ -229,19 +237,19 @@ module JSI
229
237
  # @raise [::JSON::Schema::ValidationError] raises if the instance has
230
238
  # validation errors against this schema
231
239
  def validate_instance!(other_instance)
232
- ::JSON::Validator.validate!(JSI::Typelike.as_json(schema_node.node_document), JSI::Typelike.as_json(other_instance), fragment: schema_node.node_ptr.fragment)
240
+ ::JSON::Validator.validate!(JSI::Typelike.as_json(node_document), JSI::Typelike.as_json(other_instance), fragment: node_ptr.fragment)
233
241
  end
234
242
 
235
- # @return [Array<String>] array of schema validation error messages for
243
+ # @return [Array] array of schema validation errors for
236
244
  # this schema, validated against its metaschema. a default metaschema
237
245
  # is assumed if the schema does not specify a $schema.
238
- def fully_validate_schema
239
- ::JSON::Validator.fully_validate(JSI::Typelike.as_json(schema_node.node_document), [], fragment: schema_node.node_ptr.fragment, validate_schema: true, list: true)
246
+ def fully_validate_schema(errors_as_objects: false)
247
+ ::JSON::Validator.fully_validate(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true, errors_as_objects: errors_as_objects)
240
248
  end
241
249
 
242
250
  # @return [true, false] whether this schema validates against its metaschema
243
251
  def validate_schema
244
- ::JSON::Validator.validate(JSI::Typelike.as_json(schema_node.node_document), [], fragment: schema_node.node_ptr.fragment, validate_schema: true, list: true)
252
+ ::JSON::Validator.validate(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true)
245
253
  end
246
254
 
247
255
  # @return [true] if this method does not raise, it returns true to
@@ -249,45 +257,14 @@ module JSI
249
257
  # @raise [::JSON::Schema::ValidationError] raises if this schema has
250
258
  # validation errors against its metaschema
251
259
  def validate_schema!
252
- ::JSON::Validator.validate!(JSI::Typelike.as_json(schema_node.node_document), [], fragment: schema_node.node_ptr.fragment, validate_schema: true, list: true)
253
- end
254
-
255
- # @return [String] a string for #instance and #pretty_print including the schema_id
256
- def object_group_text
257
- "schema_id=#{schema_id}"
258
- end
259
-
260
- # @return [String] a string representing this Schema
261
- def inspect
262
- "\#<#{self.class.inspect} #{object_group_text} #{schema_object.inspect}>"
260
+ ::JSON::Validator.validate!(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true)
263
261
  end
264
- alias_method :to_s, :inspect
265
262
 
266
- # pretty-prints a representation this Schema to the given printer
267
- # @return [void]
268
- def pretty_print(q)
269
- q.instance_exec(self) do |obj|
270
- text "\#<#{obj.class.inspect} #{obj.object_group_text}"
271
- group_sub {
272
- nest(2) {
273
- breakable ' '
274
- pp obj.schema_object
275
- }
276
- }
277
- breakable ''
278
- text '>'
263
+ private
264
+ def jsi_ensure_subschema_is_schema(subschema, ptr)
265
+ unless subschema.is_a?(JSI::Schema)
266
+ raise(NotASchemaError, "subschema not a schema at ptr #{ptr.inspect}: #{subschema.pretty_inspect.chomp}")
279
267
  end
280
268
  end
281
-
282
- # @return [Object] returns a jsonifiable representation of this schema
283
- def as_json(*opt)
284
- Typelike.as_json(schema_object, *opt)
285
- end
286
-
287
- # @return [Object] an opaque fingerprint of this Schema for FingerprintHash
288
- def fingerprint
289
- {class: self.class, schema_ptr: schema_node.node_ptr, schema_document: JSI::Typelike.as_json(schema_node.node_document)}
290
- end
291
- include FingerprintHash
292
269
  end
293
270
  end