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
@@ -1,79 +1,177 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
2
- # this module is just a namespace for schema classes.
3
- module SchemaClasses
4
- # JSI::SchemaClasses[schema_id] returns a class for the schema with the
5
- # given id, the same class as returned from JSI.class_for_schema.
4
+ # JSI Schema Modules are extended with JSI::SchemaModule
5
+ module SchemaModule
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
16
+ end
17
+
18
+ # @return [String]
19
+ def inspect
20
+ if name_from_ancestor
21
+ "#{name_from_ancestor} (JSI Schema Module)"
22
+ else
23
+ "(JSI Schema Module: #{schema.schema_uri || schema.jsi_ptr.uri})"
24
+ end
25
+ end
26
+
27
+ # invokes {JSI::Schema#new_jsi} on this module's schema, passing the given instance.
6
28
  #
7
- # @param schema_id [String] absolute schema id as returned by {JSI::Schema#schema_id}
8
- # @return [Class subclassing JSI::Base] the class for that schema
9
- def self.[](schema_id)
10
- @classes_by_id[schema_id]
29
+ # @param (see JSI::Schema#new_jsi)
30
+ # @return [JSI::Base] a JSI whose instance is the given instance
31
+ def new_jsi(instance, **kw, &b)
32
+ schema.new_jsi(instance, **kw, &b)
11
33
  end
12
- @classes_by_id = {}
34
+ end
13
35
 
14
- class << self
15
- include Memoize
36
+ # a module to extend the JSI Schema Module of a schema which describes other schemas
37
+ module DescribesSchemaModule
38
+ # instantiates the given schema content as a JSI Schema.
39
+ #
40
+ # see {JSI::Schema::DescribesSchema#new_schema}
41
+ #
42
+ # @param (see JSI::Schema::DescribesSchema#new_schema)
43
+ # @return [JSI::Base, JSI::Schema] a JSI whose instance is the given schema_content and whose schemas
44
+ # consist of this module's schema.
45
+ def new_schema(schema_content, **kw)
46
+ schema.new_schema(schema_content, **kw)
47
+ end
16
48
 
17
- # see {JSI.class_for_schema}
18
- def class_for_schema(schema_object)
19
- memoize(:class_for_schema, JSI::Schema.from_object(schema_object)) do |schema_|
20
- Class.new(Base).instance_exec(schema_) do |schema|
21
- define_singleton_method(:schema) { schema }
22
- define_method(:schema) { schema }
23
- include(JSI::SchemaClasses.module_for_schema(schema, conflicting_modules: [Base, BaseArray, BaseHash]))
49
+ # instantiates a given schema object as a JSI Schema and returns its JSI Schema Module.
50
+ #
51
+ # shortcut to chain {JSI::Schema::DescribesSchema#new_schema} + {Schema#jsi_schema_module}.
52
+ #
53
+ # @param (see JSI::Schema::DescribesSchema#new_schema)
54
+ # @return [Module, JSI::SchemaModule] the JSI Schema Module of the schema
55
+ def new_schema_module(schema_content, **kw)
56
+ schema.new_schema(schema_content, **kw).jsi_schema_module
57
+ end
58
+ end
59
+
60
+ # this module is a namespace for building schema classes and schema modules.
61
+ module SchemaClasses
62
+ extend Util::Memoize
63
+
64
+ class << self
65
+ # a JSI Schema Class which represents the given schemas.
66
+ # an instance of the class is a JSON Schema instance described by all of the given schemas.
67
+ # @private
68
+ # @param schemas [Enumerable<JSI::Schema>] schemas which the class will represent
69
+ # @return [Class subclassing JSI::Base]
70
+ def class_for_schemas(schemas)
71
+ schemas = SchemaSet.ensure_schema_set(schemas)
24
72
 
73
+ jsi_memoize(:class_for_schemas, schemas) do |schemas|
74
+ Class.new(Base).instance_exec(schemas) do |schemas|
75
+ define_singleton_method(:jsi_class_schemas) { schemas }
76
+ define_method(:jsi_schemas) { schemas }
77
+ schemas.each { |schema| include(schema.jsi_schema_module) }
25
78
  jsi_class = self
26
79
  define_method(:jsi_class) { jsi_class }
27
80
 
