jsi 0.2.0 → 0.6.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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +36 -0
  4. data/LICENSE.md +613 -0
  5. data/README.md +153 -52
  6. data/lib/jsi/base.rb +485 -338
  7. data/lib/jsi/jsi_coder.rb +24 -18
  8. data/lib/jsi/metaschema.rb +7 -0
  9. data/lib/jsi/metaschema_node/bootstrap_schema.rb +100 -0
  10. data/lib/jsi/metaschema_node.rb +245 -0
  11. data/lib/jsi/pathed_node.rb +49 -46
  12. data/lib/jsi/ptr.rb +292 -0
  13. data/lib/jsi/schema/application/child_application/contains.rb +16 -0
  14. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  15. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  16. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  17. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  18. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  19. data/lib/jsi/schema/application/child_application.rb +40 -0
  20. data/lib/jsi/schema/application/draft04.rb +8 -0
  21. data/lib/jsi/schema/application/draft06.rb +8 -0
  22. data/lib/jsi/schema/application/draft07.rb +8 -0
  23. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  24. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  25. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  26. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  27. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  28. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  29. data/lib/jsi/schema/application/inplace_application/someof.rb +29 -0
  30. data/lib/jsi/schema/application/inplace_application.rb +46 -0
  31. data/lib/jsi/schema/application.rb +12 -0
  32. data/lib/jsi/schema/draft04.rb +14 -0
  33. data/lib/jsi/schema/draft06.rb +14 -0
  34. data/lib/jsi/schema/draft07.rb +14 -0
  35. data/lib/jsi/schema/issue.rb +36 -0
  36. data/lib/jsi/schema/ref.rb +159 -0
  37. data/lib/jsi/schema/schema_ancestor_node.rb +119 -0
  38. data/lib/jsi/schema/validation/array.rb +69 -0
  39. data/lib/jsi/schema/validation/const.rb +20 -0
  40. data/lib/jsi/schema/validation/contains.rb +25 -0
  41. data/lib/jsi/schema/validation/core.rb +39 -0
  42. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  43. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  44. data/lib/jsi/schema/validation/draft04.rb +112 -0
  45. data/lib/jsi/schema/validation/draft06.rb +122 -0
  46. data/lib/jsi/schema/validation/draft07.rb +159 -0
  47. data/lib/jsi/schema/validation/enum.rb +25 -0
  48. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  49. data/lib/jsi/schema/validation/items.rb +54 -0
  50. data/lib/jsi/schema/validation/not.rb +20 -0
  51. data/lib/jsi/schema/validation/numeric.rb +121 -0
  52. data/lib/jsi/schema/validation/object.rb +45 -0
  53. data/lib/jsi/schema/validation/pattern.rb +34 -0
  54. data/lib/jsi/schema/validation/properties.rb +101 -0
  55. data/lib/jsi/schema/validation/property_names.rb +32 -0
  56. data/lib/jsi/schema/validation/ref.rb +40 -0
  57. data/lib/jsi/schema/validation/required.rb +27 -0
  58. data/lib/jsi/schema/validation/someof.rb +90 -0
  59. data/lib/jsi/schema/validation/string.rb +47 -0
  60. data/lib/jsi/schema/validation/type.rb +49 -0
  61. data/lib/jsi/schema/validation.rb +51 -0
  62. data/lib/jsi/schema.rb +528 -233
  63. data/lib/jsi/schema_classes.rb +238 -51
  64. data/lib/jsi/schema_registry.rb +141 -0
  65. data/lib/jsi/schema_set.rb +141 -0
  66. data/lib/jsi/simple_wrap.rb +8 -3
  67. data/lib/jsi/typelike_modules.rb +75 -68
  68. data/lib/jsi/util/attr_struct.rb +106 -0
  69. data/lib/jsi/util.rb +167 -64
  70. data/lib/jsi/validation/error.rb +34 -0
  71. data/lib/jsi/validation/result.rb +210 -0
  72. data/lib/jsi/validation.rb +15 -0
  73. data/lib/jsi/version.rb +3 -1
  74. data/lib/jsi.rb +72 -9
  75. data/lib/schemas/json-schema.org/draft-04/schema.rb +12 -0
  76. data/lib/schemas/json-schema.org/draft-06/schema.rb +12 -0
  77. data/lib/schemas/json-schema.org/draft-07/schema.rb +12 -0
  78. data/readme.rb +138 -0
  79. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  80. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  81. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  82. metadata +80 -107
  83. data/.simplecov +0 -1
  84. data/LICENSE.txt +0 -21
  85. data/Rakefile.rb +0 -9
  86. data/jsi.gemspec +0 -31
  87. data/lib/jsi/base/to_rb.rb +0 -126
  88. data/lib/jsi/json/node.rb +0 -243
  89. data/lib/jsi/json/pointer.rb +0 -330
  90. data/lib/jsi/json-schema-fragments.rb +0 -59
  91. data/lib/jsi/json.rb +0 -8
  92. data/test/base_array_test.rb +0 -209
  93. data/test/base_hash_test.rb +0 -204
  94. data/test/base_test.rb +0 -422
  95. data/test/jsi_coder_test.rb +0 -85
  96. data/test/jsi_json_arraynode_test.rb +0 -150
  97. data/test/jsi_json_hashnode_test.rb +0 -132
  98. data/test/jsi_json_node_test.rb +0 -310
  99. data/test/jsi_json_pointer_test.rb +0 -106
  100. data/test/jsi_test.rb +0 -11
  101. data/test/jsi_typelike_as_json_test.rb +0 -53
  102. data/test/schema_test.rb +0 -196
  103. data/test/spreedly_openapi_test.rb +0 -8
  104. data/test/test_helper.rb +0 -63
  105. data/test/util_test.rb +0 -62
