jsi 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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