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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +33 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +114 -42
  6. data/jsi.gemspec +14 -12
  7. data/lib/jsi/base/node.rb +183 -0
  8. data/lib/jsi/base.rb +388 -220
  9. data/lib/jsi/jsi_coder.rb +8 -7
  10. data/lib/jsi/metaschema.rb +0 -1
  11. data/lib/jsi/metaschema_node/bootstrap_schema.rb +101 -0
  12. data/lib/jsi/metaschema_node.rb +159 -135
  13. data/lib/jsi/ptr.rb +303 -0
  14. data/lib/jsi/schema/application/child_application/contains.rb +25 -0
  15. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  16. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  17. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  18. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  19. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  20. data/lib/jsi/schema/application/child_application.rb +38 -0
  21. data/lib/jsi/schema/application/draft04.rb +8 -0
  22. data/lib/jsi/schema/application/draft06.rb +8 -0
  23. data/lib/jsi/schema/application/draft07.rb +8 -0
  24. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  25. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  26. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  27. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  28. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  29. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  30. data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
  31. data/lib/jsi/schema/application/inplace_application.rb +41 -0
  32. data/lib/jsi/schema/application.rb +12 -0
  33. data/lib/jsi/schema/draft04.rb +14 -0
  34. data/lib/jsi/schema/draft06.rb +14 -0
  35. data/lib/jsi/schema/draft07.rb +14 -0
  36. data/lib/jsi/schema/issue.rb +36 -0
  37. data/lib/jsi/schema/ref.rb +160 -0
  38. data/lib/jsi/schema/schema_ancestor_node.rb +113 -0
  39. data/lib/jsi/schema/validation/array.rb +69 -0
  40. data/lib/jsi/schema/validation/const.rb +20 -0
  41. data/lib/jsi/schema/validation/contains.rb +25 -0
  42. data/lib/jsi/schema/validation/core.rb +39 -0
  43. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  44. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  45. data/lib/jsi/schema/validation/draft04.rb +112 -0
  46. data/lib/jsi/schema/validation/draft06.rb +122 -0
  47. data/lib/jsi/schema/validation/draft07.rb +159 -0
  48. data/lib/jsi/schema/validation/enum.rb +25 -0
  49. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  50. data/lib/jsi/schema/validation/items.rb +54 -0
  51. data/lib/jsi/schema/validation/not.rb +20 -0
  52. data/lib/jsi/schema/validation/numeric.rb +121 -0
  53. data/lib/jsi/schema/validation/object.rb +45 -0
  54. data/lib/jsi/schema/validation/pattern.rb +34 -0
  55. data/lib/jsi/schema/validation/properties.rb +101 -0
  56. data/lib/jsi/schema/validation/property_names.rb +32 -0
  57. data/lib/jsi/schema/validation/ref.rb +40 -0
  58. data/lib/jsi/schema/validation/required.rb +27 -0
  59. data/lib/jsi/schema/validation/someof.rb +90 -0
  60. data/lib/jsi/schema/validation/string.rb +47 -0
  61. data/lib/jsi/schema/validation/type.rb +49 -0
  62. data/lib/jsi/schema/validation.rb +51 -0
  63. data/lib/jsi/schema.rb +508 -149
  64. data/lib/jsi/schema_classes.rb +199 -59
  65. data/lib/jsi/schema_registry.rb +151 -0
  66. data/lib/jsi/schema_set.rb +181 -0
  67. data/lib/jsi/simple_wrap.rb +23 -4
  68. data/lib/jsi/util/private/attr_struct.rb +127 -0
  69. data/lib/jsi/util/private.rb +204 -0
  70. data/lib/jsi/util/typelike.rb +229 -0
  71. data/lib/jsi/util.rb +89 -53
  72. data/lib/jsi/validation/error.rb +34 -0
  73. data/lib/jsi/validation/result.rb +210 -0
  74. data/lib/jsi/validation.rb +15 -0
  75. data/lib/jsi/version.rb +3 -1
  76. data/lib/jsi.rb +44 -14
  77. data/lib/schemas/json-schema.org/draft-04/schema.rb +10 -3
  78. data/lib/schemas/json-schema.org/draft-06/schema.rb +10 -3
  79. data/lib/schemas/json-schema.org/draft-07/schema.rb +14 -0
  80. data/readme.rb +138 -0
  81. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  82. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  83. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  84. metadata +75 -122
  85. data/.simplecov +0 -3
  86. data/Rakefile.rb +0 -9
  87. data/lib/jsi/base/to_rb.rb +0 -128
  88. data/lib/jsi/json/node.rb +0 -203
  89. data/lib/jsi/json/pointer.rb +0 -419
  90. data/lib/jsi/json-schema-fragments.rb +0 -61
  91. data/lib/jsi/json.rb +0 -10
  92. data/lib/jsi/pathed_node.rb +0 -118
  93. data/lib/jsi/typelike_modules.rb +0 -240
  94. data/resources/icons/AGPL-3.0.png +0 -0
  95. data/test/base_array_test.rb +0 -323
  96. data/test/base_hash_test.rb +0 -337
  97. data/test/base_test.rb +0 -486
  98. data/test/jsi_coder_test.rb +0 -85
  99. data/test/jsi_json_arraynode_test.rb +0 -150
  100. data/test/jsi_json_hashnode_test.rb +0 -132
  101. data/test/jsi_json_node_test.rb +0 -257
  102. data/test/jsi_json_pointer_test.rb +0 -102
  103. data/test/jsi_test.rb +0 -11
  104. data/test/jsi_typelike_as_json_test.rb +0 -53
  105. data/test/metaschema_node_test.rb +0 -19
  106. data/test/schema_module_test.rb +0 -21
  107. data/test/schema_test.rb +0 -208
  108. data/test/spreedly_openapi_test.rb +0 -8
  109. data/test/test_helper.rb +0 -97
  110. data/test/util_test.rb +0 -62