data/lib/jsi/schema.rb CHANGED
@@ -1,311 +1,606 @@
1
- require 'jsi/json/node'
1
+ # frozen_string_literal: true
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.
7
- class Schema
8
- include Memoize
4
+ # JSI::Schema is a module which extends instances which represent JSON schemas.
5
+ #
6
+ # the content of an instance which is a JSI::Schema (referred to in this context as schema_content) is
7
+ # expected to be a Hash (JSON object) or a Boolean.
8
+ module Schema
9
+ autoload :Application, 'jsi/schema/application'
10
+ autoload :Validation, 'jsi/schema/validation'
11
+ autoload :Issue, 'jsi/schema/issue'
9
12
 
10
- class << self
11
- # @param schema_object [#to_hash, Boolean, JSI::Schema] an object to be instantiated as a schema.
12
- # if it's already a schema, it is returned as-is.
13
- # @return [JSI::Schema]
14
- def from_object(schema_object)
15
- if schema_object.is_a?(Schema)
16
- schema_object
13
+ autoload :SchemaAncestorNode, 'jsi/schema/schema_ancestor_node'
14
+
15
+ autoload :Ref, 'jsi/schema/ref'
16
+
17
+ autoload :Draft04, 'jsi/schema/draft04'
18
+ autoload :Draft06, 'jsi/schema/draft06'
19
+ autoload :Draft07, 'jsi/schema/draft07'
20
+
21
+ class Error < StandardError
22
+ end
23
+
24
+ # an exception raised when a thing is expected to be a JSI::Schema, but is not
25
+ class NotASchemaError < Error
26
+ end
27
+
28
+ # an exception raised when we are unable to resolve a schema reference
29
+ class ReferenceError < StandardError
30
+ end
31
+
32
+ # extends any schema which uses the keyword '$id' to identify its canonical URI
33
+ module BigMoneyId
34
+ # the contents of a $id keyword whose value is a string, or nil
35
+ # @return [#to_str, nil]
36
+ def id
37
+ if schema_content.respond_to?(:to_hash) && schema_content['$id'].respond_to?(:to_str)
38
+ schema_content['$id']
17
39
  else
18
- new(schema_object)
40
+ nil
19
41
  end
20
42
  end
21
43
  end
22
44
 
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}")
45
+ # extends any schema which uses the keyword 'id' to identify its canonical URI
46
+ module OldId
47
+ # the contents of an `id` keyword whose value is a string, or nil
48
+ # @return [#to_str, nil]
49
+ def id
50
+ if schema_content.respond_to?(:to_hash) && schema_content['id'].respond_to?(:to_str)
51
+ schema_content['id']
52
+ else
53
+ nil
54
+ end
40
55
  end
41
56
  end
42
57
 
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]
52
- end
53
-
54
- # @return [String] an absolute id for the schema, with a json pointer fragment
55
- def schema_id
56
- @schema_id ||= begin
57
- # start from schema_node and ascend parents looking for an 'id' property.
58
- # append a fragment to that id (appending to an existing fragment if there
59
- # is one) consisting of the path from that parent to our schema_node.
60
- node_for_id = schema_node
61
- path_from_id_node = []
62
- done = false
63
-
64
- 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
58
+ # extends any schema which defines an anchor as a URI fragment in the schema id
59
+ module IdWithAnchor
60
+ # a URI for the schema's id, unless the id defines an anchor in its
61
+ # fragment. nil if the schema defines no id.
62
+ # @return [Addressable::URI, nil]
63
+ def id_without_fragment
64
+ if id
65
+ id_uri = Addressable::URI.parse(id)
66
+ if id_uri.merge(fragment: nil).empty?
67
+ # fragment-only id is just an anchor
68
+ # e.g. #foo
69
+ nil
70
+ elsif id_uri.fragment == nil
71
+ # no fragment
72
+ # e.g. http://localhost:1234/bar
73
+ id_uri
74
+ elsif id_uri.fragment == ''
75
+ # empty fragment
76
+ # e.g. http://json-schema.org/draft-07/schema#
77
+ id_uri.merge(fragment: nil)
78
+ elsif jsi_schema_base_uri && jsi_schema_base_uri.join(id_uri).merge(fragment: nil) == jsi_schema_base_uri
79
+ # the id, resolved against the base uri, consists of the base uri plus an anchor fragment.
80
+ # so there's no non-fragment id.
81
+ # e.g. base uri is http://localhost:1234/bar
82
+ # and id is http://localhost:1234/bar#foo
83
+ nil
84
+ else
85
+ # e.g. http://localhost:1234/bar#foo
86
+ id_uri.merge(fragment: nil)
78
87
  end
