jsi 0.8.2 → 0.9.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 (146) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +3 -2
  3. data/CHANGELOG.md +8 -3
  4. data/LICENSE.md +2 -3
  5. data/README.md +68 -31
  6. data/docs/Glossary.md +313 -0
  7. data/jsi.gemspec +1 -0
  8. data/lib/jsi/base/mutability.rb +4 -0
  9. data/lib/jsi/base/node.rb +63 -24
  10. data/lib/jsi/base.rb +556 -173
  11. data/lib/jsi/metaschema_node/bootstrap_schema.rb +106 -56
  12. data/lib/jsi/metaschema_node.rb +227 -160
  13. data/lib/jsi/ptr.rb +32 -15
  14. data/lib/jsi/ref.rb +197 -0
  15. data/lib/jsi/registry.rb +311 -0
  16. data/lib/jsi/schema/cxt/child_application.rb +35 -0
  17. data/lib/jsi/schema/cxt/inplace_application.rb +37 -0
  18. data/lib/jsi/schema/cxt.rb +80 -0
  19. data/lib/jsi/schema/dialect.rb +137 -0
  20. data/lib/jsi/schema/draft04.rb +113 -5
  21. data/lib/jsi/schema/draft06.rb +123 -5
  22. data/lib/jsi/schema/draft07.rb +157 -5
  23. data/lib/jsi/schema/draft202012.rb +303 -0
  24. data/lib/jsi/schema/dynamic_anchor_map.rb +63 -0
  25. data/lib/jsi/schema/element.rb +69 -0
  26. data/lib/jsi/schema/elements/anchor.rb +13 -0
  27. data/lib/jsi/schema/elements/array_validation.rb +82 -0
  28. data/lib/jsi/schema/elements/comment.rb +10 -0
  29. data/lib/jsi/schema/{validation → elements}/const.rb +11 -7
  30. data/lib/jsi/schema/elements/contains.rb +59 -0
  31. data/lib/jsi/schema/elements/contains_minmax.rb +91 -0
  32. data/lib/jsi/schema/elements/content_encoding.rb +10 -0
  33. data/lib/jsi/schema/elements/content_media_type.rb +10 -0
  34. data/lib/jsi/schema/elements/content_schema.rb +16 -0
  35. data/lib/jsi/schema/elements/default.rb +11 -0
  36. data/lib/jsi/schema/elements/definitions.rb +19 -0
  37. data/lib/jsi/schema/elements/dependencies.rb +99 -0
  38. data/lib/jsi/schema/elements/dependent_required.rb +49 -0
  39. data/lib/jsi/schema/elements/dependent_schemas.rb +69 -0
  40. data/lib/jsi/schema/elements/dynamic_ref.rb +69 -0
  41. data/lib/jsi/schema/elements/enum.rb +26 -0
  42. data/lib/jsi/schema/elements/examples.rb +10 -0
  43. data/lib/jsi/schema/elements/format.rb +10 -0
  44. data/lib/jsi/schema/elements/id.rb +30 -0
  45. data/lib/jsi/schema/elements/if_then_else.rb +82 -0
  46. data/lib/jsi/schema/elements/info_bool.rb +10 -0
  47. data/lib/jsi/schema/elements/info_string.rb +10 -0
  48. data/lib/jsi/schema/elements/items.rb +93 -0
  49. data/lib/jsi/schema/elements/items_prefixed.rb +96 -0
  50. data/lib/jsi/schema/elements/not.rb +31 -0
  51. data/lib/jsi/schema/elements/numeric.rb +137 -0
  52. data/lib/jsi/schema/elements/numeric_draft04.rb +77 -0
  53. data/lib/jsi/schema/elements/object_validation.rb +55 -0
  54. data/lib/jsi/schema/elements/pattern.rb +35 -0
  55. data/lib/jsi/schema/elements/properties.rb +145 -0
  56. data/lib/jsi/schema/elements/property_names.rb +48 -0
  57. data/lib/jsi/schema/elements/ref.rb +62 -0
  58. data/lib/jsi/schema/elements/required.rb +34 -0
  59. data/lib/jsi/schema/elements/self.rb +24 -0
  60. data/lib/jsi/schema/elements/some_of.rb +180 -0
  61. data/lib/jsi/schema/elements/string_validation.rb +57 -0
  62. data/lib/jsi/schema/elements/type.rb +43 -0
  63. data/lib/jsi/schema/elements/unevaluated_items.rb +54 -0
  64. data/lib/jsi/schema/elements/unevaluated_properties.rb +54 -0
  65. data/lib/jsi/schema/elements/xschema.rb +10 -0
  66. data/lib/jsi/schema/elements/xvocabulary.rb +10 -0
  67. data/lib/jsi/schema/elements.rb +101 -0
  68. data/lib/jsi/schema/issue.rb +3 -4
  69. data/lib/jsi/schema/schema_ancestor_node.rb +103 -50
  70. data/lib/jsi/schema/vocabulary.rb +36 -0
  71. data/lib/jsi/schema.rb +519 -337
  72. data/lib/jsi/schema_classes.rb +168 -124
  73. data/lib/jsi/schema_set.rb +67 -126
  74. data/lib/jsi/set.rb +23 -0
  75. data/lib/jsi/simple_wrap.rb +13 -16
  76. data/lib/jsi/struct.rb +57 -0
  77. data/lib/jsi/uri.rb +40 -0
  78. data/lib/jsi/util/private/memo_map.rb +9 -13
  79. data/lib/jsi/util/private.rb +57 -12
  80. data/lib/jsi/util/typelike.rb +19 -64
  81. data/lib/jsi/util.rb +52 -34
  82. data/lib/jsi/validation/error.rb +41 -2
  83. data/lib/jsi/validation/result.rb +118 -71
  84. data/lib/jsi/validation.rb +1 -6
  85. data/lib/jsi/version.rb +1 -1
  86. data/lib/jsi.rb +158 -41
  87. data/lib/schemas/json-schema.org/draft/2020-12/schema.rb +67 -0
  88. data/lib/schemas/json-schema.org/draft-04/schema.rb +63 -106
  89. data/lib/schemas/json-schema.org/draft-06/schema.rb +56 -105
  90. data/lib/schemas/json-schema.org/draft-07/schema.rb +67 -124
  91. data/readme.rb +3 -3
  92. data/{resources}/schemas/2020-12_strict.json +19 -0
  93. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/applicator.json +48 -0
  94. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/content.json +17 -0
  95. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/core.json +51 -0
  96. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/format-annotation.json +14 -0
  97. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/format-assertion.json +14 -0
  98. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/meta-data.json +37 -0
  99. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/unevaluated.json +15 -0
  100. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/validation.json +98 -0
  101. data/{resources}/schemas/json-schema.org/draft/2020-12/schema.json +58 -0
  102. metadata +69 -47
  103. data/lib/jsi/schema/application/child_application/contains.rb +0 -25
  104. data/lib/jsi/schema/application/child_application/draft04.rb +0 -21
  105. data/lib/jsi/schema/application/child_application/draft06.rb +0 -28
  106. data/lib/jsi/schema/application/child_application/draft07.rb +0 -28
  107. data/lib/jsi/schema/application/child_application/items.rb +0 -18
  108. data/lib/jsi/schema/application/child_application/properties.rb +0 -25
  109. data/lib/jsi/schema/application/child_application.rb +0 -13
  110. data/lib/jsi/schema/application/draft04.rb +0 -8
  111. data/lib/jsi/schema/application/draft06.rb +0 -8
  112. data/lib/jsi/schema/application/draft07.rb +0 -8
  113. data/lib/jsi/schema/application/inplace_application/dependencies.rb +0 -28
  114. data/lib/jsi/schema/application/inplace_application/draft04.rb +0 -25
  115. data/lib/jsi/schema/application/inplace_application/draft06.rb +0 -26
  116. data/lib/jsi/schema/application/inplace_application/draft07.rb +0 -32
  117. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +0 -20
  118. data/lib/jsi/schema/application/inplace_application/ref.rb +0 -18
  119. data/lib/jsi/schema/application/inplace_application/someof.rb +0 -44
  120. data/lib/jsi/schema/application/inplace_application.rb +0 -14
  121. data/lib/jsi/schema/application.rb +0 -12
  122. data/lib/jsi/schema/ref.rb +0 -186
  123. data/lib/jsi/schema/validation/array.rb +0 -69
  124. data/lib/jsi/schema/validation/contains.rb +0 -25
  125. data/lib/jsi/schema/validation/dependencies.rb +0 -49
  126. data/lib/jsi/schema/validation/draft04/minmax.rb +0 -93
  127. data/lib/jsi/schema/validation/draft04.rb +0 -110
  128. data/lib/jsi/schema/validation/draft06.rb +0 -120
  129. data/lib/jsi/schema/validation/draft07.rb +0 -157
  130. data/lib/jsi/schema/validation/enum.rb +0 -25
  131. data/lib/jsi/schema/validation/ifthenelse.rb +0 -46
  132. data/lib/jsi/schema/validation/items.rb +0 -54
  133. data/lib/jsi/schema/validation/not.rb +0 -20
  134. data/lib/jsi/schema/validation/numeric.rb +0 -121
  135. data/lib/jsi/schema/validation/object.rb +0 -45
  136. data/lib/jsi/schema/validation/pattern.rb +0 -34
  137. data/lib/jsi/schema/validation/properties.rb +0 -101
  138. data/lib/jsi/schema/validation/property_names.rb +0 -32
  139. data/lib/jsi/schema/validation/ref.rb +0 -40
  140. data/lib/jsi/schema/validation/required.rb +0 -27
  141. data/lib/jsi/schema/validation/someof.rb +0 -90
  142. data/lib/jsi/schema/validation/string.rb +0 -47
  143. data/lib/jsi/schema/validation/type.rb +0 -49
  144. data/lib/jsi/schema/validation.rb +0 -49
  145. data/lib/jsi/schema_registry.rb +0 -200
  146. data/lib/jsi/util/private/attr_struct.rb +0 -141