28
- SchemaClasses.instance_exec(self) { |klass| @classes_by_id[klass.schema_id] = klass }
81
+ self
82
+ end
83
+ end
84
+ end
85
+
86
+ # @private
87
+ # a subclass of MetaschemaNode::BootstrapSchema with the given modules included
88
+ # @param modules [Set<Module>] metaschema instance modules
89
+ # @return [Class]
90
+ def bootstrap_schema_class(modules)
91
+ modules = Util.ensure_module_set(modules)
92
+ jsi_memoize(__method__, modules) do |modules|
93
+ Class.new(MetaschemaNode::BootstrapSchema).instance_exec(modules) do |modules|
94
+ define_singleton_method(:metaschema_instance_modules) { modules }
95
+ define_method(:metaschema_instance_modules) { modules }
96
+ modules.each { |mod| include(mod) }
29
97
 
30
98
  self
31
99
  end
32
100
  end
33
101
  end
34
102
 
35
- # a module for the given schema, with accessor methods for any object
36
- # property names the schema identifies. also has a singleton method
37
- # called #schema to access the {JSI::Schema} this module represents.
38
- #
39
- # accessor methods are defined on these modules so that methods can be
40
- # defined on {JSI.class_for_schema} classes without method redefinition
41
- # warnings. additionally, these overriding instance methods can call
42
- # `super` to invoke the normal accessor behavior.
43
- #
44
- # no property names that are the same as existing method names on the JSI
45
- # class will be defined. users should use #[] and #[]= to access properties
46
- # whose names conflict with existing methods.
47
- def SchemaClasses.module_for_schema(schema_object, conflicting_modules: [])
48
- schema__ = JSI::Schema.from_object(schema_object)
49
- memoize(:module_for_schema, schema__, conflicting_modules) do |schema_, conflicting_modules_|
103
+ # see {Schema#jsi_schema_module}
104
+ # @api private
105
+ # @return [Module]
106
+ def module_for_schema(schema)
107
+ Schema.ensure_schema(schema)
108
+ jsi_memoize(:module_for_schema, schema) do |schema|
50
109
  Module.new.tap do |m|
51
- m.instance_exec(schema_) do |schema|
110
+ m.module_eval do
52
111
  define_singleton_method(:schema) { schema }
53
- define_singleton_method(:schema_id) do
54
- schema.schema_id
112
+
113
+ extend SchemaModule
114
+
115
+ schema.jsi_schema_instance_modules.each do |mod|
116
+ include(mod)
55
117
  end