88
+ else
89
+ nil
90
+ end
91
+ end
79
92
 
80
- if parent_id || node_for_id.node_ptr.root?
81
- done = true
93
+ # an anchor defined by a non-empty fragment in the id uri
94
+ # @return [String]
95
+ def anchor
96
+ if id
97
+ id_uri = Addressable::URI.parse(id)
98
+ if id_uri.fragment == ''
99
+ nil
82
100
  else
83
- path_from_id_node.unshift(node_for_id.node_ptr.reference_tokens.last)
84
- node_for_id = node_for_id.parent_node
101
+ id_uri.fragment
85
102
  end
86
- end
87
- if parent_id
88
- parent_auri = Addressable::URI.parse(parent_id)
89
103
  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
104
+ nil
101
105
  end
102
-
103
- fragment = JSI::JSON::Pointer.new(path_from_id_node).fragment
104
- schema_id = parent_auri.to_s + fragment
105
-
106
- schema_id
107
106
  end
108
107
  end
109
108
 
110
- # @return [Class subclassing JSI::Base] shortcut for JSI.class_for_schema(schema)
111
- def jsi_schema_class
112
- JSI.class_for_schema(self)
109
+ # @private
110
+ module IntegerAllows0Fraction
111
+ # is `value` an integer?
112
+ # @private
113
+ # @param value
114
+ # @return [Boolean]
115
+ def internal_integer?(value)
116
+ value.is_a?(Integer) || (value.is_a?(Numeric) && value % 1.0 == 0.0)
117
+ end
113
118
  end
114
119
 
115
- alias_method :schema_class, :jsi_schema_class
120
+ # @private
121
+ module IntegerDisallows0Fraction
122
+ # is `value` an integer?
123
+ # @private
124
+ # @param value
125
+ # @return [Boolean]
126
+ def internal_integer?(value)
127
+ value.is_a?(Integer)
128
+ end
129
+ end
116
130
 
117
- # calls #new on the class for this schema with the given arguments. for parameters,
118
- # see JSI::Base#initialize documentation.
131
+ # JSI::Schema::DescribesSchema: a schema which describes another schema. this module
132
+ # extends a JSI::Schema instance and indicates that JSIs which instantiate the schema
133
+ # are themselves also schemas.
119
134
  #
120
- # @return [JSI::Base] a JSI whose schema is this schema and whose instance is the given instance
121
- def new_jsi(other_instance, *a, &b)
122
- jsi_schema_class.new(other_instance, *a, &b)
135
+ # examples of a schema which describes a schema include the draft JSON Schema metaschemas and
136
+ # the OpenAPI schema definition which describes "A deterministic version of a JSON Schema object."
137
+ module DescribesSchema
138
+ # instantiates the given schema content as a JSI Schema.
139
+ #
140
+ # the schema is instantiated after recursively converting any symbol hash keys in the structure
141
+ # to strings. note that this is in contrast to {JSI::Schema#new_jsi}, which does not alter its
142
+ # given instance.
143
+ #
144
+ # the schema will be registered with the `JSI.schema_registry`.
145
+ #
146
+ # @param schema_content [#to_hash, Boolean] an object to be instantiated as a schema
147
+ # @param uri [nil, #to_str, Addressable::URI] the URI of the schema document.
148
+ # relative URIs within the document are resolved using this uri as their base.
149
+ # the result schema will be registered with this URI in the {JSI.schema_registry}.
150
+ # @return [JSI::Base, JSI::Schema] a JSI whose instance is the given schema_content and whose schemas
151
+ # are inplace applicators matched from self to the schema being instantiated.
152
+ def new_schema(schema_content,
153
+ uri: nil
154
+ )
155
+ schema_jsi = new_jsi(Util.deep_stringify_symbol_keys(schema_content),
156
+ uri: uri,
157
+ )
158
+ JSI.schema_registry.register(schema_jsi)
159
+ schema_jsi
160
+ end
161
+
162
+ # instantiates a given schema object as a JSI Schema and returns its JSI Schema Module.
163
+ #
164
+ # shortcut to chain {#new_schema} + {Schema#jsi_schema_module}.
165
+ #
166
+ # @param (see #new_schema)
167
+ # @return [Module, JSI::SchemaModule] the JSI Schema Module of the schema
168
+ def new_schema_module(schema_content, **kw)
169
+ new_schema(schema_content, **kw).jsi_schema_module
170
+ end
123
171
  end
124
172
 