data/lib/jsi/schema.rb CHANGED
@@ -11,125 +11,50 @@ module JSI
11
11
  # The content of an instance which is a JSI::Schema (referred to in this context as schema_content) is
12
12
  # typically a Hash (JSON object) or a boolean.
13
13
  module Schema
14
- autoload :Application, 'jsi/schema/application'
15
- autoload :Validation, 'jsi/schema/validation'
14
+ autoload(:Element, 'jsi/schema/element')
15
+ autoload(:Vocabulary, 'jsi/schema/vocabulary')
16
+ autoload(:Dialect, 'jsi/schema/dialect')
17
+ autoload(:Cxt, 'jsi/schema/cxt')
18
+
19
+ autoload(:Elements, 'jsi/schema/elements')
16
20
 
17
21
  autoload :Issue, 'jsi/schema/issue'
18
- autoload :Ref, 'jsi/schema/ref'
22
+ autoload(:DynamicAnchorMap, 'jsi/schema/dynamic_anchor_map')
19
23
 
20
24
  autoload :SchemaAncestorNode, 'jsi/schema/schema_ancestor_node'
21
25
 
22
26
  autoload :Draft04, 'jsi/schema/draft04'
23
27
  autoload :Draft06, 'jsi/schema/draft06'
24
28
  autoload :Draft07, 'jsi/schema/draft07'
25
-
26
- class Error < StandardError
27
- end
29
+ autoload(:Draft202012, 'jsi/schema/draft202012')
28
30
 
29
31
  # an exception raised when a thing is expected to be a JSI::Schema, but is not
30
- class NotASchemaError < Error
32
+ class NotASchemaError < TypeError
31
33
  end
32
34
 
33
- # an exception raised when we are unable to resolve a schema reference
34
- class ReferenceError < StandardError
35
+ class NotAMetaSchemaError < TypeError
35
36
  end
36
37
 
37
- # extends any schema which uses the keyword '$id' to identify its canonical URI
38
- module BigMoneyId
39
- # the contents of a $id keyword whose value is a string, or nil
40
- # @return [#to_str, nil]
41
- def id
42
- if keyword?('$id') && schema_content['$id'].respond_to?(:to_str)
43
- schema_content['$id']
44
- else
45
- nil
46
- end
47
- end
48
- end
49
-
50
- # extends any schema which uses the keyword 'id' to identify its canonical URI
51
- module OldId
52
- # the contents of an `id` keyword whose value is a string, or nil
53
- # @return [#to_str, nil]
54
- def id
55
- if keyword?('id') && schema_content['id'].respond_to?(:to_str)
56
- schema_content['id']
57
- else
58
- nil
59
- end
60
- end
61
- end
62
-
63
- # extends any schema which defines an anchor as a URI fragment in the schema id
64
- module IdWithAnchor
65
- # a URI for the schema's id, unless the id defines an anchor in its
66
- # fragment. nil if the schema defines no id.
67
- # @return [Addressable::URI, nil]
68
- def id_without_fragment
69
- if id
70
- id_uri = Util.uri(id)
71
- if id_uri.merge(fragment: nil).empty?
72
- # fragment-only id is just an anchor
73
- # e.g. #foo
74
- nil
75
- elsif id_uri.fragment == nil
76
- # no fragment
77
- # e.g. http://localhost:1234/bar
78
- id_uri
79
- elsif id_uri.fragment == ''
80
- # empty fragment
81
- # e.g. http://json-schema.org/draft-07/schema#
82
- id_uri.merge(fragment: nil).freeze
83
- elsif jsi_schema_base_uri && jsi_schema_base_uri.join(id_uri).merge(fragment: nil) == jsi_schema_base_uri
84
- # the id, resolved against the base uri, consists of the base uri plus an anchor fragment.
85
- # so there's no non-fragment id.
86
- # e.g. base uri is http://localhost:1234/bar
87
- # and id is http://localhost:1234/bar#foo
88
- nil
89
- else
90
- # e.g. http://localhost:1234/bar#foo
91
- id_uri.merge(fragment: nil).freeze
92
- end
93
- else
94
- nil
95
- end
38
+ # @deprecated alias after v0.8
39
+ # an exception raised when we are unable to resolve a schema reference
40
+ ReferenceError = ResolutionError
41
+
42
+ # A reference to a schema identified by a given URI.
43
+ # {#resolve} will return a Schema, and param `referrer` must be a Schema.
44
+ class Ref < Ref
45
+ # @param ref_schema [Schema] deprecated; use `referrer`
46
+ def initialize(ref, ref_schema: nil, **kw)
47
+ super(ref, referrer: ref_schema, **kw)
96
48
  end
97
49
 
98
- # an anchor defined by a non-empty fragment in the id uri
99
- # @return [String]
100
- def anchor
101
- if id
102
- id_uri = Util.uri(id)
103
- if id_uri.fragment == ''
104
- nil
105
- else
106
- id_uri.fragment
107
- end
108
- else
109
- nil
110
- end
111
- end
112
- end
113
-
114
- # @private
115
- module IntegerAllows0Fraction
116
- # is `value` an integer?
117
- # @private
118
- # @param value
119
50
  # @return [Boolean]
120
- def internal_integer?(value)
121
- value.is_a?(Integer) || (value.is_a?(Numeric) && value % 1.0 == 0.0)
51
+ def resolve_schema?
52
+ true
122
53
  end
123
- end
124
54
 
125
- # @private
126
- module IntegerDisallows0Fraction
127
- # is `value` an integer?
128
- # @private
129
- # @param value
130
- # @return [Boolean]
131
- def internal_integer?(value)
132
- value.is_a?(Integer)
55
+ # @deprecated after v0.8
56
+ def deref_schema
57
+ resolve
133
58
  end
134
59
  end
135
60
 
@@ -144,86 +69,80 @@ module JSI
144
69
  #
145
70
  # A schema is indicated as describing other schemas using the {Schema#describes_schema!} method.
146
71
  module MetaSchema
147
- # @return [Set<Module>]
148
- attr_reader(:schema_implementation_modules)
72
+ # @return [Schema::Dialect]
73
+ attr_reader(:described_dialect)
149
74
 
150
75
  # Instantiates the given schema content as a JSI Schema.
151
76
  #
152
- # By default, the schema will be registered with the {JSI.schema_registry}.
153
- # This can be controlled by params `register` and `schema_registry`.
77
+ # By default, the schema will be registered with the {JSI.registry}.
78
+ # This can be controlled by the `register` param and {Base::Conf#registry `registry`} {Base::Conf configuration}.
154
79
  #
155
80
  # By default, the `schema_content` will have any Symbol keys of Hashes replaced with Strings
156
81
  # (recursively through the document). This is controlled by the param `stringify_symbol_keys`.
157
82
  #
158
83
  # Schemas instantiated with `new_schema` are immutable, their content transformed using