56
- define_singleton_method(:inspect) do
57
- %Q(#<Module for Schema: #{schema_id}>)
118
+
119
+ accessor_module = JSI::SchemaClasses.accessor_module_for_schema(schema,
120
+ conflicting_modules: Set[JSI::Base, JSI::PathedArrayNode, JSI::PathedHashNode] +
121
+ schema.jsi_schema_instance_modules,
122
+ )
123
+ include accessor_module
124
+
125
+ define_singleton_method(:jsi_property_accessors) { accessor_module.jsi_property_accessors }
126
+
127
+ if schema.describes_schema?
128
+ extend DescribesSchemaModule
58
129
  end
59
130
 
60
- conflicting_instance_methods = (conflicting_modules_ + [m]).map do |mod|
131
+ @possibly_schema_node = schema
132
+ extend(SchemaModulePossibly)
133
+ schema.jsi_schemas.each do |schema_schema|
134
+ extend JSI::SchemaClasses.accessor_module_for_schema(schema_schema,
135
+ conflicting_modules: Set[Module, SchemaModule, SchemaModulePossibly],
136
+ setters: false,
137
+ )
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ # a module of accessors for described property names of the given schema.
145
+ # getters are always defined. setters are defined by default.
146
+ # @param schema [JSI::Schema] a schema for which to define accessors for any described property names
147
+ # @param conflicting_modules [Enumerable<Module>] an array of modules (or classes) which
148
+ # may be used alongside the accessor module. methods defined by any conflicting_module
149
+ # will not be defined as accessors.
150
+ # @param setters [Boolean] whether to define setter methods
151
+ # @return [Module]
152
+ def accessor_module_for_schema(schema, conflicting_modules: , setters: true)
153
+ Schema.ensure_schema(schema)
154
+ jsi_memoize(:accessor_module_for_schema, schema, conflicting_modules, setters) do |schema, conflicting_modules, setters|
155
+ Module.new.tap do |m|
156
+ m.module_eval do
157
+ conflicting_instance_methods = (conflicting_modules + [m]).map do |mod|
61
158
  mod.instance_methods + mod.private_instance_methods
62
159
  end.inject(Set.new, &:|)
63
- accessors_to_define = schema.described_object_property_names.map(&:to_s) - conflicting_instance_methods.map(&:to_s)
160
+
161
+ accessors_to_define = schema.described_object_property_names.select do |name|
162
+ # must not conflict with any method on a conflicting module
163
+ Util.ok_ruby_method_name?(name) && !conflicting_instance_methods.any? { |mn| mn.to_s == name }
164
+ end.to_set.freeze
165
+
166
+ define_singleton_method(:jsi_property_accessors) { accessors_to_define }
167
+
64
168
  accessors_to_define.each do |property_name|
65
- define_method(property_name) do
66
- if respond_to?(:[])
67
- self[property_name]
68
- else
69
- raise(NoMethodError, "schema instance of class #{self.class} does not respond to []; cannot call reader '#{property_name}'. instance is #{instance.pretty_inspect.chomp}")
70
- end
169
+ define_method(property_name) do |*a|
170
+ self[property_name, *a]
71
171
  end
72
- define_method("#{property_name}=") do |value|
73
- if respond_to?(:[]=)
172
+ if setters
173
+ define_method("#{property_name}=") do |value|
74
174
  self[property_name] = value
75
- else
76
- raise(NoMethodError, "schema instance of class #{self.class} does not respond to []=; cannot call writer '#{property_name}='. instance is #{instance.pretty_inspect.chomp}")
77
175
  end
78
176
  end
79
177
  end
@@ -83,4 +181,93 @@ module JSI
83
181
  end
84
182
  end
85
183
  end
184
+
185
+ # a JSI Schema module and a JSI::NotASchemaModule are both a SchemaModulePossibly.
186
+ # this module provides a #[] method.
187
+ module SchemaModulePossibly
188
+ attr_reader :possibly_schema_node
189
+
190
+ # a name relative to a named schema module of an ancestor schema.
191
+ # for example, if `Foos = JSI::JSONSchemaOrgDraft07.new_schema_module({'items' => {}})`
192
+ # then the module `Foos.items` will have a name_from_ancestor of `"Foos.items"`
193
+ # @return [String, nil]
194
+ def name_from_ancestor
195
+ schema_ancestors = [possibly_schema_node] + possibly_schema_node.jsi_parent_nodes
196
+ named_parent_schema = schema_ancestors.detect { |jsi| jsi.is_a?(JSI::Schema) && jsi.jsi_schema_module.name }
197
+
198
+ return nil unless named_parent_schema
199
+
200
+ tokens = possibly_schema_node.jsi_ptr.ptr_relative_to(named_parent_schema.jsi_ptr).tokens
201
+ name = named_parent_schema.jsi_schema_module.name
202
+ parent = named_parent_schema
203
+ tokens.each do |token|
204
+ if parent.jsi_schemas.any? { |s| s.jsi_schema_module.jsi_property_accessors.include?(token) }
205
+ name += ".#{token}"
206
+ elsif [String, Numeric, TrueClass, FalseClass, NilClass].any? { |m| token.is_a?(m) }
207
+ name += "[#{token.inspect}]"
208
+ else
209
+ return nil
210
+ end
211
+ parent = parent[token]
212
+ end
213
+ name
214
+ end
215
+
216
+ # subscripting a JSI schema module or a NotASchemaModule will subscript the node, and
217
+ # if the result is a JSI::Schema, return the JSI Schema module of that schema; if it is a PathedNode,
218
+ # return a NotASchemaModule; or if it is another value (a basic type), return that value.
219
+ #
220
+ # @param token [Object]
221
+ # @return [Module, NotASchemaModule, Object]
222
+ def [](token)
223
+ sub = @possibly_schema_node[token]
224
+ if sub.is_a?(JSI::Schema)
225
+ sub.jsi_schema_module
226
+ elsif sub.is_a?(JSI::PathedNode)
227
+ NotASchemaModule.new(sub)
228
+ else
229
+ sub
230
+ end
231
+ end
232
+ end
233
+
234
+ # a JSI Schema Module is a module which represents a schema. a NotASchemaModule represents
235
+ # a node in a schema's document which is not a schema, such as the 'properties'
236
+ # object (which contains schemas but is not a schema).
237
+ #
238
+ # instances of this class act as a stand-in to allow users to subscript or call property accessors on
239
+ # schema modules to refer to their subschemas' schema modules.
240
+ #
241
+ # a NotASchemaModule is extended with the module_for_schema of the node's schema.
242
+ #
243
+ # NotASchemaModule holds a node which is not a schema. when subscripted, it subscripts
244
+ # its node. if the value is a JSI::Schema, its schema module is returned. if the value
245
+ # is another node, a NotASchemaModule for that node is returned. otherwise - when the
246
+ # value is a basic type - that value itself is returned.
247
+ class NotASchemaModule
248
+ # @param node [JSI::PathedNode]
249
+ def initialize(node)
250
+ unless node.is_a?(JSI::PathedNode)
251
+ raise(TypeError, "not JSI::PathedNode: #{node.pretty_inspect.chomp}")
252
+ end
253
+ if node.is_a?(JSI::Schema)
254
+ raise(TypeError, "cannot instantiate NotASchemaModule for a JSI::Schema node: #{node.pretty_inspect.chomp}")
255
+ end
256
+ @possibly_schema_node = node
257
+ node.jsi_schemas.each do |schema|
258
+ extend(JSI::SchemaClasses.accessor_module_for_schema(schema, conflicting_modules: [NotASchemaModule, SchemaModulePossibly], setters: false))
259
+ end
260
+ end
261
+
262
+ include SchemaModulePossibly
263
+
264
+ # @return [String]
265
+ def inspect
266
+ if name_from_ancestor
267
+ "#{name_from_ancestor} (JSI wrapper for Schema Module)"
268
+ else
269
+ "(JSI wrapper for Schema Module: #{@possibly_schema_node.jsi_ptr.uri})"
270
+ end
271
+ end
272
+ end
86
273
  end
@@ -0,0 +1,141 @@
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 child of the resource (including the resource itself) is registered if it is a schema that
28
+ # 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_child_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
+ register(@autoload_uris[uri].call)
93
+ end
94
+ registered_uris = @resources.keys
95
+ if !registered_uris.include?(uri)
96
+ raise(ResourceNotFound, "URI #{uri} is not registered. registered URIs:\n#{registered_uris.join("\n")}")
97
+ end
98
+ @resources[uri]
99
+ end
100
+
101
+ def dup
102
+ self.class.new.tap do |reg|
103
+ @resources.each do |uri, resource|
104
+ reg.register_single(uri, resource)
105
+ end
106
+ @autoload_uris.each do |uri, autoload|
107
+ reg.autoload_uri(uri, &autoload)
108
+ end
109
+ end
110
+ end
111
+
112
+ protected
113
+ # @param uri [Addressable::URI]
114
+ # @param resource [JSI::Base]
115
+ # @return [void]
116
+ def register_single(uri, resource)
117
+ @resources_mutex.synchronize do
118
+ ensure_uri_absolute(uri)
119
+ if @resources.key?(uri)
120
+ if @resources[uri] != resource
121
+ raise(Collision, "URI collision on #{uri}.\nexisting:\n#{@resources[uri].pretty_inspect.chomp}\nnew:\n#{resource.pretty_inspect.chomp}")
122
+ end
123
+ else
124
+ @resources[uri] = resource
125
+ end
126
+ end
127
+ nil
128
+ end
129
+
130
+ private
131
+
132
+ def ensure_uri_absolute(uri)
133
+ if uri.fragment
134
+ raise(NonAbsoluteURI, "#{self.class} only registers absolute URIs. cannot access URI with fragment: #{uri}")
135
+ end
136
+ if uri.relative?
137
+ raise(NonAbsoluteURI, "#{self.class} only registers absolute URIs. cannot access relative URI: #{uri}")
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,141 @@
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
+ super
46
+
47
+ not_schemas = reject { |s| s.is_a?(Schema) }
48
+ if !not_schemas.empty?
49
+ raise(Schema::NotASchemaError, [
50
+ "JSI::SchemaSet initialized with non-schema objects:",
51
+ *not_schemas.map { |ns| ns.pretty_inspect.chomp },
52
+ ].join("\n"))
53
+ end
54
+
55
+ freeze
56
+ end
57
+
58
+ # instantiates the given instance as a JSI. its schemas are inplace applicators matched from the schemas
59
+ # in this SchemaSet which apply to the given instance.
60
+ #
61
+ # @param instance [Object] the JSON Schema instance to be represented as a JSI
62
+ # @param uri [nil, #to_str, Addressable::URI] for an instance document containing schemas, this is
63
+ # the URI of the document, whether or not the document is itself a schema.
64
+ # in the normal case where the document does not contain any schemas, `uri` has no effect.
65
+ # schemas within the document use this uri as the base URI to resolve relative URIs.
66
+ # the resulting JSI may be registered with a {SchemaRegistry} (see {JSI.schema_registry}).
67
+ # @return [JSI::Base subclass] a JSI whose instance is the given instance and whose schemas are inplace
68
+ # applicators matched to the instance from the schemas in this set.
69
+ def new_jsi(instance,
70
+ uri: nil
71
+ )
72
+ applied_schemas = inplace_applicator_schemas(instance)
73
+
74
+ jsi = JSI::SchemaClasses.class_for_schemas(applied_schemas).new(instance,
75
+ jsi_schema_base_uri: uri,
76
+ )
77
+
78
+ jsi
79
+ end
80
+
81
+ # a set of inplace applicator schemas of each schema in this set which apply to the given instance.
82
+ # (see {Schema::Application::InplaceApplication#inplace_applicator_schemas})
83
+ #
84
+ # @param instance (see Schema::Application::InplaceApplication#inplace_applicator_schemas)
85
+ # @return [JSI::SchemaSet]
86
+ def inplace_applicator_schemas(instance)
87
+ SchemaSet.new(each_inplace_applicator_schema(instance))
88
+ end
89
+
90
+ # yields each inplace applicator schema which applies to the given instance.
91
+ #
92
+ # @param instance (see Schema::Application::InplaceApplication#inplace_applicator_schemas)
93
+ # @yield [JSI::Schema]
94
+ # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
95
+ def each_inplace_applicator_schema(instance, &block)
96
+ return to_enum(__method__, instance) unless block
97
+
98
+ each do |schema|
99
+ schema.each_inplace_applicator_schema(instance, &block)
100
+ end
101
+
102
+ nil
103
+ end
104
+
105
+ # validates the given instance against our schemas
106
+ #
107
+ # @param instance [Object] the instance to validate against our schemas
108
+ # @return [JSI::Validation::Result]
109
+ def instance_validate(instance)
110
+ results = map { |schema| schema.instance_validate(instance) }
111
+ results.inject(Validation::FullResult.new, &:merge).freeze
112
+ end
113
+
114
+ # whether the given instance is valid against our schemas
115
+ # @param instance [Object] the instance to validate against our schemas
116
+ # @return [Boolean]
117
+ def instance_valid?(instance)
118
+ all? { |schema| schema.instance_valid?(instance) }
119
+ end
120
+
121
+ # @return [String]
122
+ def inspect
123
+ "#{self.class}[#{map(&:inspect).join(", ")}]"
124
+ end
125
+
126
+ def pretty_print(q)
127
+ q.text self.class.to_s
128
+ q.text '['
129
+ q.group_sub {
130
+ q.nest(2) {
131
+ q.breakable('')
132
+ q.seplist(self, nil, :each) { |e|
133
+ q.pp e
134
+ }
135
+ }
136
+ }
137
+ q.breakable ''
138
+ q.text ']'
139
+ end
140
+ end
141
+ end
@@ -1,7 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
2
- SimpleWrap = JSI.class_for_schema({"additionalProperties": {"$ref": "#"}, "items": {"$ref": "#"}})
4
+ SimpleWrap = JSI::JSONSchemaOrgDraft06.new_schema_module({
5
+ "additionalProperties": {"$ref": "#"},
6
+ "items": {"$ref": "#"}
7
+ })
3
8
 
4
- # SimpleWrap is a JSI class which recursively wraps nested structures
5
- class SimpleWrap
9
+ # SimpleWrap is a JSI schema module which recursively wraps nested structures
10
+ module SimpleWrap
6
11
  end
7
12
  end