125
- # if this schema is a oneOf, allOf, anyOf schema, #match_to_instance finds
126
- # one of the subschemas that matches the given instance and returns it. if
127
- # there are no matching *Of schemas, this schema is returned.
128
- #
129
- # @param other_instance [Object] the instance to which to attempt to match *Of subschemas
130
- # @return [JSI::Schema] a matched subschema, or this schema (self)
131
- 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
173
+ class << self
174
+ # an application-wide default metaschema set by {default_metaschema=}, used by {JSI.new_schema}
175
+ #
176
+ # @return [nil, #new_schema]
177
+ def default_metaschema
178
+ return @default_metaschema if instance_variable_defined?(:@default_metaschema)
179
+ return JSONSchemaOrgDraft07
180
+ end
181
+
182
+ # sets an application-wide default metaschema used by {JSI.new_schema}
183
+ #
184
+ # @param default_metaschema [#new_schema] the default metaschema. this may be a metaschema or a
185
+ # metaschema's schema module (e.g. `JSI::JSONSchemaOrgDraft07`).
186
+ def default_metaschema=(default_metaschema)
187
+ unless default_metaschema.respond_to?(:new_schema)
188
+ raise(TypeError, "given default_metaschema does not respond to #new_schema")
141
189
  end
190
+ @default_metaschema = default_metaschema
142
191
  end
143
- return self
144
- end
145
192
 
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])
154
- 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
193
+ # instantiates a given schema object as a JSI Schema.
194
+ #
195
+ # the metaschema to use to instantiate the schema must be indicated.
196
+ #
197
+ # - if the schema object has a `$schema` property, that URI is resolved using the {JSI.schema_registry},
198
+ # and that metaschema is used.
199
+ # - if no `$schema` property is present, the `default_metaschema` param is used, if the caller
200
+ # specifies it.
201
+ # - if no `default_metaschema` param is specified, the application-wide default
202
+ # {JSI::Schema.default_metaschema JSI::Schema.default_metaschema} is used,
203
+ # if the application has set it.
204
+ #
205
+ # an ArgumentError is raised if none of these indicate a metaschema to use.
206
+ #
207
+ # note that if you are instantiating a schema known to have no `$schema` property, an alternative to
208
+ # passing the `default_metaschema` param is to use `.new_schema` on the metaschema or its module, e.g.
209
+ # `JSI::JSONSchemaOrgDraft07.new_schema(my_schema_object)`
210
+ #
211
+ # if the given schema_object is a JSI::Base but not already a JSI::Schema, an error
212
+ # will be raised. schemas which describe schemas must have JSI::Schema in their
213
+ # Schema#jsi_schema_instance_modules.
214
+ #
215
+ # @param schema_object [#to_hash, Boolean, JSI::Schema] an object to be instantiated as a schema.
216
+ # if it's already a JSI::Schema, it is returned as-is.
217
+ # @param uri (see DescribesSchema#new_schema)
218
+ # @param default_metaschema [#new_schema] the metaschema to use if the schema_object does not have
219
+ # a '$schema' property. this may be a metaschema or a metaschema's schema module
220
+ # (e.g. `JSI::JSONSchemaOrgDraft07`).
221
+ # @return [JSI::Schema] a JSI::Schema representing the given schema_object
222
+ def new_schema(schema_object, default_metaschema: nil, **kw)
223
+ default_metaschema_new_schema = -> {
224
+ default_metaschema ||= JSI::Schema.default_metaschema
225
+ if default_metaschema.nil?
226
+ raise(ArgumentError, [
227
+ "when instantiating a schema with no `$schema` property, you must specify the metaschema.",
228
+ "you may pass the `default_metaschema` param to this method.",
229
+ "JSI::Schema.default_metaschema may be set to an application-wide default metaschema.",
230
+ "you may alternatively use new_schema on the appropriate metaschema or its schema module.",
231
+ "instantiating schema_object: #{schema_object.pretty_inspect.chomp}",
232
+ ].join("\n"))
233
+ end
234
+ if !default_metaschema.respond_to?(:new_schema)
235
+ raise(TypeError, "given default_metaschema does not respond to #new_schema: #{default_metaschema.pretty_inspect.chomp}")
236
+ end
237
+ default_metaschema.new_schema(schema_object, **kw)
238
+ }
239
+ if schema_object.is_a?(Schema)
240
+ schema_object
241
+ elsif schema_object.is_a?(JSI::Base)
242
+ raise(NotASchemaError, "the given schema_object is a JSI::Base, but is not a JSI::Schema: #{schema_object.pretty_inspect.chomp}")
243
+ elsif schema_object.respond_to?(:to_hash)
244
+ if schema_object.key?('$schema') && schema_object['$schema'].respond_to?(:to_str)
245
+ metaschema = Schema::Ref.new(schema_object['$schema']).deref_schema
246
+ unless metaschema.describes_schema?
247
+ raise(Schema::ReferenceError, "given schema_object contains a $schema but the resource it identifies does not describe a schema")
158
248
  end
249
+ metaschema.new_schema(schema_object, **kw)
250
+ else
251
+ default_metaschema_new_schema.call
159
252
  end
