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