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_classes.rb
CHANGED
|
@@ -3,41 +3,94 @@
|
|
|
3
3
|
module JSI
|
|
4
4
|
# JSI Schema Modules are extended with JSI::SchemaModule
|
|
5
5
|
module SchemaModule
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
# @!method schema
|
|
7
|
+
# the schema of which this is the JSI Schema Module
|
|
8
|
+
# @return [Schema]
|
|
9
|
+
# note: defined on JSI Schema Module by JSI::SchemaClasses.module_for_schema
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# a URI which refers to the schema. see {Schema#schema_uri}.
|
|
13
|
+
# @return (see Schema#schema_uri)
|
|
14
|
+
def schema_uri
|
|
15
|
+
schema.schema_uri
|
|
10
16
|
end
|
|
11
17
|
|
|
12
18
|
# @return [String]
|
|
13
19
|
def inspect
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"#{name} (#{uri})"
|
|
20
|
+
if name_from_ancestor
|
|
21
|
+
"#{name_from_ancestor} (JSI Schema Module)"
|
|
17
22
|
else
|
|
18
|
-
"(JSI Schema Module: #{uri})"
|
|
23
|
+
"(JSI Schema Module: #{schema.schema_uri || schema.jsi_ptr.uri})"
|
|
19
24
|
end
|
|
20
25
|
end
|
|
21
26
|
|
|
27
|
+
alias_method :to_s, :inspect
|
|
28
|
+
|
|
22
29
|
# invokes {JSI::Schema#new_jsi} on this module's schema, passing the given instance.
|
|
30
|
+
#
|
|
31
|
+
# @param (see JSI::Schema#new_jsi)
|
|
23
32
|
# @return [JSI::Base] a JSI whose instance is the given instance
|
|
24
|
-
def new_jsi(instance,
|
|
25
|
-
schema.new_jsi(instance,
|
|
33
|
+
def new_jsi(instance, **kw)
|
|
34
|
+
schema.new_jsi(instance, **kw)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# a module to extend the JSI Schema Module of a schema which describes other schemas
|
|
39
|
+
module DescribesSchemaModule
|
|
40
|
+
# instantiates the given schema content as a JSI Schema.
|
|
41
|
+
#
|
|
42
|
+
# see {JSI::Schema::DescribesSchema#new_schema}
|
|
43
|
+
#
|
|
44
|
+
# @param (see JSI::Schema::DescribesSchema#new_schema)
|
|
45
|
+
# @return [JSI::Base, JSI::Schema] a JSI whose instance is the given schema_content and whose schemas
|
|
46
|
+
# consist of this module's schema.
|
|
47
|
+
def new_schema(schema_content, **kw)
|
|
48
|
+
schema.new_schema(schema_content, **kw)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# instantiates a given schema object as a JSI Schema and returns its JSI Schema Module.
|
|
52
|
+
#
|
|
53
|
+
# shortcut to chain {JSI::Schema::DescribesSchema#new_schema} + {Schema#jsi_schema_module}.
|
|
54
|
+
#
|
|
55
|
+
# @param (see JSI::Schema::DescribesSchema#new_schema)
|
|
56
|
+
# @return [Module, JSI::SchemaModule] the JSI Schema Module of the schema
|
|
57
|
+
def new_schema_module(schema_content, **kw)
|
|
58
|
+
schema.new_schema(schema_content, **kw).jsi_schema_module
|
|
26
59
|
end
|
|
60
|
+
|
|
61
|
+
# @return [Set<Module>]
|
|
62
|
+
attr_reader :schema_implementation_modules
|
|
27
63
|
end
|
|
28
64
|
|
|
29
|
-
# this module is
|
|
65
|
+
# this module is a namespace for building schema classes and schema modules.
|
|
30
66
|
module SchemaClasses
|
|
67
|
+
extend Util::Memoize
|
|
68
|
+
|
|
31
69
|
class << self
|
|
32
|
-
|
|
70
|
+
# @api private
|
|
71
|
+
# @return [Set<Module>]
|
|
72
|
+
def includes_for(instance)
|
|
73
|
+
includes = Set[]
|
|
74
|
+
includes << Base::ArrayNode if instance.respond_to?(:to_ary)
|
|
75
|
+
includes << Base::HashNode if instance.respond_to?(:to_hash)
|
|
76
|
+
includes.freeze
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# a JSI Schema Class which represents the given schemas.
|
|
80
|
+
# an instance of the class is a JSON Schema instance described by all of the given schemas.
|
|
81
|
+
# @api private
|
|
82
|
+
# @param schemas [Enumerable<JSI::Schema>] schemas which the class will represent
|
|
83
|
+
# @param includes [Enumerable<Module>] modules which will be included on the class
|
|
84
|
+
# @return [Class subclassing JSI::Base]
|
|
85
|
+
def class_for_schemas(schemas, includes: )
|
|
86
|
+
schemas = SchemaSet.ensure_schema_set(schemas)
|
|
87
|
+
includes = Util.ensure_module_set(includes)
|
|
33
88
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
schemas = schema_objects.map { |schema_object| JSI::Schema.from_object(schema_object) }.to_set
|
|
37
|
-
jsi_memoize(:class_for_schemas, schemas) do |schemas|
|
|
38
|
-
Class.new(Base).instance_exec(schemas) do |schemas|
|
|
89
|
+
jsi_memoize(:class_for_schemas, schemas: schemas, includes: includes) do |schemas: , includes: |
|
|
90
|
+
Class.new(Base).instance_exec(schemas: schemas, includes: includes) do |schemas: , includes: |
|
|
39
91
|
define_singleton_method(:jsi_class_schemas) { schemas }
|
|
40
92
|
define_method(:jsi_schemas) { schemas }
|
|
93
|
+
includes.each { |m| include(m) }
|
|
41
94
|
schemas.each { |schema| include(schema.jsi_schema_module) }
|
|
42
95
|
jsi_class = self
|
|
43
96
|
define_method(:jsi_class) { jsi_class }
|
|
@@ -47,55 +100,96 @@ module JSI
|
|
|
47
100
|
end
|
|
48
101
|
end
|
|
49
102
|
|
|
50
|
-
# a
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
103
|
+
# a subclass of MetaschemaNode::BootstrapSchema with the given modules included
|
|
104
|
+
# @api private
|
|
105
|
+
# @param modules [Set<Module>] schema implementation modules
|
|
106
|
+
# @return [Class]
|
|
107
|
+
def bootstrap_schema_class(modules)
|
|
108
|
+
modules = Util.ensure_module_set(modules)
|
|
109
|
+
jsi_memoize(__method__, modules: modules) do |modules: |
|
|
110
|
+
Class.new(MetaschemaNode::BootstrapSchema) do
|
|
111
|
+
define_singleton_method(:schema_implementation_modules) { modules }
|
|
112
|
+
define_method(:schema_implementation_modules) { modules }
|
|
113
|
+
modules.each { |mod| include(mod) }
|
|
114
|
+
|
|
115
|
+
self
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# see {Schema#jsi_schema_module}
|
|
121
|
+
# @api private
|
|
122
|
+
# @return [Module]
|
|
123
|
+
def module_for_schema(schema)
|
|
124
|
+
Schema.ensure_schema(schema)
|
|
125
|
+
jsi_memoize(:module_for_schema, schema: schema) do |schema: |
|
|
126
|
+
Module.new do
|
|
127
|
+
begin
|
|
60
128
|
define_singleton_method(:schema) { schema }
|
|
61
129
|
|
|
62
130
|
extend SchemaModule
|
|
63
131
|
|
|
64
|
-
|
|
132
|
+
schema.jsi_schema_instance_modules.each do |mod|
|
|
133
|
+
include(mod)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
accessor_module = JSI::SchemaClasses.accessor_module_for_schema(schema,
|
|
137
|
+
conflicting_modules: Set[JSI::Base, JSI::Base::ArrayNode, JSI::Base::HashNode] +
|
|
138
|
+
schema.jsi_schema_instance_modules,
|
|
139
|
+
)
|
|
140
|
+
include accessor_module
|
|
141
|
+
|
|
142
|
+
define_singleton_method(:jsi_property_accessors) { accessor_module.jsi_property_accessors }
|
|
65
143
|
|
|
66
144
|
@possibly_schema_node = schema
|
|
67
145
|
extend(SchemaModulePossibly)
|
|
68
146
|
schema.jsi_schemas.each do |schema_schema|
|
|
69
|
-
extend
|
|
147
|
+
extend JSI::SchemaClasses.accessor_module_for_schema(schema_schema,
|
|
148
|
+
conflicting_modules: Set[Module, SchemaModule, SchemaModulePossibly],
|
|
149
|
+
setters: false,
|
|
150
|
+
)
|
|
70
151
|
end
|
|
71
152
|
end
|
|
72
153
|
end
|
|
73
154
|
end
|
|
74
155
|
end
|
|
75
156
|
|
|
157
|
+
# a module of accessors for described property names of the given schema.
|
|
158
|
+
# getters are always defined. setters are defined by default.
|
|
159
|
+
#
|
|
160
|
+
# @api private
|
|
76
161
|
# @param schema [JSI::Schema] a schema for which to define accessors for any described property names
|
|
77
162
|
# @param conflicting_modules [Enumerable<Module>] an array of modules (or classes) which
|
|
78
163
|
# may be used alongside the accessor module. methods defined by any conflicting_module
|
|
79
164
|
# will not be defined as accessors.
|
|
80
|
-
# @
|
|
81
|
-
#
|
|
82
|
-
def accessor_module_for_schema(schema, conflicting_modules: )
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
conflicting_instance_methods =
|
|
165
|
+
# @param setters [Boolean] whether to define setter methods
|
|
166
|
+
# @return [Module]
|
|
167
|
+
def accessor_module_for_schema(schema, conflicting_modules: , setters: true)
|
|
168
|
+
Schema.ensure_schema(schema)
|
|
169
|
+
jsi_memoize(:accessor_module_for_schema, schema: schema, conflicting_modules: conflicting_modules, setters: setters) do |schema: , conflicting_modules: , setters: |
|
|
170
|
+
Module.new do
|
|
171
|
+
begin
|
|
172
|
+
define_singleton_method(:inspect) { '(JSI Schema Accessor Module)' }
|
|
173
|
+
|
|
174
|
+
conflicting_instance_methods = conflicting_modules.map do |mod|
|
|
90
175
|
mod.instance_methods + mod.private_instance_methods
|
|
91
176
|
end.inject(Set.new, &:|)
|
|
92
|
-
|
|
177
|
+
|
|
178
|
+
accessors_to_define = schema.described_object_property_names.select do |name|
|
|
179
|
+
# must not conflict with any method on a conflicting module
|
|
180
|
+
Util.ok_ruby_method_name?(name) && !conflicting_instance_methods.any? { |mn| mn.to_s == name }
|
|
181
|
+
end.to_set.freeze
|
|
182
|
+
|
|
183
|
+
define_singleton_method(:jsi_property_accessors) { accessors_to_define }
|
|
184
|
+
|
|
93
185
|
accessors_to_define.each do |property_name|
|
|
94
|
-
define_method(property_name) do
|
|
95
|
-
self[property_name]
|
|
186
|
+
define_method(property_name) do |**kw|
|
|
187
|
+
self[property_name, **kw]
|
|
96
188
|
end
|
|
97
|
-
|
|
98
|
-
|
|
189
|
+
if setters
|
|
190
|
+
define_method("#{property_name}=") do |value|
|
|
191
|
+
self[property_name] = value
|
|
192
|
+
end
|
|
99
193
|
end
|
|
100
194
|
end
|
|
101
195
|
end
|
|
@@ -105,32 +199,71 @@ module JSI
|
|
|
105
199
|
end
|
|
106
200
|
end
|
|
107
201
|
|
|
108
|
-
# a JSI
|
|
202
|
+
# a JSI Schema module and a JSI::NotASchemaModule are both a SchemaModulePossibly.
|
|
109
203
|
# this module provides a #[] method.
|
|
110
204
|
module SchemaModulePossibly
|
|
111
205
|
attr_reader :possibly_schema_node
|
|
112
206
|
|
|
207
|
+
# a name relative to a named schema module of an ancestor schema.
|
|
208
|
+
# for example, if `Foos = JSI::JSONSchemaOrgDraft07.new_schema_module({'items' => {}})`
|
|
209
|
+
# then the module `Foos.items` will have a name_from_ancestor of `"Foos.items"`
|
|
210
|
+
# @api private
|
|
211
|
+
# @return [String, nil]
|
|
212
|
+
def name_from_ancestor
|
|
213
|
+
named_ancestor_schema, tokens = named_ancestor_schema_tokens
|
|
214
|
+
return nil unless named_ancestor_schema
|
|
215
|
+
|
|
216
|
+
name = named_ancestor_schema.jsi_schema_module.name
|
|
217
|
+
ancestor = named_ancestor_schema
|
|
218
|
+
tokens.each do |token|
|
|
219
|
+
if ancestor.jsi_schemas.any? { |s| s.jsi_schema_module.jsi_property_accessors.include?(token) }
|
|
220
|
+
name += ".#{token}"
|
|
221
|
+
elsif [String, Numeric, TrueClass, FalseClass, NilClass].any? { |m| token.is_a?(m) }
|
|
222
|
+
name += "[#{token.inspect}]"
|
|
223
|
+
else
|
|
224
|
+
return nil
|
|
225
|
+
end
|
|
226
|
+
ancestor = ancestor[token]
|
|
227
|
+
end
|
|
228
|
+
name
|
|
229
|
+
end
|
|
230
|
+
|
|
113
231
|
# subscripting a JSI schema module or a NotASchemaModule will subscript the node, and
|
|
114
|
-
# if the result is a JSI::Schema, return
|
|
232
|
+
# if the result is a JSI::Schema, return the JSI Schema module of that schema; if it is a JSI::Base,
|
|
115
233
|
# return a NotASchemaModule; or if it is another value (a basic type), return that value.
|
|
116
234
|
#
|
|
117
235
|
# @param token [Object]
|
|
118
|
-
# @return [
|
|
119
|
-
def [](token)
|
|
236
|
+
# @return [Module, NotASchemaModule, Object]
|
|
237
|
+
def [](token, **kw)
|
|
238
|
+
raise(ArgumentError) unless kw.empty? # TODO remove eventually (keyword argument compatibility)
|
|
120
239
|
sub = @possibly_schema_node[token]
|
|
121
240
|
if sub.is_a?(JSI::Schema)
|
|
122
241
|
sub.jsi_schema_module
|
|
123
|
-
elsif sub.is_a?(JSI::
|
|
242
|
+
elsif sub.is_a?(JSI::Base)
|
|
124
243
|
NotASchemaModule.new(sub)
|
|
125
244
|
else
|
|
126
245
|
sub
|
|
127
246
|
end
|
|
128
247
|
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
# @return [Array<JSI::Schema, Array>, nil]
|
|
252
|
+
def named_ancestor_schema_tokens
|
|
253
|
+
schema_ancestors = possibly_schema_node.jsi_ancestor_nodes
|
|
254
|
+
named_ancestor_schema = schema_ancestors.detect { |jsi| jsi.is_a?(JSI::Schema) && jsi.jsi_schema_module.name }
|
|
255
|
+
return nil unless named_ancestor_schema
|
|
256
|
+
tokens = possibly_schema_node.jsi_ptr.relative_to(named_ancestor_schema.jsi_ptr).tokens
|
|
257
|
+
[named_ancestor_schema, tokens]
|
|
258
|
+
end
|
|
129
259
|
end
|
|
130
260
|
|
|
131
|
-
# a
|
|
261
|
+
# a JSI Schema Module is a module which represents a schema. a NotASchemaModule represents
|
|
132
262
|
# a node in a schema's document which is not a schema, such as the 'properties'
|
|
133
|
-
#
|
|
263
|
+
# object (which contains schemas but is not a schema).
|
|
264
|
+
#
|
|
265
|
+
# instances of this class act as a stand-in to allow users to subscript or call property accessors on
|
|
266
|
+
# schema modules to refer to their subschemas' schema modules.
|
|
134
267
|
#
|
|
135
268
|
# a NotASchemaModule is extended with the module_for_schema of the node's schema.
|
|
136
269
|
#
|
|
@@ -139,20 +272,27 @@ module JSI
|
|
|
139
272
|
# is another node, a NotASchemaModule for that node is returned. otherwise - when the
|
|
140
273
|
# value is a basic type - that value itself is returned.
|
|
141
274
|
class NotASchemaModule
|
|
142
|
-
# @param node [JSI::
|
|
275
|
+
# @param node [JSI::Base]
|
|
143
276
|
def initialize(node)
|
|
144
|
-
unless node.is_a?(JSI::
|
|
145
|
-
|
|
146
|
-
end
|
|
147
|
-
if node.is_a?(JSI::Schema)
|
|
148
|
-
raise(TypeError, "cannot instantiate NotASchemaModule for a JSI::Schema node: #{node.pretty_inspect.chomp}")
|
|
149
|
-
end
|
|
277
|
+
raise(Bug, "node must be JSI::Base: #{node.pretty_inspect.chomp}") unless node.is_a?(JSI::Base)
|
|
278
|
+
raise(Bug, "node must not be JSI::Schema: #{node.pretty_inspect.chomp}") if node.is_a?(JSI::Schema)
|
|
150
279
|
@possibly_schema_node = node
|
|
151
280
|
node.jsi_schemas.each do |schema|
|
|
152
|
-
extend(JSI::SchemaClasses.accessor_module_for_schema(schema, conflicting_modules: [NotASchemaModule, SchemaModulePossibly]))
|
|
281
|
+
extend(JSI::SchemaClasses.accessor_module_for_schema(schema, conflicting_modules: [NotASchemaModule, SchemaModulePossibly], setters: false))
|
|
153
282
|
end
|
|
154
283
|
end
|
|
155
284
|
|
|
156
285
|
include SchemaModulePossibly
|
|
286
|
+
|
|
287
|
+
# @return [String]
|
|
288
|
+
def inspect
|
|
289
|
+
if name_from_ancestor
|
|
290
|
+
"#{name_from_ancestor} (JSI wrapper for Schema Module)"
|
|
291
|
+
else
|
|
292
|
+
"(JSI wrapper for Schema Module: #{@possibly_schema_node.jsi_ptr.uri})"
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
alias_method :to_s, :inspect
|
|
157
297
|
end
|
|
158
298
|
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
class SchemaRegistry
|
|
5
|
+
# an exception raised when an attempt is made to register a resource using a URI which is already
|
|
6
|
+
# registered with another resource
|
|
7
|
+
class Collision < StandardError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# an exception raised when an attempt is made to access (register or find) a resource of the
|
|
11
|
+
# registry using a URI which is not absolute (it is a relative URI or it contains a fragment)
|
|
12
|
+
class NonAbsoluteURI < StandardError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# an exception raised when a URI we are looking for has not been registered
|
|
16
|
+
class ResourceNotFound < StandardError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@resources = {}
|
|
21
|
+
@autoload_uris = {}
|
|
22
|
+
@resources_mutex = Mutex.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# registers the given resource and/or schema resources it contains in the registry.
|
|
26
|
+
#
|
|
27
|
+
# each descendent node of the resource (including the resource itself) is registered if it is a schema
|
|
28
|
+
# that has an absolute URI (generally defined by the '$id' keyword).
|
|
29
|
+
#
|
|
30
|
+
# the given resource itself will be registered, whether or not it is a schema, if it is the root
|
|
31
|
+
# of its document and was instantiated with the option `uri` specified.
|
|
32
|
+
#
|
|
33
|
+
# @param resource [JSI::Base] a JSI containing resources to register
|
|
34
|
+
# @return [void]
|
|
35
|
+
def register(resource)
|
|
36
|
+
unless resource.is_a?(JSI::Base)
|
|
37
|
+
raise(ArgumentError, "resource must be a JSI::Base. got: #{resource.pretty_inspect.chomp}")
|
|
38
|
+
end
|
|
39
|
+
unless resource.is_a?(JSI::Schema) || resource.jsi_ptr.root?
|
|
40
|
+
# unsure, should this be allowed? the given JSI is not a "resource" as we define it, but
|
|
41
|
+
# if this check is removed it will just register any resources (schemas) below the given JSI.
|
|
42
|
+
raise(ArgumentError, "undefined behavior: registration of a JSI which is not a schema and is not at the root of a document")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# allow for registration of resources at the root of a document whether or not they are schemas.
|
|
46
|
+
# jsi_schema_base_uri at the root comes from the `uri` parameter to new_jsi / new_schema.
|
|
47
|
+
if resource.jsi_schema_base_uri && resource.jsi_ptr.root?
|
|
48
|
+
register_single(resource.jsi_schema_base_uri, resource)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
resource.jsi_each_descendent_node do |node|
|
|
52
|
+
if node.is_a?(JSI::Schema) && node.schema_absolute_uri
|
|
53
|
+
register_single(node.schema_absolute_uri, node)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# takes a URI identifying a resource to be loaded by the given block
|
|
61
|
+
# when a reference to the URI is followed.
|
|
62
|
+
#
|
|
63
|
+
# for example:
|
|
64
|
+
#
|
|
65
|
+
# JSI.schema_registry.autoload_uri('http://example.com/schema.json') do
|
|
66
|
+
# JSI.new_schema({
|
|
67
|
+
# '$schema' => 'http://json-schema.org/draft-07/schema#',
|
|
68
|
+
# '$id' => 'http://example.com/schema.json',
|
|
69
|
+
# 'title' => 'my schema',
|
|
70
|
+
# })
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# the block would normally load JSON from the filesystem or similar.
|
|
74
|
+
#
|
|
75
|
+
# @param uri [Addressable::URI]
|
|
76
|
+
# @yieldreturn [JSI::Base] a JSI instance containing the resource identified by the given uri
|
|
77
|
+
# @return [void]
|
|
78
|
+
def autoload_uri(uri, &block)
|
|
79
|
+
uri = Addressable::URI.parse(uri)
|
|
80
|
+
ensure_uri_absolute(uri)
|
|
81
|
+
@autoload_uris[uri] = block
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @param uri [Addressable::URI, #to_str]
|
|
86
|
+
# @return [JSI::Base]
|
|
87
|
+
# @raise [JSI::SchemaRegistry::ResourceNotFound]
|
|
88
|
+
def find(uri)
|
|
89
|
+
uri = Addressable::URI.parse(uri)
|
|
90
|
+
ensure_uri_absolute(uri)
|
|
91
|
+
if @autoload_uris.key?(uri) && !@resources.key?(uri)
|
|
92
|
+
autoloaded = @autoload_uris[uri].call
|
|
93
|
+
register(autoloaded)
|
|
94
|
+
end
|
|
95
|
+
registered_uris = @resources.keys
|
|
96
|
+
if !registered_uris.include?(uri)
|
|
97
|
+
if @autoload_uris.key?(uri)
|
|
98
|
+
msg = [
|
|
99
|
+
"URI #{uri} was registered with autoload_uri but the result did not contain a resource with that URI.",
|
|
100
|
+
"the resource resulting from autoload_uri was:",
|
|
101
|
+
autoloaded.pretty_inspect.chomp,
|
|
102
|
+
]
|
|
103
|
+
else
|
|
104
|
+
msg = ["URI #{uri} is not registered. registered URIs:", *registered_uris]
|
|
105
|
+
end
|
|
106
|
+
raise(ResourceNotFound, msg.join("\n"))
|
|
107
|
+
end
|
|
108
|
+
@resources[uri]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def dup
|
|
112
|
+
self.class.new.tap do |reg|
|
|
113
|
+
@resources.each do |uri, resource|
|
|
114
|
+
reg.register_single(uri, resource)
|
|
115
|
+
end
|
|
116
|
+
@autoload_uris.each do |uri, autoload|
|
|
117
|
+
reg.autoload_uri(uri, &autoload)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
protected
|
|
123
|
+
# @param uri [Addressable::URI]
|
|
124
|
+
# @param resource [JSI::Base]
|
|
125
|
+
# @return [void]
|
|
126
|
+
def register_single(uri, resource)
|
|
127
|
+
@resources_mutex.synchronize do
|
|
128
|
+
ensure_uri_absolute(uri)
|
|
129
|
+
if @resources.key?(uri)
|
|
130
|
+
if @resources[uri] != resource
|
|
131
|
+
raise(Collision, "URI collision on #{uri}.\nexisting:\n#{@resources[uri].pretty_inspect.chomp}\nnew:\n#{resource.pretty_inspect.chomp}")
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
@resources[uri] = resource
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def ensure_uri_absolute(uri)
|
|
143
|
+
if uri.fragment
|
|
144
|
+
raise(NonAbsoluteURI, "#{self.class} only registers absolute URIs. cannot access URI with fragment: #{uri}")
|
|
145
|
+
end
|
|
146
|
+
if uri.relative?
|
|
147
|
+
raise(NonAbsoluteURI, "#{self.class} only registers absolute URIs. cannot access relative URI: #{uri}")
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
# a Set of JSI Schemas. always frozen.
|
|
5
|
+
#
|
|
6
|
+
# any schema instance is described by a set of schemas.
|
|
7
|
+
class SchemaSet < ::Set
|
|
8
|
+
class << self
|
|
9
|
+
# builds a SchemaSet from a mutable Set which is added to by the given block
|
|
10
|
+
#
|
|
11
|
+
# @yield [Set] a Set to which the block may add schemas
|
|
12
|
+
# @return [SchemaSet]
|
|
13
|
+
def build
|
|
14
|
+
mutable_set = Set.new
|
|
15
|
+
yield mutable_set
|
|
16
|
+
new(mutable_set)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# ensures the given param becomes a SchemaSet. returns the param if it is already SchemaSet, otherwise
|
|
20
|
+
# initializes a SchemaSet from it.
|
|
21
|
+
#
|
|
22
|
+
# @param schemas [SchemaSet, Enumerable] the object to ensure becomes a SchemaSet
|
|
23
|
+
# @return [SchemaSet] the given SchemaSet, or a SchemaSet initialized from the given Enumerable
|
|
24
|
+
# @raise [ArgumentError] when the schemas param is not an Enumerable
|
|
25
|
+
# @raise [Schema::NotASchemaError] when the schemas param contains objects which are not Schemas
|
|
26
|
+
def ensure_schema_set(schemas)
|
|
27
|
+
if schemas.is_a?(SchemaSet)
|
|
28
|
+
schemas
|
|
29
|
+
else
|
|
30
|
+
new(schemas)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# initializes a SchemaSet from the given enum and freezes it.
|
|
36
|
+
#
|
|
37
|
+
# if a block is given, each element of the enum is passed to it, and the result must be a Schema.
|
|
38
|
+
# if no block is given, the enum must contain only Schemas.
|
|
39
|
+
#
|
|
40
|
+
# @param enum [#each] the schemas to be included in the SchemaSet, or items to be passed to the block
|
|
41
|
+
# @yieldparam yields each element of enum for preprocessing into a Schema
|
|
42
|
+
# @yieldreturn [JSI::Schema]
|
|
43
|
+
# @raise [JSI::Schema::NotASchemaError]
|
|
44
|
+
def initialize(enum, &block)
|
|
45
|
+
if enum.is_a?(Schema)
|
|
46
|
+
raise(ArgumentError, [
|
|
47
|
+
"#{SchemaSet} initialized with a #{Schema}",
|
|
48
|
+
"you probably meant to pass that to #{SchemaSet}[]",
|
|
49
|
+
"or to wrap that schema in a Set or Array for #{SchemaSet}.new",
|
|
50
|
+
"given: #{enum.pretty_inspect.chomp}",
|
|
51
|
+
].join("\n"))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
super
|
|
55
|
+
|
|
56
|
+
not_schemas = reject { |s| s.is_a?(Schema) }
|
|
57
|
+
if !not_schemas.empty?
|
|
58
|
+
raise(Schema::NotASchemaError, [
|
|
59
|
+
"#{SchemaSet} initialized with non-schema objects:",
|
|
60
|
+
*not_schemas.map { |ns| ns.pretty_inspect.chomp },
|
|
61
|
+
].join("\n"))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
freeze
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# instantiates the given instance as a JSI. its schemas are inplace applicators matched from the schemas
|
|
68
|
+
# in this SchemaSet which apply to the given instance.
|
|
69
|
+
#
|
|
70
|
+
# @param instance [Object] the JSON Schema instance to be represented as a JSI
|
|
71
|
+
# @param uri [nil, #to_str, Addressable::URI] for an instance document containing schemas, this is
|
|
72
|
+
# the URI of the document, whether or not the document is itself a schema.
|
|
73
|
+
# in the normal case where the document does not contain any schemas, `uri` has no effect.
|
|
74
|
+
# schemas within the document use this uri as the base URI to resolve relative URIs.
|
|
75
|
+
# the resulting JSI may be registered with a {SchemaRegistry} (see {JSI.schema_registry}).
|
|
76
|
+
# @return [JSI::Base subclass] a JSI whose instance is the given instance and whose schemas are inplace
|
|
77
|
+
# applicators matched to the instance from the schemas in this set.
|
|
78
|
+
def new_jsi(instance,
|
|
79
|
+
uri: nil
|
|
80
|
+
)
|
|
81
|
+
applied_schemas = inplace_applicator_schemas(instance)
|
|
82
|
+
|
|
83
|
+
jsi_class = JSI::SchemaClasses.class_for_schemas(applied_schemas,
|
|
84
|
+
includes: SchemaClasses.includes_for(instance),
|
|
85
|
+
)
|
|
86
|
+
jsi = jsi_class.new(instance,
|
|
87
|
+
jsi_schema_base_uri: uri,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
jsi
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# a set of inplace applicator schemas of each schema in this set which apply to the given instance.
|
|
94
|
+
# (see {Schema::Application::InplaceApplication#inplace_applicator_schemas})
|
|
95
|
+
#
|
|
96
|
+
# @param instance (see Schema::Application::InplaceApplication#inplace_applicator_schemas)
|
|
97
|
+
# @return [JSI::SchemaSet]
|
|
98
|
+
def inplace_applicator_schemas(instance)
|
|
99
|
+
SchemaSet.new(each_inplace_applicator_schema(instance))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# yields each inplace applicator schema which applies to the given instance.
|
|
103
|
+
#
|
|
104
|
+
# @param instance (see Schema::Application::InplaceApplication#inplace_applicator_schemas)
|
|
105
|
+
# @yield [JSI::Schema]
|
|
106
|
+
# @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
|
|
107
|
+
def each_inplace_applicator_schema(instance, &block)
|
|
108
|
+
return to_enum(__method__, instance) unless block
|
|
109
|
+
|
|
110
|
+
each do |schema|
|
|
111
|
+
schema.each_inplace_applicator_schema(instance, &block)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# a set of child applicator subschemas of each schema in this set which apply to the child
|
|
118
|
+
# of the given instance on the given token.
|
|
119
|
+
# (see {Schema::Application::ChildApplication#child_applicator_schemas})
|
|
120
|
+
#
|
|
121
|
+
# @param instance (see Schema::Application::ChildApplication#child_applicator_schemas)
|
|
122
|
+
# @return [JSI::SchemaSet]
|
|
123
|
+
def child_applicator_schemas(token, instance)
|
|
124
|
+
SchemaSet.new(each_child_applicator_schema(token, instance))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# yields each child applicator schema which applies to the child of
|
|
128
|
+
# the given instance on the given token.
|
|
129
|
+
#
|
|
130
|
+
# @param (see Schema::Application::ChildApplication#child_applicator_schemas)
|
|
131
|
+
# @yield [JSI::Schema]
|
|
132
|
+
# @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
|
|
133
|
+
def each_child_applicator_schema(token, instance, &block)
|
|
134
|
+
return to_enum(__method__, token, instance) unless block
|
|
135
|
+
|
|
136
|
+
each do |schema|
|
|
137
|
+
schema.each_child_applicator_schema(token, instance, &block)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# validates the given instance against our schemas
|
|
144
|
+
#
|
|
145
|
+
# @param instance [Object] the instance to validate against our schemas
|
|
146
|
+
# @return [JSI::Validation::Result]
|
|
147
|
+
def instance_validate(instance)
|
|
148
|
+
results = map { |schema| schema.instance_validate(instance) }
|
|
149
|
+
results.inject(Validation::FullResult.new, &:merge).freeze
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# whether the given instance is valid against our schemas
|
|
153
|
+
# @param instance [Object] the instance to validate against our schemas
|
|
154
|
+
# @return [Boolean]
|
|
155
|
+
def instance_valid?(instance)
|
|
156
|
+
all? { |schema| schema.instance_valid?(instance) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @return [String]
|
|
160
|
+
def inspect
|
|
161
|
+
"#{self.class}[#{map(&:inspect).join(", ")}]"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
alias_method :to_s, :inspect
|
|
165
|
+
|
|
166
|
+
def pretty_print(q)
|
|
167
|
+
q.text self.class.to_s
|
|
168
|
+
q.text '['
|
|
169
|
+
q.group_sub {
|
|
170
|
+
q.nest(2) {
|
|
171
|
+
q.breakable('')
|
|
172
|
+
q.seplist(self, nil, :each) { |e|
|
|
173
|
+
q.pp e
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
q.breakable ''
|
|
178
|
+
q.text ']'
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|