160
- if pattern_schema_object
161
- self.class.new(pattern_schema_object)
253
+ elsif [true, false].include?(schema_object)
254
+ default_metaschema_new_schema.call
255
+ else
256
+ raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
257
+ end
258
+ end
259
+
260
+ # @deprecated
261
+ alias_method :new, :new_schema
262
+
263
+ # @deprecated
264
+ alias_method :from_object, :new_schema
265
+
266
+ # ensure the given object is a JSI Schema
267
+ #
268
+ # @param schema [Object] the thing the caller wishes to ensure is a Schema
269
+ # @param msg [#to_s, #to_ary] lines of the error message preceding the pretty-printed schema param
270
+ # if the schema param is not a schema
271
+ # @raise [NotASchemaError] if the schema param is not a schema
272
+ # @return [Schema] the given schema
273
+ def ensure_schema(schema, msg: "indicated object is not a schema:", reinstantiate_as: nil)
274
+ if schema.is_a?(Schema)
275
+ schema
276
+ else
277
+ if reinstantiate_as
278
+ # TODO warn; behavior is undefined and I hate this implementation
279
+
280
+ result_schema_schemas = schema.jsi_schemas + reinstantiate_as
281
+
282
+ result_schema_class = JSI::SchemaClasses.class_for_schemas(result_schema_schemas)
283
+
284
+ result_schema_class.new(Base::NOINSTANCE,
285
+ jsi_document: schema.jsi_document,
286
+ jsi_ptr: schema.jsi_ptr,
287
+ jsi_root_node: schema.jsi_root_node,
288
+ jsi_schema_base_uri: schema.jsi_schema_base_uri,
289
+ jsi_schema_resource_ancestors: schema.jsi_schema_resource_ancestors,
290
+ )
162
291
  else
163
- if schema_object['additionalProperties'].respond_to?(:to_hash)
164
- self.class.new(schema_object['additionalProperties'])
165
- else
166
- nil
167
- end
292
+ raise(NotASchemaError, [
293
+ *msg,
294
+ schema.pretty_inspect.chomp,
295
+ ].join("\n"))
168
296
  end
169
297
  end
170
298
  end
171
299
  end
172
300
 
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'])
301
+ # the underlying JSON data used to instantiate this JSI::Schema.
302
+ # this is an alias for PathedNode#jsi_node_content, named for clarity in the context of working with
303
+ # a schema.
304
+ def schema_content
305
+ jsi_node_content
306
+ end
307
+
308
+ # the URI of this schema, calculated from our `#id`, resolved against our `#jsi_schema_base_uri`
309
+ # @return [Addressable::URI, nil]
310
+ def schema_absolute_uri
311
+ if respond_to?(:id_without_fragment) && id_without_fragment
312
+ if jsi_schema_base_uri
313
+ Addressable::URI.parse(jsi_schema_base_uri).join(id_without_fragment)
314
+ elsif id_without_fragment.absolute?
315
+ id_without_fragment
186
316
  else
317
+ # TODO warn / schema_error
187
318
  nil
188
319
  end
189
320
  end
190
321
  end
191
322
 