159
- # the `to_immutable` param.
160
- #
161
- # @param schema_content an object to be instantiated as a JSI Schema - typically a Hash
162
- # @param uri [#to_str, Addressable::URI] The retrieval URI of the schema document.
163
- # If specified, the root schema will be identified by this URI, in addition
164
- # to any absolute URI declared with an id keyword, for resolution in the `schema_registry`.
165
- #
166
- # It is rare that this needs to be specified. Most schemas, if they use absolute URIs, will
167
- # use the `$id` keyword (`id` in draft 4) to specify this. A different retrieval URI is useful
168
- # in unusual cases:
84
+ # the {Base::Conf configured} {Base::Conf#to_immutable `to_immutable`}.
169
85
  #
170
- # - A schema in the document uses relative URIs for `$id` or `$ref` without an absolute id in an
171
- # ancestor schema - these will be resolved relative to this URI
172
- # - Another schema refers with `$ref` to the schema being instantiated by this retrieval URI,
173
- # rather than an id declared in the schema - the schema is resolvable by this URI in the
174
- # `schema_registry`.
175
- # @param register [Boolean] Whether the instantiated schema and any subschemas with absolute URIs
176
- # will be registered in the schema registry indicated by param `schema_registry`.
177
- # @param schema_registry [SchemaRegistry, nil] The registry this schema will use.
86
+ # Parameters are passed to {SchemaSet#new_jsi} and are documented there, but some have
87
+ # different defaults for new_schema.
178
88
  #
179
- # - The schema and subschemas will be registered here with any declared URI,
180
- # unless the `register` param is false.
181
- # - References from within the schema (typically from `$ref` keywords) are resolved using this registry.
182
- # @param stringify_symbol_keys [Boolean] Whether the schema content will have any Symbol keys of Hashes
183
- # replaced with Strings (recursively through the document).
184
- # Replacement is done on a copy; the given schema content is not modified.
185
- # @param to_immutable (see SchemaSet#new_jsi)
186
- # @yield If a block is given, it is evaluated in the context of the schema's JSI schema module
187
- # using [Module#module_exec](https://ruby-doc.org/core/Module.html#method-i-module_exec).
188
- # @return [JSI::Base subclass + JSI::Schema] a JSI which is a {JSI::Schema} whose content comes from
189
- # the given `schema_content` and whose schemas are this schema's inplace applicators.
89
+ # @param schema_content an object to be instantiated as a JSI Schema - typically a Hash
90
+ # @param base_uri
91
+ # @param register
92
+ # @param stringify_symbol_keys
93
+ # @param conf_kw (see SchemaSet#new_jsi)
94
+ # @return [Base + Schema] A JSI which is a {Schema} whose content comes from
95
+ # the given `schema_content` and whose schemas are this meta-schema's in-place applicators.
190
96
  def new_schema(schema_content,
191
- uri: nil,
97
+ base_uri: nil,
192
98
  register: true,
193
- schema_registry: JSI.schema_registry,
194
99
  stringify_symbol_keys: true,
195
- to_immutable: DEFAULT_CONTENT_TO_IMMUTABLE,
196
- &block
100
+ **conf_kw
197
101
  )
198
- schema_jsi = new_jsi(schema_content,
199
- uri: uri,
102
+ raise(BlockGivenError) if block_given?
103
+ new_jsi(schema_content,
104
+ base_uri: base_uri,
200
105
  register: register,
201
- schema_registry: schema_registry,
202
106
  stringify_symbol_keys: stringify_symbol_keys,
203
- to_immutable: to_immutable,
107
+ **conf_kw,
204
108
  mutable: false,
205
109
  )
206
-
207
- schema_jsi.jsi_schema_module_exec(&block) if block
208
-
209
- schema_jsi
210
110
  end
211
111
 
212
112
  # Instantiates the given schema content as a JSI Schema, passing all params to
213
113
  # {Schema::MetaSchema#new_schema}, and returns its {Schema#jsi_schema_module JSI Schema Module}.
214
114
  #
115
+ # @yield If a block is given, it is evaluated in the context of the schema module
116
+ # using [Module#module_exec](https://ruby-doc.org/core/Module.html#method-i-module_exec).
215
117
  # @return [JSI::SchemaModule] the JSI Schema Module of the instantiated schema
216
118
  def new_schema_module(schema_content, **kw, &block)
217
- new_schema(schema_content, **kw, &block).jsi_schema_module
119
+ schema_jsi = new_schema(schema_content, **kw)
120
+ schema_jsi.jsi_schema_module_exec(&block) if block
121
+ schema_jsi.jsi_schema_module
218
122
  end
219
123
  end
220
124
 
221
- class << self
125
+ # @private
126
+ module ExtendedInitialize
222
127
  def extended(o)
223
128
  super
224
129
  o.send(:jsi_schema_initialize)
225
130
  end
131
+
132
+ def included(m)
133
+ super
134
+ return if m.is_a?(Class)
135
+
136
+ # if a module (m) includes Schema, and an object (o) is extended with m,
137
+ # then o should have #jsi_schema_initialize called, but Schema.extended is not called,
138
+ # so m needs its own .extended method to call jsi_schema_initialize.
139
+ # note: extending m with ExtendedInitialize for .extended, rather than m.define_singleton_method(:extended),
140
+ # avoids possibly clobbering an existing singleton .extended method the module has defined.
141
+ m.extend(ExtendedInitialize)
142
+ end
226
143
  end
144
+
145
+ extend(ExtendedInitialize)
227
146
  end
228
147
 
229
148
  class << self
@@ -251,8 +170,9 @@ module JSI
251
170
  #
252
171
  # The meta-schema that describes the schema must be indicated:
253
172
  #
254
- # - If the schema object has a `$schema` property, that URI is resolved using the `schema_registry`
255
- # param (by default {JSI.schema_registry}), and that meta-schema is used. For example:
173
+ # - If the schema object has a `$schema` property, that URI is resolved using the
174
+ # {Base::Conf configured} {Base::Conf#registry `registry`} (by default {JSI.registry JSI.registry}),
175
+ # and that meta-schema is used. For example:
256
176
  #
257
177
  # ```ruby
258
178
  # JSI.new_schema({
@@ -283,45 +203,44 @@ module JSI
283
203
  # specifying a `default_metaschema` is to call `new_schema` on the
284
204
  # {Schema::MetaSchema#new_schema meta-schema} or its
285
205
  # {SchemaModule::MetaSchemaModule#new_schema schema module}, e.g.
286
- # `JSI::JSONSchemaDraft07.new_schema(my_schema_content)`
206
+ # `JSI::JSONSchemaDraft07.new_schema(my_schema_content)`. This will ignore any `$schema` keyword
207
+ # that may be present.
287
208
  #
288
209
  # Schemas instantiated with `new_schema` are immutable, their content transformed using
289
- # the `to_immutable` param.
210
+ # the {Base::Conf configured} {Base::Conf#to_immutable `to_immutable`}.
211
+ #
212
+ # Most parameters are passed to {SchemaSet#new_jsi} and are documented there, but some have
213
+ # different defaults for JSI.new_schema.
290
214
  #
291
215
  # @param schema_content (see Schema::MetaSchema#new_schema)
292
216
  # @param default_metaschema [Schema::MetaSchema, SchemaModule::MetaSchemaModule, #to_str]
293
217
  # Indicates the meta-schema to use if the given `schema_content` does not have a `$schema` property.
294
218
  # This may be a meta-schema or a meta-schema's schema module (e.g. `JSI::JSONSchemaDraft07`),
295
219
  # or a URI (as would be in a `$schema` keyword).
296
- # @param uri (see Schema::MetaSchema#new_schema)
297
- # @param register (see Schema::MetaSchema#new_schema)
298
- # @param schema_registry (see Schema::MetaSchema#new_schema)
299
- # @param stringify_symbol_keys (see Schema::MetaSchema#new_schema)
300
- # @param to_immutable (see Schema::DescribesSchema#new_schema)
301
- # @yield (see Schema::MetaSchema#new_schema)
302
- # @return [JSI::Base subclass + JSI::Schema] a JSI which is a {JSI::Schema} whose content comes from
303
- # the given `schema_content` and whose schemas are inplace applicators of the indicated meta-schema
220
+ # @param base_uri
221
+ # @param register
222
+ # @param stringify_symbol_keys
223
+ # @param conf_kw (see SchemaSet#new_jsi)
224
+ # @return [Base + Schema] A JSI which is a {Schema} whose content comes from
225
+ # the given `schema_content` and whose schemas are in-place applicators of the indicated meta-schema.
304
226
  def new_schema(schema_content,
305
227
  default_metaschema: nil,
306
- # params of Schema::MetaSchema#new_schema have their default values repeated here. delegating in a splat
307
- # would remove repetition, but yard doesn't display delegated defaults with its (see X) directive.
308
- uri: nil,
228
+ base_uri: nil,
309
229
  register: true,
310
- schema_registry: JSI.schema_registry,
311
230
  stringify_symbol_keys: true,
312
- to_immutable: DEFAULT_CONTENT_TO_IMMUTABLE,
313
- &block
231
+ **conf_kw
314
232
  )
233
+ raise(BlockGivenError) if block_given?
315
234
  new_schema_params = {
316
- uri: uri,
235
+ base_uri: base_uri,
317
236
  register: register,
318
- schema_registry: schema_registry,
319
237
  stringify_symbol_keys: stringify_symbol_keys,
320
- to_immutable: to_immutable,
238
+ **conf_kw,
321
239
  }
240
+ conf = Base::Conf.new(**conf_kw) # some redundancy instantiating this - not passed to MetaSchema#new_schema, just used in this method
322
241
  default_metaschema_new_schema = -> {
323
242
  default_metaschema = if default_metaschema
324
- Schema.ensure_metaschema(default_metaschema, name: "default_metaschema")
243
+ Schema.ensure_metaschema(default_metaschema, name: "default_metaschema", registry: conf.registry)
325
244
  elsif self.default_metaschema
326
245
  self.default_metaschema
327
246
  else
@@ -336,7 +255,7 @@ module JSI
336
255
  "instantiating schema_content: #{schema_content.pretty_inspect.chomp}",
337
256
  ].join("\n"))