@@ -3,41 +3,94 @@
3
3
  module JSI
4
4
  # JSI Schema Modules are extended with JSI::SchemaModule
5
5
  module SchemaModule
6
- # @return [String] absolute schema_id of the schema this module represents.
7
- # see {Schema#schema_id}.
8
- def schema_id
9
- schema.schema_id
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
- uri = schema.schema_id || schema.node_ptr.uri
15
- if name
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, *a, &b)
25
- schema.new_jsi(instance, *a, &b)
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 just a namespace for schema classes.
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
- include Util::Memoize
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
- # see {JSI.class_for_schemas}
35
- def class_for_schemas(schema_objects)
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 module for the given schema, with accessor methods for any object property names the schema
51
- # identifies (see {JSI::Schema#described_object_property_names}).
52
- #
53
- # defines a singleton method #schema to access the {JSI::Schema} this module represents, and extends
54
- # the module with {JSI::SchemaModule}.
55
- def module_for_schema(schema_object)
56
- schema = JSI::Schema.from_object(schema_object)
57
- jsi_memoize(:module_for_schema, schema) do |schema|
58
- Module.new.tap do |m|
59
- m.module_eval do
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
- include JSI::SchemaClasses.accessor_module_for_schema(schema, conflicting_modules: [JSI::Base, JSI::PathedArrayNode, JSI::PathedHashNode])
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(JSI::SchemaClasses.accessor_module_for_schema(schema_schema, conflicting_modules: [Module, SchemaModule, SchemaModulePossibly]))
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
- # @return [Module] a module of accessors (setters and getters) for described property names of the given
81
- # schema
82
- def accessor_module_for_schema(schema, conflicting_modules: )
83
- unless schema.is_a?(JSI::Schema)
84
- raise(JSI::Schema::NotASchemaError, "not a schema: #{schema.pretty_inspect.chomp}")
85
- end
86
- jsi_memoize(:accessor_module_for_schema, schema, conflicting_modules) do |schema, conflicting_modules|
87
- Module.new.tap do |m|
88
- m.module_eval do
89
- conflicting_instance_methods = (conflicting_modules + [m]).map do |mod|
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
- accessors_to_define = schema.described_object_property_names.map(&:to_s) - conflicting_instance_methods.map(&:to_s)
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
- define_method("#{property_name}=") do |value|
98
- self[property_name] = value
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::Schema module and a JSI::NotASchemaModule are both a SchemaModulePossibly.
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 a JSI::Schema class; if it is a PathedNode,
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 [Class, NotASchemaModule, Object]
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::PathedNode)
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 schema module is a module which represents a schema. a NotASchemaModule represents
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
- # node (which contains schemas but is not a schema).
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::PathedNode]
275
+ # @param node [JSI::Base]
143
276
  def initialize(node)
144
- unless node.is_a?(JSI::PathedNode)
145
- raise(TypeError, "not JSI::PathedNode: #{node.pretty_inspect.chomp}")
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