192
- # @return [Set] any object property names this schema indicates may be
193
- # present on its instances. this includes, if present: keys of this
194
- # schema's "properties" object; entries of this schema's array of
195
- # "required" property keys. if this schema has oneOf/allOf/anyOf
196
- # subschemas, those schemas are checked (recursively) for their
197
- # described object property names.
198
- def described_object_property_names
199
- memoize(:described_object_property_names) do
200
- Set.new.tap do |property_names|
201
- if schema_node['properties'].respond_to?(:to_hash)
202
- property_names.merge(schema_node['properties'].keys)
203
- end
204
- if schema_node['required'].respond_to?(:to_ary)
205
- property_names.merge(schema_node['required'].to_ary)
206
- end
207
- # we _could_ look at the properties of 'default' and each 'enum' but ... nah.
208
- # we should look at dependencies (TODO).
209
- %w(oneOf allOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |schemas_key|
210
- schema_node[schemas_key].map(&:deref).map do |someof_node|
211
- property_names.merge(self.class.new(someof_node).described_object_property_names)
212
- end
323
+ # a nonrelative URI which refers to this schema.
324
+ # nil if no parent of this schema defines an id.
325
+ # see {#schema_uris} for all URIs known to refer to this schema.
326
+ # @return [Addressable::URI, nil]
327
+ def schema_uri
328
+ schema_uris.first
329
+ end
330
+
331
+ # nonrelative URIs (that is, absolute, but possibly with a fragment) which refer to this schema
332
+ # @return [Array<Addressable::URI>]
333
+ def schema_uris
334
+ jsi_memoize(:schema_uris) do
335
+ each_schema_uri.to_a
336
+ end
337
+ end
338
+
339
+ # see {#schema_uris}
340
+ # @yield [Addressable::URI]
341
+ # @return [Enumerator, nil]
342
+ def each_schema_uri
343
+ return to_enum(__method__) unless block_given?
344
+
345
+ yield schema_absolute_uri if schema_absolute_uri
346
+
347
+ parent_schemas = jsi_subschema_resource_ancestors.reverse_each.select do |resource|
348
+ resource.is_a?(Schema) && resource.schema_absolute_uri
349
+ end
350
+
351
+ anchored = self.anchor
352
+ parent_schemas.each do |parent_schema|
353
+ if anchored
354
+ if parent_schema.jsi_anchor_subschema(anchor) == self
355
+ yield parent_schema.schema_absolute_uri.merge(fragment: anchor)
356
+ else
357
+ anchored = false
213
358
  end
214
359
  end
360
+
361
+ relative_ptr = self.jsi_ptr.ptr_relative_to(parent_schema.jsi_ptr)
362
+ yield parent_schema.schema_absolute_uri.merge(fragment: relative_ptr.fragment)
215
363
  end
364
+
365
+ nil
366
+ end
367
+
368
+ # a module which extends all instances of this schema. this may be opened by the application to add
369
+ # methods to schema instances.
370
+ #
371
+ # this module includes accessor methods for object property names this schema
372
+ # describes (see {#described_object_property_names}). these accessors wrap {Base#[]} and {Base#[]=}.
373
+ #
374
+ # some functionality is also defined on the module itself (its singleton class, not for its instances):
375
+ #
376
+ # - the module is extended with {JSI::SchemaModule}, which defines .new_jsi to instantiate instances
377
+ # of this schema (see {#new_jsi}).
378
+ # - properties described by this schema's metaschema are defined as methods to get subschemas' schema
379
+ # modules, so for example `schema.jsi_schema_module.items` returns the same module
380
+ # as `schema.items.jsi_schema_module`.
381
+ # - method .schema which returns this schema.
382
+ #
383
+ # @return [Module]
384
+ def jsi_schema_module
385
+ JSI::SchemaClasses.module_for_schema(self)
386
+ end
387
+
388
+ # Evaluates the given block in the context of this schema's JSI schema module.
389
+ # Any arguments passed to this method will be passed to the block.
390
+ # shortcut to invoke [Module#module_exec](https://ruby-doc.org/core/Module.html#method-i-module_exec)
391
+ # on our {#jsi_schema_module}.
392
+ #
393
+ # @return the result of evaluating the block
394
+ def jsi_schema_module_exec(*a, **kw, &block)
395
+ jsi_schema_module.module_exec(*a, **kw, &block)
396
+ end
397
+
398
+ # @private @deprecated
399
+ def jsi_schema_class
400
+ JSI::SchemaClasses.class_for_schemas(SchemaSet[self])
401
+ end
402
+
403
+ # instantiates the given instance as a JSI::Base class for schemas matched from this schema to the
404
+ # instance.
405
+ #
406
+ # @param instance [Object] the JSON Schema instance to be represented as a JSI
407
+ # @param uri (see SchemaSet#new_jsi)
408
+ # @return [JSI::Base subclass] a JSI whose instance is the given instance and whose schemas are matched
409
+ # from this schema.
410
+ def new_jsi(instance,
411
+ **kw
412
+ )
413
+ SchemaSet[self].new_jsi(instance, **kw)
414
+ end
415
+
416
+ # does this schema itself describe a schema?
417
+ # @return [Boolean]
418
+ def describes_schema?
419
+ jsi_schema_instance_modules.any? { |m| m <= JSI::Schema }
420
+ end
421
+
422
+ # modules to apply to instances described by this schema. these modules are included
423
+ # on this schema's {#jsi_schema_module}
424
+ # @return [Set<Module>]
425
+ def jsi_schema_instance_modules
426
+ return @jsi_schema_instance_modules if instance_variable_defined?(:@jsi_schema_instance_modules)
427
+ return Set[].freeze
428
+ end
429
+
430
+ # see {#jsi_schema_instance_modules}
431
+ #
432
+ # @return [void]
433
+ def jsi_schema_instance_modules=(jsi_schema_instance_modules)
434
+ @jsi_schema_instance_modules = Util.ensure_module_set(jsi_schema_instance_modules)
435
+ end
436
+
437
+ # a resource containing this schema.
438
+ #
439
+ # if any parent, or this schema itself, is a schema with an absolute uri (see #schema_absolute_uri),
440
+ # the resource root is the closest schema with an absolute uri.
441
+ #
442
+ # if no parent schema has an absolute uri, the schema_resource_root is the root of the document
443
+ # (our #jsi_root_node). in this case, the resource root may or may not be a schema itself.
444
+ #
445
+ # @return [JSI::Base] resource containing this schema
446
+ def schema_resource_root
447
+ jsi_subschema_resource_ancestors.reverse_each.detect(&:schema_resource_root?) || jsi_root_node
216
448
  end
217
449
 
218
- def default_value
219
- if schema_node.key?('default')
220
- if schema_node['default'].respond_to?(:to_ary) || schema_node['default'].respond_to?(:to_hash)
221
- schema_class.new(schema_node['default'])
450
+ # is this schema the root of a schema resource?
451
+ # @return [Boolean]
452
+ def schema_resource_root?
453
+ jsi_ptr.root? || !!schema_absolute_uri
454
+ end
455
+
456
+ # a subschema of this Schema
457
+ #
458
+ # @param subptr [JSI::Ptr, #to_ary] a relative pointer, or array of tokens, pointing to the subschema
459
+ # @return [JSI::Schema] the subschema at the location indicated by subptr. self if subptr is empty.
460
+ def subschema(subptr)
461
+ subschema_map[Ptr.ary_ptr(subptr)]
462
+ end
463
+
464
+ private
465
+
466
+ def subschema_map
467
+ jsi_memomap(:subschema) do |subptr|
468
+ if is_a?(MetaschemaNode::BootstrapSchema)
469
+ self.class.new(
470
+ jsi_document,
471
+ jsi_ptr: jsi_ptr + subptr,
472
+ jsi_schema_base_uri: jsi_resource_ancestor_uri,
473
+ )
222
474
  else
223
- schema_node['default']
475
+ Schema.ensure_schema(subptr.evaluate(self, as_jsi: true), msg: [
476
+ "subschema is not a schema at pointer: #{subptr.pointer}"
477
+ ])
224
478
  end
225
- else
226
- nil
227
479
  end
228
480
  end
229
481
 
230
- def default_value?
231
- schema_node.key?('default')
482
+ public
483
+
484
+ # a schema in the same schema resource as this one (see #schema_resource_root) at the given
485
+ # pointer relative to the root of the schema resource.
486
+ #
487
+ # @param ptr [JSI::Ptr, #to_ary] a pointer to a schema from our schema resource root
488
+ # @return [JSI::Schema] the schema pointed to by ptr
489
+ def resource_root_subschema(ptr)
490
+ resource_root_subschema_map[Ptr.ary_ptr(ptr)]
232
491
  end
233
492
 
234
- # @return [Array<String>] array of schema validation error messages for
235
- # the given instance against this schema
236
- def fully_validate_instance(other_instance)
237
- ::JSON::Validator.fully_validate(JSI::Typelike.as_json(schema_node.node_document), JSI::Typelike.as_json(other_instance), fragment: schema_node.node_ptr.fragment)
493
+ private
494
+
495
+ def resource_root_subschema_map
496
+ jsi_memomap(:resource_root_subschema_map) do |ptr|
497
+ schema = self
498
+ if schema.is_a?(MetaschemaNode::BootstrapSchema)
499
+ # BootstrapSchema does not track jsi_schema_resource_ancestors used by #schema_resource_root;
500
+ # resource_root_subschema is always relative to the document root.
501
+ # BootstrapSchema also does not implement jsi_root_node or #[]. we instantiate the ptr directly
502
+ # rather than as a subschema from the root.
503
+ schema.class.new(
504
+ schema.jsi_document,
505
+ jsi_ptr: ptr,
506
+ jsi_schema_base_uri: nil,
507
+ )
508
+ else
509
+ resource_root = schema.schema_resource_root
510
+ Schema.ensure_schema(ptr.evaluate(resource_root, as_jsi: true),
511
+ msg: [
512
+ "subschema is not a schema at pointer: #{ptr.pointer}"
513
+ ],
514
+ reinstantiate_as: schema.jsi_schemas.select(&:describes_schema?)
515
+ )
516
+ end
517
+ end
238
518
  end
239
519
 
240
- # @return [true, false] whether the given instance validates against this schema
241
- def validate_instance(other_instance)
242
- ::JSON::Validator.validate(JSI::Typelike.as_json(schema_node.node_document), JSI::Typelike.as_json(other_instance), fragment: schema_node.node_ptr.fragment)
520
+ public
521
+
522
+ # any object property names this schema indicates may be present on its instances.
523
+ # this includes any keys of this schema's "properties" object and any entries of this schema's
524
+ # array of "required" property keys.
525
+ # @return [Set]
526
+ def described_object_property_names
527
+ jsi_memoize(:described_object_property_names) do
528
+ Set.new.tap do |property_names|
529
+ if schema_content.respond_to?(:to_hash) && schema_content['properties'].respond_to?(:to_hash)
530
+ property_names.merge(schema_content['properties'].keys)
531
+ end
532
+ if schema_content.respond_to?(:to_hash) && schema_content['required'].respond_to?(:to_ary)
533
+ property_names.merge(schema_content['required'].to_ary)
534
+ end
535
+ end.freeze
536
+ end
243
537
  end
244
538
 
245
- # @return [true] if this method does not raise, it returns true to
246
- # indicate the instance is valid against this schema
247
- # @raise [::JSON::Schema::ValidationError] raises if the instance has
248
- # validation errors against this schema
249
- def validate_instance!(other_instance)
250
- ::JSON::Validator.validate!(JSI::Typelike.as_json(schema_node.node_document), JSI::Typelike.as_json(other_instance), fragment: schema_node.node_ptr.fragment)
539
+ # validates the given instance against this schema
540
+ #
541
+ # @param instance [Object] the instance to validate against this schema
542
+ # @return [JSI::Validation::Result]
543
+ def instance_validate(instance)
544
+ if instance.is_a?(JSI::PathedNode)
545
+ instance_ptr = instance.jsi_ptr
546
+ instance_document = instance.jsi_document
547
+ else
548
+ instance_ptr = Ptr[]
549
+ instance_document = instance
550
+ end
551
+ internal_validate_instance(instance_ptr, instance_document)
251
552
  end
252
553
 
253
- # @return [Array<String>] array of schema validation error messages for
254
- # this schema, validated against its metaschema. a default metaschema
255
- # is assumed if the schema does not specify a $schema.
256
- def fully_validate_schema
257
- ::JSON::Validator.fully_validate(JSI::Typelike.as_json(schema_node.node_document), [], fragment: schema_node.node_ptr.fragment, validate_schema: true, list: true)
554
+ # whether the given instance is valid against this schema
555
+ # @param instance [Object] the instance to validate against this schema
556
+ # @return [Boolean]
557
+ def instance_valid?(instance)
558
+ if instance.is_a?(JSI::PathedNode)
559
+ instance = instance.jsi_node_content
560
+ end
561
+ internal_validate_instance(Ptr[], instance, validate_only: true).valid?
258
562
  end
259
563
 
260
- # @return [true, false] whether this schema validates against its metaschema
261
- def validate_schema
262
- ::JSON::Validator.validate(JSI::Typelike.as_json(schema_node.node_document), [], fragment: schema_node.node_ptr.fragment, validate_schema: true, list: true)
564
+ # @private
565
+ def fully_validate_instance(other_instance, errors_as_objects: false)
566
+ raise(NotImplementedError, "Schema#fully_validate_instance removed: see new validation interface Schema#instance_validate")
263
567
  end
264
568
 
265
- # @return [true] if this method does not raise, it returns true to
266
- # indicate this schema is valid against its metaschema
267
- # @raise [::JSON::Schema::ValidationError] raises if this schema has
268
- # validation errors against its metaschema
269
- def validate_schema!
270
- ::JSON::Validator.validate!(JSI::Typelike.as_json(schema_node.node_document), [], fragment: schema_node.node_ptr.fragment, validate_schema: true, list: true)
569
+ # @private
570
+ def validate_instance(other_instance)
571
+ raise(NotImplementedError, "Schema#validate_instance renamed: see Schema#instance_valid?")
271
572
  end
272
573
 
273
- # @return [String] a string for #instance and #pretty_print including the schema_id
274
- def object_group_text
275
- "schema_id=#{schema_id}"
574
+ # @private
575
+ def validate_instance!(other_instance)
576
+ raise(NotImplementedError, "Schema#validate_instance! removed")
276
577
  end
277
578
 
278
- # @return [String] a string representing this Schema
279
- def inspect
280
- "\#<#{self.class.inspect} #{object_group_text} #{schema_object.inspect}>"
579
+ # @private
580
+ def fully_validate_schema(errors_as_objects: false)
581
+ raise(NotImplementedError, "Schema#fully_validate_schema removed: use validation interface Base#jsi_validate on the schema")
281
582
  end
282
- alias_method :to_s, :inspect
283
583
 
284
- # pretty-prints a representation this Schema to the given printer
285
- # @return [void]
286
- def pretty_print(q)
287
- q.instance_exec(self) do |obj|
288
- text "\#<#{obj.class.inspect} #{obj.object_group_text}"
289
- group_sub {
290
- nest(2) {
291
- breakable ' '
292
- pp obj.schema_object
293
- }
294
- }
295
- breakable ''
296
- text '>'
297
- end
584
+ # @private
585
+ def validate_schema
586
+ raise(NotImplementedError, "Schema#validate_schema removed: use validation interface Base#jsi_valid? on the schema")
298
587
  end
299
588
 
300
- # @return [Object] returns a jsonifiable representation of this schema
301
- def as_json(*opt)
302
- Typelike.as_json(schema_object, *opt)
589
+ # @private
590
+ def validate_schema!
591
+ raise(NotImplementedError, "Schema#validate_schema! removed")
303
592
  end
304
593
 
305
- # @return [Object] an opaque fingerprint of this Schema for FingerprintHash
306
- def fingerprint
307
- {class: self.class, schema_ptr: schema_node.node_ptr, schema_document: JSI::Typelike.as_json(schema_node.node_document)}
594
+ # schema resources which are ancestors of any subschemas below this schema.
595
+ # this may include this schema if this is a schema resource root.
596
+ # @private
597
+ # @return [Array<JSI::Schema>]
598
+ def jsi_subschema_resource_ancestors
599
+ if schema_resource_root?
600
+ jsi_schema_resource_ancestors + [self]
601
+ else
602
+ jsi_schema_resource_ancestors
603
+ end
308
604
  end
309
- include FingerprintHash
310
605
  end
311
606
  end