338
257
  end
339
- default_metaschema.new_schema(schema_content, **new_schema_params, &block)
258
+ default_metaschema.new_schema(schema_content, **new_schema_params)
340
259
  }
341
260
  if schema_content.is_a?(Schema)
342
261
  raise(TypeError, [
@@ -354,8 +273,8 @@ module JSI
354
273
  unless id.respond_to?(:to_str)
355
274
  raise(ArgumentError, "given schema_content keyword `$schema` is not a string")
356
275
  end
357
- metaschema = Schema.ensure_metaschema(id, name: '$schema', schema_registry: schema_registry)
358
- metaschema.new_schema(schema_content, **new_schema_params, &block)
276
+ metaschema = Schema.ensure_metaschema(id, name: '$schema', registry: conf.registry)
277
+ metaschema.new_schema(schema_content, **new_schema_params)
359
278
  else
360
279
  default_metaschema_new_schema.call
361
280
  end
@@ -372,11 +291,10 @@ module JSI
372
291
  # ensure the given object is a JSI Schema
373
292
  #
374
293
  # @param schema [Object] the thing the caller wishes to ensure is a Schema
375
- # @param msg [#to_s, #to_ary] lines of the error message preceding the pretty-printed schema param
376
- # if the schema param is not a schema
294
+ # @yieldreturn [#to_s, #to_ary] first line(s) of the error message, overriding the default
377
295
  # @raise [NotASchemaError] if the schema param is not a schema
378
296
  # @return [Schema] the given schema
379
- def ensure_schema(schema, msg: "indicated object is not a schema:", reinstantiate_as: nil)
297
+ def ensure_schema(schema, reinstantiate_as: nil)
380
298
  if schema.is_a?(Schema)
381
299
  schema
382
300
  else
@@ -384,27 +302,33 @@ module JSI
384
302
  # TODO warn; behavior is undefined and I hate this implementation
385
303
 
386
304
  result_schema_indicated_schemas = SchemaSet.new(schema.jsi_indicated_schemas + reinstantiate_as)
387
- result_schema_applied_schemas = result_schema_indicated_schemas.inplace_applicator_schemas(schema.jsi_node_content)
305
+ result_schema_applied_schemas = result_schema_indicated_schemas.each_yield_set do |is, y|
306
+ is.each_inplace_applicator_schema(schema.jsi_node_content, &y)
307
+ end
388
308
 
389
309
  result_schema_class = JSI::SchemaClasses.class_for_schemas(result_schema_applied_schemas,
390
310
  includes: SchemaClasses.includes_for(schema.jsi_node_content),
391
311
  mutable: schema.jsi_mutable?,
392
312
  )
393
313
 
394
- result_schema_class.new(schema.jsi_document,
314
+ result_schema_class.new(
315
+ jsi_document: schema.jsi_document,
395
316
  jsi_ptr: schema.jsi_ptr,
396
317
  jsi_indicated_schemas: result_schema_indicated_schemas,
397
- jsi_schema_base_uri: schema.jsi_schema_base_uri,
318
+ jsi_base_uri: schema.jsi_base_uri,
398
319
  jsi_schema_resource_ancestors: schema.jsi_schema_resource_ancestors,
399
- jsi_schema_registry: schema.jsi_schema_registry,
400
- jsi_content_to_immutable: schema.jsi_content_to_immutable,
401
- jsi_root_node: schema.jsi_ptr.root? ? nil : schema.jsi_root_node, # bad
402
- )
320
+ jsi_schema_dynamic_anchor_map: schema.jsi_schema_dynamic_anchor_map,
321
+ jsi_conf: schema.equal?(schema.jsi_root_node) ? schema.jsi_conf : nil,
322
+ jsi_root_node: schema.equal?(schema.jsi_root_node) ? nil : schema.jsi_root_node, # bad
323
+ ).send(:jsi_initialized)
403
324
  else
404
- raise(NotASchemaError, [
405
- *msg,
406
- schema.pretty_inspect.chomp,
407
- ].join("\n"))
325
+ msg = []
326
+ msg.concat([*(block_given? ? yield : "indicated object is not a schema:")])
327
+ msg << schema.pretty_inspect.chomp
328
+ if schema.is_a?(Base)
329
+ msg << "its schemas (which should include a Meta-Schema): #{schema.jsi_schemas.pretty_inspect.chomp}"
330
+ end
331
+ raise(NotASchemaError, msg.compact.join("\n"))
408
332
  end
409
333
  end
410
334
  end
@@ -415,11 +339,11 @@ module JSI
415
339
  # @param metaschema [Schema::MetaSchema, SchemaModule::MetaSchemaModule, #to_str]
416
340
  # @raise [TypeError] if the param does not indicate a meta-schema
417
341
  # @return [Base + Schema + Schema::MetaSchema]
418
- def ensure_metaschema(metaschema, name: nil, schema_registry: JSI.schema_registry)
342
+ def ensure_metaschema(metaschema, name: nil, registry: JSI.registry)
419
343
  if metaschema.respond_to?(:to_str)
420
- schema = Schema::Ref.new(metaschema, schema_registry: schema_registry).deref_schema
344
+ schema = Schema::Ref.new(metaschema, registry: registry).resolve
421
345
  if !schema.describes_schema?
422
- raise(TypeError, [name, "URI indicates a schema that is not a meta-schema: #{metaschema.pretty_inspect.chomp}"].compact.join(" "))
346
+ raise(NotAMetaSchemaError, [name, "URI indicates a schema that is not a meta-schema: #{metaschema.pretty_inspect.chomp}"].compact.join(" "))
423
347
  end
424
348
  schema
425
349
  elsif metaschema.is_a?(SchemaModule::MetaSchemaModule)
@@ -427,7 +351,7 @@ module JSI
427
351
  elsif metaschema.is_a?(Schema::MetaSchema)
428
352
  metaschema
429
353
  else
430
- raise(TypeError, "#{name || "param"} does not indicate a meta-schema: #{metaschema.pretty_inspect.chomp}")
354
+ raise(NotAMetaSchemaError, "#{name || "param"} does not indicate a meta-schema: #{metaschema.pretty_inspect.chomp}")
431
355
  end
432
356
  end
433
357
  end
@@ -444,6 +368,12 @@ module JSI
444
368
  end
445
369
  end
446
370
 
371
+ # @!method dialect
372
+ # The dialect of this schema
373
+ # @return [Schema::Dialect]
374
+ # note: defined on a meta-schema's schema module by Schema#describes_schema!
375
+
376
+
447
377
  # the underlying JSON data used to instantiate this JSI::Schema.
448
378
  # this is an alias for {Base#jsi_node_content}, named for clarity in the context of working with
449
379
  # a schema.
@@ -458,83 +388,93 @@ module JSI
458
388
  schema_content.respond_to?(:to_hash) && schema_content.key?(keyword)
459
389
  end
460
390
 
461
- # the URI of this schema, calculated from our `#id`, resolved against our `#jsi_schema_base_uri`
462
- # @return [Addressable::URI, nil]
391
+ # Does this schema contain the given keyword with the given value?
392
+ # @return [Boolean]
393
+ def keyword_value?(keyword, value)
394
+ keyword?(keyword) && schema_content[keyword] == value
395
+ end
396
+
397
+ # the string contents of an `$id`/`id` keyword, or nil
398
+ # @return [#to_str, nil]
399
+ def id
400
+ dialect_invoke_each(:id).first
401
+ end
402
+
403
+ # @return [Enumerable<String>]
404
+ def anchors
405
+ anchors = Set[]
406
+ anchors.merge(dialect_invoke_each(:anchor))
407
+ anchors.merge(dialect_invoke_each(:dynamicAnchor))
408
+ anchors.freeze
409
+ end
410
+
411
+ # the URI of this schema, from an `$id` keyword, resolved against our `#jsi_base_uri`
412
+ # @deprecated after v0.8 - use `#jsi_resource_uri`
413
+ # @return [URI, nil]
463
414
  def schema_absolute_uri
464
- if respond_to?(:id_without_fragment) && id_without_fragment
465
- if jsi_schema_base_uri
466
- jsi_schema_base_uri.join(id_without_fragment).freeze
415
+ jsi_resource_uri
416
+ end
417
+
418
+ # @deprecated after v0.8 - use `#jsi_resource_uris`
419
+ # @return [Enumerable<URI>]
420
+ def schema_absolute_uris
421
+ jsi_resource_uris
422
+ end
423
+
424
+ # @yield [URI]
425
+ private def jsi_each_resource_uri_compute
426
+ dialect_invoke_each(:id_without_fragment) do |id_without_fragment|
427
+ if jsi_base_uri
428
+ yield(jsi_base_uri.join(id_without_fragment))
467
429
  elsif id_without_fragment.absolute?
468
- id_without_fragment
469
- else
470
- # TODO warn / schema_error
471
- nil
430
+ yield(id_without_fragment)
472
431
  end
473
432
  end
433
+ super
474
434
  end
475
435
 
476
436
  # a nonrelative URI which refers to this schema.
477
437
  # `nil` if no ancestor of this schema defines an id.
478
438
  # see {#schema_uris} for all URIs known to refer to this schema.
479
- # @return [Addressable::URI, nil]
439
+ # @return [URI, nil]
480
440
  def schema_uri
481
441
  schema_uris.first
482
442
  end
483
443
 
484
444
  # nonrelative URIs (that is, absolute, but possibly with a fragment) which refer to this schema
485
- # @return [Array<Addressable::URI>]
445
+ # @return [Array<URI>]
486
446
  def schema_uris
487
- @schema_uris_map[]
488
- end
489
-
490
- private def schema_uris_compute(**_) # TODO remove **_ eventually (keyword argument compatibility)
491
- each_schema_uri.to_a
447
+ @schema_uris_map[schema_content: schema_content]
492
448
  end
493
449
 
494
- # see {#schema_uris}
495
- # @yield [Addressable::URI]
496
- # @return [Enumerator, nil]
497
- def each_schema_uri
498
- return to_enum(__method__) unless block_given?
499
-
500
- yield schema_absolute_uri if schema_absolute_uri
501
-
502
- ancestor_schemas = jsi_subschema_resource_ancestors.reverse_each.select do |resource|
503
- resource.schema_absolute_uri
504
- end
450
+ # @yield [URI]
451
+ private def schema_uris_compute(&block)
452
+ jsi_resource_uris.each(&block)
505
453
 
506
- anchored = respond_to?(:anchor) ? anchor : nil
507
- ancestor_schemas.each do |ancestor_schema|
508
- if anchored
509
- if ancestor_schema.jsi_anchor_subschema(anchor) == self
510
- yield(ancestor_schema.schema_absolute_uri.merge(fragment: anchor).freeze)
511
- else
512
- anchored = false
454
+ if jsi_resource_root
455
+ anchors.each do |anchor|
456
+ jsi_resource_root.jsi_resource_uris.each do |uri|
457
+ yield(uri.merge(fragment: anchor))
513
458
  end
514
459
  end
460
+ end
515
461
 
462
+ jsi_subschema_resource_ancestors.reverse_each do |ancestor_schema|
516
463
  relative_ptr = jsi_ptr.relative_to(ancestor_schema.jsi_ptr)
517
- yield(ancestor_schema.schema_absolute_uri.merge(fragment: relative_ptr.fragment).freeze)
464
+ ancestor_schema.jsi_resource_uris.each do |uri|
465
+ yield(uri.merge(fragment: relative_ptr.fragment))
466
+ end
518
467
  end
519
468
 
520
469
  nil
521
470
  end
522
471
 
523
- # a module which extends all instances of this schema. this may be opened by the application to add
524
- # methods to schema instances.
525
- #
526
- # some functionality is also defined on the module itself (its singleton class, not for its instances):
527
- #
528
- # - the module is extended with {JSI::SchemaModule}, which defines .new_jsi to instantiate instances
529
- # of this schema (see {#new_jsi}).
530
- # - properties described by this schema's metaschema are defined as methods to get subschemas' schema
531
- # modules, so for example `schema.jsi_schema_module.items` returns the same module
532
- # as `schema.items.jsi_schema_module`.
533
- # - method .schema which returns this schema.
472
+ # The {SchemaModule JSI Schema Module} for this schema.
473
+ # JSI instances described by this schema are instances of this module.
534
474
  #
535
475
  # @return [SchemaModule]
536
476
  def jsi_schema_module
537
- JSI::SchemaClasses.module_for_schema(self)
477
+ jsi_schema_module_connection
538
478
  end
539
479
 
540
480
  # Evaluates the given block in the context of this schema's JSI schema module.
@@ -547,28 +487,40 @@ module JSI
547
487
  jsi_schema_module.module_exec(*a, **kw, &block)
548
488
  end
549
489
 
490
+ # @return [String, nil]
491
+ def jsi_schema_module_name
492
+ # don't hit #jsi_schema_module - avoid creating module, avoid erroring for MSN::BootstrapSchema
493
+ @memos[:schema_module_connection] && @memos[:schema_module_connection].name
494
+ end
495
+
496
+ # @return [String, nil]
497
+ def jsi_schema_module_name_from_ancestor
498
+ is_a?(Base) ? jsi_schema_module.name_from_ancestor : nil
499
+ end
500
+
550
501
  # Instantiates a new JSI whose content comes from the given `instance` param.
551
- # This schema indicates the schemas of the JSI - its schemas are inplace
502
+ # This schema indicates the schemas of the JSI - its schemas are in-place
552
503
  # applicators of this schema which apply to the given instance.
553
504
  #
554
- # @param (see SchemaSet#new_jsi)
555
- # @return [JSI::Base subclass] a JSI whose content comes from the given instance and whose schemas are
556
- # inplace applicators of this schema.
505
+ # All parameters are passed to {SchemaSet#new_jsi}.
506
+ #
507
+ # @return [Base] a JSI whose content comes from the given instance and whose schemas are
508
+ # in-place applicators of this schema.
557
509
  def new_jsi(instance, **kw)
510
+ raise(BlockGivenError) if block_given?
558
511
  SchemaSet[self].new_jsi(instance, **kw)
559
512
  end
560
513
 
561
- # @param keyword schema keyword e.g. "$ref", "$schema"
514
+ # @param ref [#to_str] ref URI
562
515
  # @return [Schema::Ref]
563
- def schema_ref(keyword = "$ref")
564
- raise(ArgumentError, "keyword not present: #{keyword}") unless keyword?(keyword)
565
- @schema_ref_map[keyword: keyword, value: schema_content[keyword]]
516
+ def schema_ref(ref = schema_content["$ref"])
517
+ @schema_ref_map[ref]
566
518
  end
567
519
 
568
520
  # Does this schema itself describe a schema? I.e. is this schema a meta-schema?
569
521
  # @return [Boolean]
570
522
  def describes_schema?
571
- jsi_schema_module <= JSI::Schema || false
523
+ is_a?(Schema::MetaSchema)
572
524
  end
573
525
 
574
526
  # Is this a JSI Schema?
@@ -580,30 +532,36 @@ module JSI
580
532
  # Indicates that this schema describes schemas, i.e. it is a meta-schema.
581
533
  # this schema is extended with {Schema::MetaSchema} and its {#jsi_schema_module} is extended
582
534
  # with {SchemaModule::MetaSchemaModule}, and the JSI Schema Module will include
583
- # JSI::Schema and the given modules.
535
+ # JSI::Schema.
584
536
  #
585
- # @param schema_implementation_modules [Enumerable<Module>] modules which implement the functionality of
586
- # the schema to extend schemas described by this schema.
537
+ # @param dialect [Schema::Dialect, nil] dialect may be passed, or inferred from `$vocabulary`
587
538
  # @return [void]
588
- def describes_schema!(schema_implementation_modules)
589
- schema_implementation_modules = Util.ensure_module_set(schema_implementation_modules)
539
+ def describes_schema!(dialect = nil)
540
+ # TODO rm bridge code hax
541
+ dialect = dialect.first::DIALECT if dialect.is_a?(Array) && dialect.size == 1
542
+
543
+ if !dialect
544
+ raise(ArgumentError, "no dialect given and no $vocabulary hash/object") if !schema_content['$vocabulary'].respond_to?(:to_hash)
545
+ dialect = Schema::Dialect.from_xvocabulary(schema_content['$vocabulary'], registry: jsi_registry)
546
+ end
547
+
548
+ raise(TypeError) if !dialect.is_a?(Schema::Dialect)
590
549
 
591
- if describes_schema?
592
- # this schema, or one equal to it, has already had describes_schema! called on it.
550
+ if jsi_schema_module <= Schema
551
+ # this schema has already had describes_schema! called on it.
593
552
  # this is to be avoided, but is not particularly a problem.
594
- # it is a bug if it was called different times with different schema_implementation_modules, though.
595
- unless jsi_schema_module.schema_implementation_modules == schema_implementation_modules
596
- raise(ArgumentError, "this schema already describes a schema with different schema_implementation_modules")
553
+ # it is a bug if it was called different times with different dialect, though.
554
+ if @described_dialect != dialect
555
+ raise(ArgumentError, "this schema already describes a schema with different dialect")
597
556
  end
598
557
  else
599
558
  jsi_schema_module.include(Schema)
600
- schema_implementation_modules.each do |mod|
601
- jsi_schema_module.include(mod)
602
- end
559
+ jsi_schema_module.send(:define_method, :dialect) { dialect }
560
+ proc { |metaschema| jsi_schema_module.send(:define_method, :metaschema) { metaschema } }[self]
603
561
  jsi_schema_module.extend(SchemaModule::MetaSchemaModule)
604
562
  end
605
563
 
606
- @schema_implementation_modules = schema_implementation_modules
564
+ @described_dialect = dialect
607
565
  extend(Schema::MetaSchema)
608
566
 
609
567
  nil
@@ -617,15 +575,21 @@ module JSI
617
575
  # If no ancestor schema has an absolute uri, the schema_resource_root is the {Base#jsi_root_node document's root node}.
618
576
  # In this case, the resource root may or may not be a schema itself.
619
577
  #
578
+ # @deprecated after v0.8
620
579
  # @return [JSI::Base] resource containing this schema
621
580
  def schema_resource_root
622
- jsi_subschema_resource_ancestors.last || jsi_root_node
581
+ jsi_resource_root
623
582
  end
624
583
 
625
584
  # is this schema the root of a schema resource?
626
585
  # @return [Boolean]
586
+ def jsi_is_resource_root?
587
+ super || jsi_resource_uris.any?
588
+ end
589
+
590
+ # @deprecated after v0.8
627
591
  def schema_resource_root?
628
- jsi_ptr.root? || !!schema_absolute_uri
592
+ jsi_is_resource_root?
629
593
  end
630
594
 
631
595
  # a subschema of this Schema
@@ -633,78 +597,202 @@ module JSI
633
597
  # @param subptr [JSI::Ptr, #to_ary] a relative pointer, or array of tokens, pointing to the subschema
634
598
  # @return [JSI::Schema] the subschema at the location indicated by subptr. self if subptr is empty.
635
599
  def subschema(subptr)
636
- subptr = Ptr.ary_ptr(subptr)
637
- Schema.ensure_schema(jsi_descendent_node(subptr), msg: [
638
- "subschema is not a schema at pointer: #{subptr.pointer}"
639
- ])
600
+ Schema.ensure_schema(jsi_descendent_node(subptr)) { "subschema is not a schema at pointer: #{Ptr.ary_ptr(subptr).pointer}" }
640
601
  end
641
602
 
642
- # a schema in the same schema resource as this one (see {#schema_resource_root}) at the given
603
+ # A schema in the same schema resource as this one (see {Schema::SchemaAncestorNode#jsi_resource_root}) at the given
643
604
  # pointer relative to the root of the schema resource.
644
605
  #
645
606
  # @param ptr [JSI::Ptr, #to_ary] a pointer to a schema from our schema resource root
646
607
  # @return [JSI::Schema] the schema pointed to by ptr
647
608
  def resource_root_subschema(ptr)
648
- ptr = Ptr.ary_ptr(ptr)
649
- Schema.ensure_schema(schema_resource_root.jsi_descendent_node(ptr),
650
- reinstantiate_as: jsi_schemas.select(&:describes_schema?)
609
+ Schema.ensure_schema(jsi_resource_root.jsi_descendent_node(ptr),
610
+ reinstantiate_as: jsi_conf.reinstantiate_nonschemas && jsi_schemas.select(&:describes_schema?),
651
611
  )
652
612
  end
653
613
 
654
- # a set of inplace applicator schemas of this schema (from $ref, allOf, etc.) which apply to the
655
- # given instance.
656
- #
657
- # the returned set will contain this schema itself, unless this schema contains a $ref keyword.
658
- #
659
- # @param instance [Object] the instance to check any applicators against
660
- # @return [JSI::SchemaSet] matched applicator schemas
661
- def inplace_applicator_schemas(instance)
662
- SchemaSet.new(each_inplace_applicator_schema(instance))
614
+ # @yield [Schema]
615
+ def jsi_each_descendent_schema(&block)
616
+ return(to_enum(__method__)) unless block_given?
617
+
618
+ yield(self)
619
+ dialect_invoke_each(:subschema) { |ptr| subschema(ptr).jsi_each_descendent_schema(&block) }
620
+ end
621
+
622
+ # yields each descendent of this node (including itself) within the same resource that is a Schema
623
+ # @yield [Schema]
624
+ def jsi_each_descendent_schema_same_resource(&block)
625
+ return(to_enum(__method__)) unless block_given?
626
+
627
+ yield(self)
628
+ dialect_invoke_each(:subschema) do |ptr|
629
+ desc = subschema(ptr)
630
+ if !desc.jsi_is_resource_root?
631
+ desc.jsi_each_descendent_schema_same_resource(&block)
632
+ end
633
+ end
634
+ end
635
+
636
+ # @yield [Ptr]
637
+ def each_immediate_subschema_ptr
638
+ return(to_enum(__method__)) unless block_given?
639
+
640
+ dialect_invoke_each(:subschema) { |ptr| yield(Ptr.ary_ptr(ptr)) }
663
641
  end
664
642
 
665
- # yields each inplace applicator schema which applies to the given instance.
643
+ # @private
644
+ # @yield each in-place applicator schema and params yielded from action :inplace_applicate
645
+ def each_immediate_inplace_applicator_schema(
646
+ instance: ,
647
+ visited_refs: ,
648
+ collect_evaluated: ,
649
+ &block
650
+ )
651
+ # if collect_evaluated, applicators must validate the instance to set `evaluated`
652
+ if collect_evaluated || @inplace_application_requires_instance
653
+ dialect_invoke_each(:inplace_applicate, Cxt::InplaceApplication::WithInstance,
654
+ instance: instance,
655
+ visited_refs: visited_refs,
656
+ collect_evaluated: collect_evaluated,
657
+ &block
658
+ )
659
+ else
660
+ # memoize: if the instance is not used by any in-place applicator present in this schema,
661
+ # the schema can do in-place application once instead of for every instance,
662
+ # for a very substantial performance gain.
663
+ #
664
+ # :inplace_applicate yields (schema, **keywords)
665
+ # so @memos[:immediate_inplace_applicators] is a 2D Array of tuples (schema, keywords)
666
+ @memos[:immediate_inplace_applicators] ||= begin
667
+ immediate_inplace_applicators = []
668
+ dialect_invoke_each(:inplace_applicate, Cxt::InplaceApplication,
669
+ visited_refs: visited_refs,
670
+ ) do |s, **kw|
671
+ immediate_inplace_applicators.push([s, kw])
672
+ end
673
+ immediate_inplace_applicators.freeze
674
+ end
675
+
676
+ @memos[:immediate_inplace_applicators].each do |(s, kw)|
677
+ yield(s, **kw)
678
+ end
679
+ nil
680
+ end
681
+ end
682
+
683
+ # Yields each in-place applicator schema which applies to the given instance.
666
684
  #
667
- # @param instance (see #inplace_applicator_schemas)
685
+ # @param instance [Object] the instance to check any applicators against
668
686
  # @param visited_refs [Enumerable<JSI::Schema::Ref>]
669
687
  # @yield [JSI::Schema]
670
- # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
688
+ # @return [nil]
671
689
  def each_inplace_applicator_schema(
672
690
  instance,
673
691
  visited_refs: Util::EMPTY_ARY,
674
692
  &block
675
693
  )
676
- return to_enum(__method__, instance, visited_refs: visited_refs) unless block
677
-
678
- catch(:jsi_application_done) do
679
- internal_inplace_applicate_keywords(instance, visited_refs, &block)
694
+ each_immediate_inplace_applicator_schema(
695
+ instance: instance,
696
+ visited_refs: visited_refs,
697
+ collect_evaluated: false, # child application is not invoked so no evaluated children to collect
698
+ ) do |schema, ref: nil, applicate: true|
699
+ if schema.equal?(self) && !ref
700
+ yield(self)
701
+ elsif applicate
702
+ schema.each_inplace_applicator_schema(
703
+ instance,
704
+ visited_refs: Util.add_visited_ref(visited_refs, ref),
705
+ &block
706
+ )
707
+ end
680
708
  end
681
-
682
- nil
683
- end
684
-
685
- # a set of child applicator subschemas of this schema which apply to the child of the given instance
686
- # on the given token.
687
- #
688
- # @param token [Object] the array index or object property name for the child instance
689
- # @param instance [Object] the instance to check any child applicators against
690
- # @return [JSI::SchemaSet] child applicator subschemas of this schema for the given token
691
- # of the instance
692
- def child_applicator_schemas(token, instance)
693
- SchemaSet.new(each_child_applicator_schema(token, instance))
694
709
  end
695
710
 
696
711
  # yields each child applicator subschema (from properties, items, etc.) which applies to the child of
697
712
  # the given instance on the given token.
698
713
  #
699
- # @param (see #child_applicator_schemas)
714
+ # @param token [Object] the array index or object property name for the child instance
715
+ # @param instance [Object] the instance to check any child applicators against
700
716
  # @yield [JSI::Schema]
701
717
  # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
702
718
  def each_child_applicator_schema(token, instance, &block)
703
- return to_enum(__method__, token, instance) unless block
719
+ dialect_invoke_each(:child_applicate,
720
+ Cxt::ChildApplication,
721
+ instance: instance,
722
+ token: token,
723
+ collect_evaluated: false,
724
+ collect_evaluated_validate: false,
725
+ evaluated: false,
726
+ &block
727
+ )
728
+ end
729
+
730
+ # For each in-place applicator schema that applies to the given instance, yields each child applicator
731
+ # of that schema that applies to the child of the instance on the given token.
732
+ #
733
+ # This method handles collection of whether the child was evaluated by any applicator
734
+ # when that evaluation is needed by either this schema or the caller (per param `collect_evaluated`).
735
+ # This is relevant to schemas containing `unevaluatedProperties` or `unevaluatedItems`.
736
+ #
737
+ # @param token [Object] array index or hash/object property name
738
+ # @param instance [Object]
739
+ # @param collect_evaluated [Boolean] Does the caller need this method to collect successful child evaluation?
740
+ # Note: this method will still collect child evaluation if this schema needs it; this only needs to be
741
+ # passed true when called by an in-place applicator schema that needs it (i.e. contains `unevaluated*`).
742
+ # @param collect_evaluated_validate [Boolean] See {Base::Conf#application_collect_evaluated_validate}
743
+ # @yield [Schema]
744
+ # @return [Boolean] if `collect_evaluated` is true, whether the child was successfully evaluated
745
+ # by a child applicator schema. if `collect_evaluated` is false, undefined/void.
746
+ def each_inplace_child_applicator_schema(
747
+ token,
748
+ instance,
749
+ visited_refs: Util::EMPTY_ARY,
750
+ collect_evaluated: false,
751
+ collect_evaluated_validate: false,
752
+ &block
753
+ )
754
+ collect_evaluated ||= application_requires_evaluated
755
+ inplace_child_evaluated = false
756
+ applicate_self = false
704
757
 
705
- internal_child_applicate_keywords(token, instance, &block)
758
+ each_immediate_inplace_applicator_schema(
759
+ instance: instance,
760
+ visited_refs: visited_refs,
761
+ collect_evaluated: collect_evaluated,
762
+ ) do |schema, ref: nil, applicate: true|
763
+ if schema.equal?(self) && !ref
764
+ applicate_self = true
765
+ elsif applicate || (collect_evaluated && !inplace_child_evaluated)
766
+ schema_evaluated = schema.each_inplace_child_applicator_schema(
767
+ token,
768
+ instance,
769
+ visited_refs: Util.add_visited_ref(visited_refs, ref),
770
+ collect_evaluated: collect_evaluated && !inplace_child_evaluated,
771
+ collect_evaluated_validate: collect_evaluated_validate,
772
+ # the `if` keyword needs to yield to here because it does affect `evaluated`,
773
+ # but it does not applicate itself/its applicators, so does not yield to the given block.
774
+ &(applicate ? block : proc { })
775
+ )
776
+ inplace_child_evaluated ||= collect_evaluated && schema_evaluated && (!collect_evaluated_validate || schema.instance_valid?(instance))
777
+ end
778
+ end
706
779
 
707
- nil
780
+ if applicate_self
781
+ child_application = dialect.invoke(:child_applicate, Cxt::ChildApplication.new(
782
+ schema: self,
783
+ abort: false,
784
+ token: token,
785
+ instance: instance,
786
+ collect_evaluated: collect_evaluated,
787
+ collect_evaluated_validate: collect_evaluated_validate,
788
+ evaluated: inplace_child_evaluated,
789
+ block: block,
790
+ ))
791
+
792
+ child_application.evaluated
793
+ else
794
+ inplace_child_evaluated
795
+ end
708
796
  end
709
797
 
710
798
  # any object property names this schema indicates may be present on its instances.
@@ -712,24 +800,13 @@ module JSI
712
800
  # array of "required" property keys.
713
801
  # @return [Set]
714
802
  def described_object_property_names
715
- @described_object_property_names_map[]
716
- end
717
-
718
- private def described_object_property_names_compute(**_) # TODO remove **_ eventually (keyword argument compatibility)
719
- Set.new.tap do |property_names|
720
- if schema_content.respond_to?(:to_hash) && schema_content['properties'].respond_to?(:to_hash)
721
- property_names.merge(schema_content['properties'].keys)
722
- end
723
- if schema_content.respond_to?(:to_hash) && schema_content['required'].respond_to?(:to_ary)
724
- property_names.merge(schema_content['required'].to_ary)
725
- end
726
- end.freeze
803
+ @described_object_property_names_map[schema_content: schema_content]
727
804
  end
728
805
 
729
- # validates the given instance against this schema
806
+ # Validates the given instance against this schema, returning a result with each validation error.
730
807
  #
731
808
  # @param instance [Object] the instance to validate against this schema
732
- # @return [JSI::Validation::Result]
809
+ # @return [JSI::Validation::Result::Full]
733
810
  def instance_validate(instance)
734
811
  if instance.is_a?(SchemaAncestorNode)
735
812
  instance_ptr = instance.jsi_ptr
@@ -751,6 +828,15 @@ module JSI
751
828
  internal_validate_instance(Ptr[], instance, validate_only: true).valid?
752
829
  end
753
830
 
831
+ # Asserts that the given instance is valid against this schema.
832
+ # {JSI::Invalid} is raised if it is not.
833
+ #
834
+ # @raise [Invalid]
835
+ # @return [nil]
836
+ def instance_valid!(instance)
837
+ instance_validate(instance).valid!
838
+ end
839
+
754
840
  # validates the given instance against this schema
755
841
  #
756
842
  # @private
@@ -766,13 +852,14 @@ module JSI
766
852
  validate_only: false
767
853
  )
768
854
  if validate_only
769
- result = JSI::Validation::VALID
855
+ result = JSI::Validation::Result::Valid.new
770
856
  else
771
- result = JSI::Validation::FullResult.new
857
+ result = JSI::Validation::Result::Full.new
772
858
  end
773
859
  result_builder = result.class::Builder.new(
774
860
  result: result,
775
861
  schema: self,
862
+ abort: false,
776
863
  instance_ptr: instance_ptr,
777
864
  instance_document: instance_document,
778
865
  validate_only: validate_only,
@@ -780,43 +867,138 @@ module JSI
780
867
  )
781
868
 
782
869
  catch(:jsi_validation_result) do
783
- # note: true/false are not valid as schemas in draft 4; they are only values of
784
- # additionalProperties / additionalItems. since their behavior is undefined, though,
785
- # it's fine for them to behave the same as boolean schemas in later drafts.
786
- # I don't care about draft 4 to implement a different structuring for that.
787
- if schema_content == true
788
- # noop
789
- elsif schema_content == false
790
- result_builder.validate(false, 'instance is not valid against `false` schema')
791
- elsif schema_content.respond_to?(:to_hash)
792
- internal_validate_keywords(result_builder)
793
- else
794
- result_builder.schema_error('schema is not a boolean or a JSON object')
795
- end
870
+ dialect.invoke(:validate, result_builder)
871
+
796
872
  result
797
873
  end.freeze
798
874
  end
799
875
 
876
+ # See {Base#jsi_as_child_default_as_jsi}. true for Schema, including boolean schemas.
877
+ def jsi_as_child_default_as_jsi
878
+ true
879
+ end
880
+
881
+ # @param action_name [Symbol]
882
+ # @param cxt_class [Class]
883
+ # @yield
884
+ def dialect_invoke_each(
885
+ action_name,
886
+ cxt_class = Cxt::Block,
887
+ **cxt_param,
888
+ &block
889
+ )
890
+ return(to_enum(__method__, action_name, cxt_class, **cxt_param)) unless block_given?
891
+
892
+ cxt = cxt_class.new(
893
+ schema: self,
894
+ abort: false,
895
+ block: block,
896
+ **cxt_param,
897
+ )
898
+ dialect.invoke(action_name, cxt)
899
+
900
+ nil
901
+ end
902
+
800
903
  # schema resources which are ancestors of any subschemas below this schema.
801
904
  # this may include this schema if this is a schema resource root.
802
905
  # @api private
803
906
  # @return [Array<JSI::Schema>]
804
907
  def jsi_subschema_resource_ancestors
805
- if schema_resource_root?
908
+ if jsi_is_resource_root?
806
909
  jsi_schema_resource_ancestors.dup.push(self).freeze
807
910
  else
808
911
  jsi_schema_resource_ancestors
809
912
  end
810
913
  end
811
914
 
915
+ # @private
916
+ def jsi_next_schema_dynamic_anchor_map
917
+ return @memos[:next_schema_dynamic_anchor_map] if @memos.key?(:next_schema_dynamic_anchor_map)
918
+
919
+ if !dialect.elements.any? { |e| e.invokes?(:dynamicAnchor) }
920
+ return @memos[:next_schema_dynamic_anchor_map] = jsi_schema_dynamic_anchor_map
921
+ end
922
+ if !jsi_resource_root.is_a?(Schema)
923
+ # if the resource root is not a schema, then this schema does not add to dynamic_anchor_map.
924
+ # we could treat self as the anchor root, but that complicates DynamicAnchorMap#without_node.
925
+ return @memos[:next_schema_dynamic_anchor_map] = jsi_schema_dynamic_anchor_map
926
+ end
927
+
928
+ map = jsi_schema_dynamic_anchor_map
929
+
930
+ anchor_root = jsi_resource_root
931
+ descendent_schemas = [[anchor_root, Util::EMPTY_ARY]]
932
+
933
+ while !descendent_schemas.empty?
934
+ descendent_schema, ptrs = *descendent_schemas.shift
935
+
936
+ descendent_schema.dialect_invoke_each(:dynamicAnchor) do |anchor|
937
+ next if map.key?(anchor)
938
+ map = map.merge({
939
+ anchor => [anchor_root.jsi_with_schema_dynamic_anchor_map(Schema::DynamicAnchorMap::EMPTY), ptrs].freeze,
940
+ }).freeze
941
+ end
942
+
943
+ descendent_schema.each_immediate_subschema_ptr do |subptr|
944
+ # we want a schema at subptr to
945
+ # - check if it is a schema resource root
946
+ # - check for $dynamicAnchor
947
+ # can't use #subschema here (it would need to pass this method's result to instantiate the subschema);
948
+ # a minimal bootstrap schema is used instead.
949
+ # note: not using dialect.bootstrap_schema. this bootstrap is only used once, skip memoization.
950
+ descendent_subschema = MetaSchemaNode::BootstrapSchema.new(
951
+ dialect: dialect,
952
+ jsi_document: jsi_document,
953
+ jsi_ptr: descendent_schema.jsi_ptr + subptr,
954
+ # note: same as anchor_root.jsi_next_base_uri since we don't cross resource boundaries.
955
+ jsi_base_uri: descendent_schema.jsi_next_base_uri,
956
+ )
957
+ if !descendent_subschema.jsi_is_resource_root?
958
+ descendent_schemas.push([descendent_subschema, ptrs.dup.push(subptr).freeze])
959
+ end
960
+ end
961
+ end
962
+
963
+ @memos[:next_schema_dynamic_anchor_map] = map
964
+ end
965
+
966
+ # @private pending stronger stability of dynamic scope
967
+ def with_dynamic_scope_from(node)
968
+ node = node.jsi_node if node.is_a?(SchemaModule::Connection)
969
+ jsi_with_schema_dynamic_anchor_map(node.jsi_next_schema_dynamic_anchor_map)
970
+ end
971
+
972
+ # Does application require collection of evaluated children?
973
+ # (i.e. does the schema contain `unevaluatedItems` / `unevaluatedProperties`?)
974
+ # @private
975
+ # @return [Boolean]
976
+ attr_reader(:application_requires_evaluated)
977
+
978
+ # @private
979
+ # @return [#to_s, nil]
980
+ def jsi_schema_identifier(required: false)
981
+ name = jsi_schema_module_name_from_ancestor
982
+ return name if name
983
+ return schema_uri || (required ? jsi_ptr.uri : nil) if jsi_schema_dynamic_anchor_map.empty?
984
+ -"#{schema_uri || jsi_ptr.uri}#{jsi_schema_dynamic_anchor_map.anchor_schemas_identifier}"
985
+ end
986
+
812
987
  private
813
988
 
989
+ KEY_BY_NONE = proc { nil }
990
+
814
991
  def jsi_schema_initialize
815
- @schema_ref_map = jsi_memomap(key_by: proc { |i| i[:keyword] }) do |keyword: , value: |
816
- Schema::Ref.new(value, ref_schema: self)
992
+ # guard against being called twice on MetaSchemaNode, first from extend(Schema) then extend(jsi_schema_module) that includes Schema.
993
+ # both extends need to initialize for edge case of draft4's boolean schema that is not described by meta-schema.
994
+ instance_variable_defined?(:@jsi_schema_initialized) ? return : (@jsi_schema_initialized = true)
995
+ @schema_ref_map = Hash.new { |h, ref| h[ref] = Schema::Ref.new(ref, referrer: self) }
996
+ @schema_uris_map = jsi_memomap(key_by: KEY_BY_NONE) { to_enum(:schema_uris_compute).to_a.freeze }
997
+ @described_object_property_names_map = jsi_memomap(key_by: KEY_BY_NONE) do
998
+ Set.new(dialect_invoke_each(:described_object_property_names)).freeze
817
999
  end
818
- @schema_uris_map = jsi_memomap(&method(:schema_uris_compute))
819
- @described_object_property_names_map = jsi_memomap(&method(:described_object_property_names_compute))
1000
+ @application_requires_evaluated = dialect_invoke_each(:application_requires_evaluated).any?
1001
+ @inplace_application_requires_instance = dialect_invoke_each(:inplace_application_requires_instance).any?
820
1002
  end
821
1003
  end
822
1004
  end