jsi 0.8.1 → 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 +9 -0
  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 +520 -338
  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 +70 -48
  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/base.rb CHANGED
@@ -10,9 +10,10 @@ module JSI
10
10
  # These subclasses are generally intended to be ignored by applications using this library - the purpose
11
11
  # they serve is to include modules relevant to the instance. The modules these classes include are:
12
12
  #
13
- # - the {Schema#jsi_schema_module} of each schema which describes the instance
13
+ # - the {SchemaModule JSI Schema Module} of each schema that describes the instance
14
14
  # - {Base::HashNode}, {Base::ArrayNode}, or {Base::StringNode} if the instance is
15
15
  # a hash/object, array, or string
16
+ # - {Base::Immutable} or {Base::Mutable}
16
17
  # - Modules defining accessor methods for property names described by the schemas
17
18
  class Base
18
19
  autoload :ArrayNode, 'jsi/base/node'
@@ -22,39 +23,139 @@ module JSI
22
23
  autoload(:Immutable, 'jsi/base/mutability')
23
24
 
24
25
  include Schema::SchemaAncestorNode
26
+ include(Util::Pretty)
25
27
 
26
28
  # An exception raised when attempting to access a child of a node which cannot have children.
27
29
  # A complex node can have children, a simple node cannot.
28
30
  class SimpleNodeChildError < StandardError
29
31
  end
30
32
 
33
+ class ChildNotPresent < StandardError
34
+ end
35
+
36
+ conf_attrs = {
37
+ root_uri: {fingerprint: true },
38
+ registry: {fingerprint: true },
39
+ application_collect_evaluated_validate: {fingerprint: false},
40
+ reinstantiate_nonschemas: {fingerprint: false},
41
+ after_initialize: {fingerprint: false},
42
+ child_as_jsi: {fingerprint: false},
43
+ child_use_default: {fingerprint: false},
44
+ to_immutable: {fingerprint: false},
45
+ }.freeze
46
+ Conf = Struct.subclass(*conf_attrs.keys)
47
+ class Conf end
48
+ Conf::ATTRS = conf_attrs
49
+
50
+ # Configuration, shared across all nodes of a document. A JSI's {Base#jsi_conf}.
51
+ #
52
+ # Configuration parameters are set from `**conf_kw` params passed to {SchemaSet#new_jsi #new_jsi},
53
+ # {Schema::MetaSchema#new_schema #new_schema} and related methods.
54
+ #
55
+ # @!attribute root_uri
56
+ # A URI identifying the document root resource.
57
+ # References (e.g. a schema `$ref`) can resolve the resource with this URI.
58
+ #
59
+ # It is rare that this needs to be specified. Most resources that would be
60
+ # referenced are schemas that use the `$id` keyword to specify their URI.
61
+ # However, there are cases when a resource may be referenced using a retrieval URI
62
+ # that does not match the resource's `$id`, and `root_uri` enables resolution.
63
+ # @return [URI, nil]
64
+ # @!attribute registry
65
+ # The registry from which references are resolved.
66
+ # For schemas (or documents containing schemas), this is mainly used with `$ref` values.
67
+ # It is unused in instances that do not contain schemas.
68
+ #
69
+ # Default: {JSI.registry}
70
+ # @return [Registry, nil]
71
+ # @!attribute application_collect_evaluated_validate
72
+ # Shall schema application perform validation when collecting child evaluation
73
+ # (for `unevaluatedProperties`, `unevaluatedItems`)?
74
+ #
75
+ # A child should not be considered evaluated by a schema when it fails to validate[^1].
76
+ # This means that `unevaluatedItems` or `unevaluatedProperties` should
77
+ # only apply to a child if no other applicator schema validates the child.
78
+ # The computational cost of this validation is significant, however, and may be unacceptable for performance.
79
+ #
80
+ # Set to `true`, child evaluation will perform validation, and `unevaluated*` will applicate
81
+ # correctly, at some cost in CPU time.
82
+ #
83
+ # Set to `false`, a child will be considered evaluated when a child applicator schema applies to it,
84
+ # regardless of validity, which will result in an `unevaluated*` schema incorrectly failing to
85
+ # applicate when the child is not valid.
86
+ #
87
+ # The default is false. It is expected that application of `unevaluated*` schemas to such children
88
+ # is not typically relied on, so validation is not typically worth the cost of its computation.
89
+ #
90
+ # [^1]: (ref: the JSON Schema spec states, "Schema objects that produce a false assertion result MUST
91
+ # NOT produce any annotation results, whether from their own keywords or from keywords in subschemas.")
92
+ #
93
+ # Default: false
94
+ # @return [Boolean]
95
+ # @!attribute reinstantiate_nonschemas
96
+ # _private, not officially supported_. whether Schema#resource_root_subschema reinstantiates.
97
+ # @!attribute after_initialize
98
+ # _EXPERIMENTAL_ - a callback that is called with each JSI node in the document after the node is initialized.
99
+ # @return [#call, nil]
100
+ # @!attribute child_as_jsi
101
+ # Default value for {Base#jsi_as_child_default_as_jsi}.
102
+ # @return [Boolean]
103
+ # @!attribute child_use_default
104
+ # Default value for {Base#jsi_child_use_default_default}.
105
+ # @return [Boolean]
106
+ # @!attribute to_immutable
107
+ # A callable that transforms given instance content to an immutable (i.e. deeply frozen) object equal to it.
108
+ #
109
+ # Used when instantiating immutable JSIs and modified copies of them, so their content is immutable.
110
+ #
111
+ # If the instantiated JSI will be mutable, this is not used.
112
+ #
113
+ # Though not recommended, this may be nil with immutable JSIs if the instance content is otherwise
114
+ # guaranteed to be immutable, as well as any modified copies of the instance.
115
+ #
116
+ # Default: {DEFAULT_CONTENT_TO_IMMUTABLE}
117
+ # @return [#call, nil]
118
+ class Conf
119
+ def initialize(
120
+ root_uri: nil,
121
+ registry: JSI.registry,
122
+ application_collect_evaluated_validate: false,
123
+ child_as_jsi: false,
124
+ child_use_default: false,
125
+ to_immutable: DEFAULT_CONTENT_TO_IMMUTABLE,
126
+ **kw
127
+ )
128
+ super(
129
+ root_uri: Util.uri(root_uri, nnil: false, yabs: true),
130
+ registry: registry,
131
+ application_collect_evaluated_validate: application_collect_evaluated_validate,
132
+ child_as_jsi: child_as_jsi,
133
+ child_use_default: child_use_default,
134
+ to_immutable: to_immutable,
135
+ **kw,
136
+ )
137
+ freeze
138
+ end
139
+
140
+ # @private
141
+ # @return [Hash]
142
+ def for_fingerprint
143
+ to_h.select { |k, _| self.class::ATTRS.fetch(k).fetch(:fingerprint) }.freeze
144
+ end
145
+ end
146
+
31
147
  class << self
32
148
  # A string indicating the schema module name
33
149
  # and/or schema URI of each schema the class represents.
34
150
  # @return [String]
35
151
  def inspect
36
- if !respond_to?(:jsi_class_schemas)
37
- super
38
- else
39
- schema_names = jsi_class_schemas.map do |schema|
40
- mod_name = schema.jsi_schema_module.name_from_ancestor
41
- if mod_name && schema.schema_absolute_uri
42
- "#{mod_name} <#{schema.schema_absolute_uri}>"
43
- elsif mod_name
44
- mod_name
45
- elsif schema.schema_uri
46
- schema.schema_uri.to_s
47
- else
48
- schema.jsi_ptr.uri.to_s
49
- end
50
- end
51
-
52
- if schema_names.empty?
53
- "(JSI Schema Class for 0 schemas#{jsi_class_includes.map { |n| " + #{n}" }})"
54
- else
55
- -"(JSI Schema Class: #{(schema_names + jsi_class_includes.map(&:name)).join(' + ')})"
56
- end
152
+ return super unless respond_to?(:jsi_class_schemas)
153
+ schema_names = jsi_class_schemas.map do |schema|
154
+ mod_name = schema.jsi_schema_module_name_from_ancestor
155
+ next "#{mod_name} <#{schema.jsi_resource_uri}>" if mod_name && schema.jsi_resource_uri
156
+ mod_name || "<#{schema.schema_uri || schema.jsi_ptr.uri}>"
57
157
  end
158
+ "(#{[superclass, *schema_names, *jsi_class_includes].join(' + ')})"
58
159
  end
59
160
 
60
161
  def to_s
@@ -74,9 +175,9 @@ module JSI
74
175
  return super unless respond_to?(:jsi_class_schemas)
75
176
  alnum = proc { |id| (id % 36**4).to_s(36).rjust(4, '0').upcase }
76
177
  schema_names = jsi_class_schemas.map do |schema|
77
- named_ancestor_schema, tokens = schema.jsi_schema_module.send(:named_ancestor_schema_tokens)
78
- if named_ancestor_schema
79
- [named_ancestor_schema.jsi_schema_module.name, *tokens].join('_')
178
+ named_ancestor, tokens = schema.jsi_schema_module.send(:named_ancestor_tokens)
179
+ if named_ancestor
180
+ [named_ancestor.jsi_schema_module_connection.name, *tokens].join('_')
80
181
  elsif schema.schema_uri
81
182
  schema.schema_uri.to_s
82
183
  else
@@ -108,36 +209,43 @@ module JSI
108
209
  # @api private
109
210
  # @param jsi_document [Object] the document containing the instance
110
211
  # @param jsi_ptr [JSI::Ptr] a pointer pointing to the JSI's instance in the document
111
- # @param jsi_schema_base_uri [Addressable::URI] see {SchemaSet#new_jsi} param uri
212
+ # @param jsi_base_uri [URI, nil] see {SchemaSet#new_jsi} param `base_uri`
112
213
  # @param jsi_schema_resource_ancestors [Array<JSI::Base + JSI::Schema>]
113
- # @param jsi_root_node [JSI::Base] the JSI of the root of the document containing this JSI
114
- def initialize(jsi_document,
214
+ # @param jsi_schema_dynamic_anchor_map [Schema::DynamicAnchorMap]
215
+ # @param jsi_dynamic_root_map map of (ptr, dynamic_anchor_map) → (Base root node), shared across dynamic root nodes
216
+ # @param jsi_conf [Base::Conf]
217
+ # @param jsi_root_node [JSI::Base, nil]
218
+ def initialize(
219
+ jsi_document: ,
115
220
  jsi_ptr: Ptr[],
116
221
  jsi_indicated_schemas: ,
117
- jsi_schema_base_uri: nil,
222
+ jsi_base_uri: nil,
118
223
  jsi_schema_resource_ancestors: Util::EMPTY_ARY,
119
- jsi_schema_registry: ,
120
- jsi_content_to_immutable: ,
224
+ jsi_schema_dynamic_anchor_map: Schema::DynamicAnchorMap::EMPTY,
225
+ jsi_dynamic_root_map: nil,
226
+ jsi_conf: nil,
121
227
  jsi_root_node: nil
122
228
  )
123
- #chkbug raise(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)
124
-
125
- self.jsi_document = jsi_document
126
- self.jsi_ptr = jsi_ptr
127
- self.jsi_indicated_schemas = jsi_indicated_schemas
128
- self.jsi_schema_base_uri = jsi_schema_base_uri
229
+ #chkbug fail(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)
230
+
231
+ #chkbug fail(Bug) if !jsi_root_node ^ jsi_conf
232
+ @jsi_conf = jsi_conf = jsi_conf || jsi_root_node.jsi_conf
233
+ @jsi_document = jsi_document
234
+ #chkbug fail(Bug) unless jsi_ptr.is_a?(Ptr)
235
+ #chkbug fail(Bug) unless jsi_ptr.resolve_against(jsi_document).equal?(jsi_ptr)
236
+ @jsi_ptr = jsi_ptr
237
+ #chkbug fail(Bug) unless jsi_indicated_schemas.is_a?(SchemaSet)
238
+ @jsi_indicated_schemas = jsi_indicated_schemas
239
+ self.jsi_base_uri = jsi_base_uri
129
240
  self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors
130
- self.jsi_schema_registry = jsi_schema_registry
131
- @jsi_content_to_immutable = jsi_content_to_immutable
132
- if @jsi_ptr.root?
133
- #chkbug raise(Bug, "jsi_root_node specified for root JSI") if jsi_root_node
134
- @jsi_root_node = self
135
- else
136
- #chkbug raise(Bug, "jsi_root_node is not JSI::Base") if !jsi_root_node.is_a?(JSI::Base)
137
- #chkbug raise(Bug, "jsi_root_node ptr is not root") if !jsi_root_node.jsi_ptr.root?
138
- @jsi_root_node = jsi_root_node
139
- end
140
-
241
+ self.jsi_schema_dynamic_anchor_map = jsi_schema_dynamic_anchor_map
242
+ #chkbug fail(Bug) if jsi_root_node && jsi_dynamic_root_map
243
+ @jsi_dynamic_root_map = jsi_dynamic_root_map || (jsi_root_node ? jsi_root_node.jsi_dynamic_root_map : jsi_memomap(&method(:jsi_dynamic_root_compute)))
244
+ @jsi_root_node = jsi_root_node || self
245
+ @root_rel_ptr = @jsi_ptr.relative_to(@jsi_root_node.jsi_ptr)
246
+
247
+ # @memos does not freeze if/when the node freezes
248
+ @memos = {}
141
249
  jsi_memomaps_initialize
142
250
  jsi_mutability_initialize
143
251
 
@@ -150,7 +258,7 @@ module JSI
150
258
 
151
259
  # @!method jsi_schemas
152
260
  # The set of schemas that describe this instance.
153
- # These are the applicator schemas that apply to this instance, the result of inplace application
261
+ # These are the applicator schemas that apply to this instance, the result of in-place application
154
262
  # of our {#jsi_indicated_schemas}.
155
263
  # @return [JSI::SchemaSet]
156
264
  # note: defined on subclasses by JSI::SchemaClasses.class_for_schemas
@@ -163,13 +271,20 @@ module JSI
163
271
  # @return [JSI::Ptr]
164
272
  attr_reader :jsi_ptr
165
273
 
166
- # Comes from the param `to_immutable` of {SchemaSet#new_jsi} (or other `new_jsi` /
167
- # `new_schema` / `new_schema_module` method).
168
- # Immutable JSIs use this when instantiating a modified copy so its instance is also immutable.
169
- # @return [#call, nil]
170
- attr_reader(:jsi_content_to_immutable)
274
+ # See {SchemaSet#new_jsi} param `registry`
275
+ # @return [Registry, nil]
276
+ def jsi_registry
277
+ jsi_conf.registry
278
+ end
279
+
280
+ attr_reader(:jsi_dynamic_root_map)
281
+ protected(:jsi_dynamic_root_map)
282
+
283
+ # @return [Base::Conf]
284
+ attr_reader(:jsi_conf)
171
285
 
172
- # the JSI at the root of this JSI's document
286
+ # The root ancestor of this node. This is typically the document root, though
287
+ # it can be a different resource root when dynamic scope is overridden.
173
288
  # @return [JSI::Base]
174
289
  attr_reader :jsi_root_node
175
290
 
@@ -185,12 +300,12 @@ module JSI
185
300
  jsi_node_content
186
301
  end
187
302
 
188
- # the schemas indicated as describing this instance, prior to inplace application.
303
+ # The schemas indicated as describing this instance, prior to in-place application.
189
304
  #
190
- # this is different from {#jsi_schemas}, which are the inplace applicator schemas
305
+ # This is different from {#jsi_schemas}, which are the in-place applicator schemas
191
306
  # which describe this instance. for most purposes, `#jsi_schemas` is more relevant.
192
307
  #
193
- # `jsi_indicated_schemas` does not include inplace applicator schemas, such as the
308
+ # `jsi_indicated_schemas` does not include in-place applicator schemas, such as the
194
309
  # subschemas of `allOf`, whereas `#jsi_schemas` does.
195
310
  #
196
311
  # this does include indicated schemas which do not apply themselves, such as `$ref`
@@ -209,7 +324,22 @@ module JSI
209
324
  # @yield [JSI::Base] each descendent node, starting with self
210
325
  # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
211
326
  def jsi_each_descendent_node(propertyNames: false, &block)
212
- return to_enum(__method__, propertyNames: propertyNames) unless block
327
+ unless block
328
+ return to_enum(__method__, propertyNames: propertyNames) do
329
+ # size
330
+ Util.ycomb do |rec|
331
+ proc do |node|
332
+ if node.respond_to?(:to_hash)
333
+ node.to_hash.inject(1) { |c, (k, child)| c + rec[child] + (propertyNames ? rec[k] : 0) }
334
+ elsif node.respond_to?(:to_ary)
335
+ node.to_ary.inject(1) { |c, child| c + rec[child] }
336
+ else
337
+ 1
338
+ end
339
+ end
340
+ end[jsi_node_content]
341
+ end
342
+ end
213
343
 
214
344
  yield self
215
345
 
@@ -226,6 +356,32 @@ module JSI
226
356
  nil
227
357
  end
228
358
 
359
+ # yields each descendent of this node that is a JSI Schema
360
+ # @yield [Base + Schema]
361
+ # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
362
+ def jsi_each_descendent_schema(&block)
363
+ return(to_enum(__method__)) unless block_given?
364
+
365
+ # note: this never yields self; if self is a Schema, Schema#jsi_each_descendent_schema overrides this method
366
+ jsi_each_child_token do |token|
367
+ jsi_child_node(token).jsi_each_descendent_schema(&block)
368
+ end
369
+ end
370
+
371
+ # yields each descendent of this node within the same resource that is a Schema
372
+ # @yield [Schema]
373
+ def jsi_each_descendent_schema_same_resource(&block)
374
+ return(to_enum(__method__)) unless block_given?
375
+
376
+ jsi_each_child_token do |token|
377
+ child = jsi_child_node(token)
378
+ if !child.jsi_is_resource_root?
379
+ # note: if child is a Schema, Schema#jsi_each_descendent_schema_same_resource overrides Base
380
+ child.jsi_each_descendent_schema_same_resource(&block)
381
+ end
382
+ end
383
+ end
384
+
229
385
  # recursively selects descendent nodes of this JSI, returning a modified copy of self containing only
230
386
  # descendent nodes for which the given block had a true-ish result.
231
387
  #
@@ -286,24 +442,24 @@ module JSI
286
442
  end
287
443
  end
288
444
 
289
- # an array of JSI instances above this one in the document.
445
+ # JSI nodes above this one in the document.
290
446
  #
291
447
  # @return [Array<JSI::Base>]
292
448
  def jsi_parent_nodes
293
- parent = jsi_root_node
294
-
295
- jsi_ptr.tokens.map do |token|
296
- parent.tap do
297
- parent = parent.jsi_child_node(token)
298
- end
299
- end.reverse!.freeze
449
+ parent_nodes = []
450
+ ptr = @root_rel_ptr
451
+ while !ptr.root?
452
+ ptr = ptr.parent
453
+ parent_nodes.push(jsi_root_node.jsi_descendent_node(ptr))
454
+ end
455
+ parent_nodes.freeze
300
456
  end
301
457
 
302
458
  # the immediate parent of this JSI. nil if there is no parent.
303
459
  #
304
460
  # @return [JSI::Base, nil]
305
461
  def jsi_parent_node
306
- jsi_ptr.root? ? nil : jsi_root_node.jsi_descendent_node(jsi_ptr.parent)
462
+ @root_rel_ptr.root? ? nil : jsi_root_node.jsi_descendent_node(@root_rel_ptr.parent)
307
463
  end
308
464
 
309
465
  # ancestor JSI instances from this node up to the root. this node itself is always its own first ancestor.
@@ -314,7 +470,7 @@ module JSI
314
470
  ancestor = jsi_root_node
315
471
  ancestors << ancestor
316
472
 
317
- jsi_ptr.tokens.each do |token|
473
+ @root_rel_ptr.tokens.each do |token|
318
474
  ancestor = ancestor.jsi_child_node(token)
319
475
  ancestors << ancestor
320
476
  end
@@ -326,11 +482,11 @@ module JSI
326
482
  # @param ptr [JSI::Ptr, #to_ary]
327
483
  # @return [JSI::Base]
328
484
  def jsi_descendent_node(ptr)
329
- descendent = Ptr.ary_ptr(ptr).evaluate(self, as_jsi: true)
330
- descendent
485
+ tokens = Ptr.ary_ptr(ptr).resolve_against(jsi_node_content).tokens
486
+ tokens.inject(self, &:jsi_child_node)
331
487
  end
332
488
 
333
- # A shorthand alias for {#jsi_descendent_node}.
489
+ # The descendent node at the given {Ptr}, token array, or pointer string.
334
490
  #
335
491
  # Note that, though more convenient to type, using an operator whose meaning may not be intuitive
336
492
  # to a reader could impair readability of code.
@@ -339,13 +495,14 @@ module JSI
339
495
  #
340
496
  # my_jsi / ['foo', 'bar']
341
497
  # my_jsi / %w(foo bar)
498
+ # my_jsi / '/foo/bar'
342
499
  # my_schema / JSI::Ptr['additionalProperties']
343
500
  # my_schema / %w(properties foo items additionalProperties)
344
501
  #
345
- # @param (see #jsi_descendent_node)
502
+ # @param ptr [JSI::Ptr, #to_ary, #to_str]
346
503
  # @return (see #jsi_descendent_node)
347
504
  def /(ptr)
348
- jsi_descendent_node(ptr)
505
+ jsi_descendent_node(ptr.respond_to?(:to_str) ? Ptr.from_pointer(ptr) : ptr)
349
506
  end
350
507
 
351
508
  # yields each token (array index or hash key) identifying a child node.
@@ -367,11 +524,20 @@ module JSI
367
524
  #
368
525
  # @param token [String, Integer]
369
526
  # @return [Boolean]
370
- def jsi_child_token_in_range?(token)
527
+ def jsi_child_token_present?(token)
371
528
  # note: overridden by Base::HashNode, Base::ArrayNode
372
529
  false
373
530
  end
374
531
 
532
+ # @api private
533
+ # @return [nil]
534
+ def jsi_child_ensure_present(token)
535
+ if !jsi_child_token_present?(token)
536
+ raise(ChildNotPresent, -"token does not identify a child that is present: #{token.inspect}\nself = #{pretty_inspect.chomp}")
537
+ end
538
+ nil
539
+ end
540
+
375
541
  # The child of the {#jsi_node_content} identified by the given token,
376
542
  # or `nil` if the token does not identify an existing child.
377
543
  #
@@ -386,32 +552,36 @@ module JSI
386
552
  end
387
553
 
388
554
  # A child JSI node, or the child of our {#jsi_instance}, identified by the given token.
389
- # The token must identify an existing child; behavior if the child does not exist is undefined.
390
555
  #
391
556
  # @param token (see Base#[])
392
557
  # @param as_jsi (see Base#[])
393
558
  # @return [JSI::Base, Object]
559
+ # @raise [Base::ChildNotPresent]
394
560
  def jsi_child(token, as_jsi: )
561
+ jsi_child_as_jsi(jsi_child_node(token), as_jsi)
562
+ end
563
+ private :jsi_child # internals for #[] but idk, could be public
564
+
565
+ # @param token An array index or Hash/object property name identifying a present child of this node
566
+ # @return [JSI::Base]
567
+ # @raise [Base::ChildNotPresent]
568
+ def jsi_child_node(token)
569
+ @child_node_by_token_map[token: token]
570
+ end
571
+
572
+ private def jsi_child_node_by_token_compute(token: )
573
+ jsi_child_ensure_present(token)
574
+
395
575
  child_content = jsi_node_content_child(token)
396
576
 
397
577
  child_indicated_schemas = @child_indicated_schemas_map[token: token, content: jsi_node_content]
398
578
  child_applied_schemas = @child_applied_schemas_map[token: token, child_indicated_schemas: child_indicated_schemas, child_content: child_content]
399
-
400
- jsi_child_as_jsi(child_content, child_applied_schemas, as_jsi) do
401
- @child_node_map[
579
+ @child_node_map[
402
580
  token: token,
403
581
  child_indicated_schemas: child_indicated_schemas,
404
582
  child_applied_schemas: child_applied_schemas,
405
583
  includes: SchemaClasses.includes_for(child_content),
406
- ]
407
- end
408
- end
409
- private :jsi_child # internals for #[] but idk, could be public
410
-
411
- # @param token (see Base#[])
412
- # @return [JSI::Base]
413
- protected def jsi_child_node(token)
414
- jsi_child(token, as_jsi: true)
584
+ ]
415
585
  end
416
586
 
417
587
  # A default value for a child of this node identified by the given token, if schemas describing
@@ -433,41 +603,42 @@ module JSI
433
603
 
434
604
  defaults = Set.new
435
605
  child_applied_schemas.each do |child_schema|
436
- if child_schema.keyword?('default')
437
- defaults << child_schema.jsi_node_content['default']
438
- end
606
+ defaults.merge(child_schema.dialect_invoke_each(:default))
439
607
  end
440
608
 
441
609
  if defaults.size == 1
442
610
  # use the default value
443
- jsi_child_as_jsi(defaults.first, child_applied_schemas, as_jsi) do
611
+ default_child_node =
444
612
  jsi_modified_copy do |i|
445
613
  i.dup.tap { |i_dup| i_dup[token] = defaults.first }
446
614
  end.jsi_child_node(token)
447
- end
615
+ jsi_child_as_jsi(default_child_node, as_jsi)
448
616
  else
449
617
  child_content
450
618
  end
451
619
  end
452
620
  private :jsi_default_child # internals for #[] but idk, could be public
453
621
 
454
- # subscripts to return a child value identified by the given token.
622
+ # Returns a child or children identified by param `token`.
623
+ #
624
+ # @param token [String, Integer, Range, Object] Identifies the child or children to return.
625
+ # Typically an array index or hash key (JSON object property name) of the instance.
626
+ #
627
+ # For an array instance, this may also be a Range (in which case an Array of children is returned)
628
+ # or a negative index; these behave as Array#[] does.
629
+ # @param as_jsi [:auto, true, false] (default is `:auto` or {Base::Conf#child_as_jsi conf child_as_jsi})
455
630
  #
456
- # @param token [String, Integer, Object] an array index or hash key (JSON object property name)
457
- # of the instance identifying the child value
458
- # @param as_jsi [:auto, true, false] (default is `:auto`)
459
631
  # Whether to return the child as a JSI. One of:
460
632
  #
461
633
  # - `:auto`: By default a JSI will be returned when either:
462
634
  #
463
- # - the result is a complex value (responds to #to_ary or #to_hash)
464
- # - the result is a schema (including true/false schemas)
635
+ # - the child is an array or hash
636
+ # - the child is a schema (including true/false schemas)
465
637
  #
466
638
  # The plain content is returned when it is a simple type.
467
639
  #
468
- # - true: the result value will always be returned as a JSI. the {#jsi_schemas} of the result may be
469
- # empty if no schemas describe the instance.
470
- # - false: the result value will always be the plain instance.
640
+ # - true: the result will always be returned as a JSI.
641
+ # - false: the result will always be the node's content.
471
642
  #
472
643
  # note that nil is returned (regardless of as_jsi) when there is no value to return because the token
473
644
  # is not a hash key or array index of the instance and no default value applies.
@@ -484,15 +655,26 @@ module JSI
484
655
  #
485
656
  # if the child instance's schemas do not indicate a single default value (that is, if zero or multiple
486
657
  # defaults are specified across those schemas), nil is returned.
487
- # (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
488
- # unspecified behavior.)
489
- # @return [JSI::Base, Object, nil] the child value identified by the subscript token
658
+ # @return [Base, Object, Array, nil] the child or children identified by `token`
490
659
  def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default)
660
+ raise(BlockGivenError) if block_given?
491
661
  # note: overridden by Base::HashNode, Base::ArrayNode
492
662
  jsi_simple_node_child_error(token)
493
663
  end
494
664
 
665
+ # When accessing this node as a child (from a parent's {#[]} or a property reader), should the result
666
+ # by default be a JSI node (this node), or its node content?
667
+ # This default may be overridden using the `as_jsi` parameter calling the parent's {#[]}.
668
+ # {Base::Conf Configurable} using {Base::Conf#child_as_jsi `child_as_jsi`}.
669
+ # @return [Boolean]
670
+ def jsi_as_child_default_as_jsi
671
+ # base default is false, for simple types. overridden by complex types (HashNode, ArrayNode), Schema, and others.
672
+ jsi_conf.child_as_jsi
673
+ end
674
+
495
675
  # The default value for the param `as_jsi` of {#[]}, controlling whether a child is returned as a JSI instance.
676
+ # @deprecated after v0.8. This is the parent node's preference whether its children are returned as JSIs, but it
677
+ # is better for a child to indicate whether it should be a JSI by overriding {#jsi_as_child_default_as_jsi}.
496
678
  # @return [:auto, true, false] a valid value of the `as_jsi` param of {#[]}
497
679
  def jsi_child_as_jsi_default
498
680
  :auto
@@ -500,33 +682,28 @@ module JSI
500
682
 
501
683
  # The default value for the param `use_default` of {#[]}, controlling whether a schema default value is
502
684
  # returned when a token refers to a child that is not in the document.
685
+ # {Base::Conf Configurable} using {Base::Conf#child_use_default `child_use_default`}.
503
686
  # @return [true, false] a valid value of the `use_default` param of {#[]}
504
687
  def jsi_child_use_default_default
505
- false
688
+ jsi_conf.child_use_default
506
689
  end
507
690
 
508
- # assigns the subscript of the instance identified by the given token to the given value.
509
- # if the value is a JSI, its instance is assigned instead of the JSI value itself.
691
+ # Assigns a child identified by the given token to the given value.
692
+ # If the given value is a JSI node, its content is used; its {#jsi_schemas} are not.
510
693
  #
511
- # @param token [String, Integer, Object] token identifying the subscript to assign
694
+ # @param token [String, Integer, Object] token identifying the child to assign
512
695
  # @param value [JSI::Base, Object] the value to be assigned
513
696
  def []=(token, value)
514
697
  unless jsi_array? || jsi_hash?
515
698
  jsi_simple_node_child_error(token)
516
699
  end
517
700
  if value.is_a?(Base)
518
- self[token] = value.jsi_instance
701
+ self[token] = value.jsi_node_content
519
702
  else
520
- jsi_instance[token] = value
703
+ jsi_node_content[token] = value
521
704
  end
522
705
  end
523
706
 
524
- # the set of JSI schema modules corresponding to the schemas that describe this JSI
525
- # @return [Set<Module>]
526
- def jsi_schema_modules
527
- Util.ensure_module_set(jsi_schemas.map(&:jsi_schema_module))
528
- end
529
-
530
707
  # Is this JSI described by the given schema (or schema module)?
531
708
  #
532
709
  # @param schema [Schema, SchemaModule]
@@ -535,7 +712,7 @@ module JSI
535
712
  if schema.is_a?(Schema)
536
713
  jsi_schemas.include?(schema)
537
714
  elsif schema.is_a?(SchemaModule)
538
- jsi_schema_modules.include?(schema)
715
+ jsi_schemas.include?(schema.schema)
539
716
  else
540
717
  raise(TypeError, "expected a Schema or Schema Module; got: #{schema.pretty_inspect.chomp}")
541
718
  end
@@ -556,17 +733,37 @@ module JSI
556
733
  #
557
734
  # @yield [Object] this JSI's instance. the block should result
558
735
  # in a nondestructively modified copy of this.
559
- # @return [JSI::Base subclass] the modified copy of self
560
- def jsi_modified_copy(&block)
736
+ # @return [Base] the modified copy of self
737
+ def jsi_modified_copy(**conf_kw, &block)
561
738
  modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
562
- modified_jsi_root_node = @jsi_root_node.jsi_indicated_schemas.new_jsi(modified_document,
563
- uri: @jsi_root_node.jsi_schema_base_uri,
564
- register: false, # default is already false but this is a place to be explicit
565
- schema_registry: jsi_schema_registry,
739
+
740
+ conf = jsi_conf.merge(**conf_kw)
741
+
742
+ modified_document = conf.to_immutable.call(modified_document) if !jsi_mutable? && conf.to_immutable
743
+
744
+ root_content = jsi_root_node.jsi_ptr.evaluate(modified_document)
745
+
746
+ root_applied_schemas = SchemaSet.build do |y|
747
+ c = y.method(:yield) # TODO drop c, just pass y, when all supported Enumerator::Yielder.method_defined?(:to_proc)
748
+ jsi_root_node.jsi_indicated_schemas.each do |is|
749
+ is.each_inplace_applicator_schema(root_content, &c)
750
+ end
751
+ end
752
+
753
+ root_class = JSI::SchemaClasses.class_for_schemas(root_applied_schemas,
754
+ includes: SchemaClasses.includes_for(root_content),
566
755
  mutable: jsi_mutable?,
567
- to_immutable: jsi_content_to_immutable,
568
756
  )
569
- modified_jsi_root_node.jsi_descendent_node(@jsi_ptr)
757
+ modified_jsi_root_node = root_class.new(
758
+ jsi_document: modified_document,
759
+ jsi_ptr: jsi_root_node.jsi_ptr,
760
+ jsi_indicated_schemas: jsi_root_node.jsi_indicated_schemas,
761
+ jsi_base_uri: jsi_root_node.jsi_base_uri,
762
+ jsi_schema_dynamic_anchor_map: jsi_root_node.jsi_schema_dynamic_anchor_map,
763
+ jsi_conf: conf,
764
+ ).send(:jsi_initialized)
765
+
766
+ modified_jsi_root_node.jsi_descendent_node(@root_rel_ptr)
570
767
  end
571
768
 
572
769
  # Is the instance an array?
@@ -601,7 +798,7 @@ module JSI
601
798
 
602
799
  # validates this JSI's instance against its schemas
603
800
  #
604
- # @return [JSI::Validation::FullResult]
801
+ # @return [JSI::Validation::Result::Full]
605
802
  def jsi_validate
606
803
  jsi_indicated_schemas.instance_validate(self)
607
804
  end
@@ -612,6 +809,15 @@ module JSI
612
809
  jsi_indicated_schemas.instance_valid?(self)
613
810
  end
614
811
 
812
+ # Asserts that this JSI is valid against its schemas.
813
+ # {JSI::Invalid} is raised if it is not.
814
+ #
815
+ # @raise [Invalid]
816
+ # @return [nil]
817
+ def jsi_valid!
818
+ jsi_validate.valid!
819
+ end
820
+
615
821
  # queries this JSI using the [JMESPath Ruby](https://rubygems.org/gems/jmespath) gem.
616
822
  # see [https://jmespath.org/](https://jmespath.org/) to learn the JMESPath query language.
617
823
  #
@@ -629,41 +835,161 @@ module JSI
629
835
  JMESPath.search(expression, self, **runtime_options)
630
836
  end
631
837
 
632
- def dup
633
- jsi_modified_copy(&:dup)
838
+ # The nearest ancestor (including this node) that is a resource root.
839
+ #
840
+ # A resource root is a schema with an absolute URI, or the {#jsi_root_node document's root node}
841
+ # (which might not be a schema and might not have an absolute URI).
842
+ # @return [Base]
843
+ def jsi_resource_root
844
+ super || jsi_root_node
634
845
  end
635
846
 
636
- # a string representing this JSI, indicating any named schemas and inspecting its instance
637
- # @return [String]
638
- def inspect
639
- -"\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
847
+ # @private
848
+ # @return [URI, nil]
849
+ def jsi_root_uri
850
+ jsi_conf.root_uri
640
851
  end
641
852
 
642
- def to_s
643
- inspect
853
+ # @private
854
+ # @return [SchemaModule::Connection]
855
+ def jsi_schema_module_connection
856
+ raise(BlockGivenError) if block_given?
857
+ @memos.fetch(:schema_module_connection) do
858
+ if is_a?(Schema)
859
+ raise(TypeError, "mutable schema may not have a schema module: #{self}") if jsi_mutable?
860
+ module_class = SchemaModule
861
+ else
862
+ raise(TypeError, "mutable JSI may not have a SchemaModule::Connection: #{self}") if jsi_mutable?
863
+ module_class = SchemaModule::Connection
864
+ end
865
+ @memos[:schema_module_connection] = module_class.new(self)
866
+ end
644
867
  end
645
868
 
646
- # pretty-prints a representation of this JSI to the given printer
869
+ # @private experimental
870
+ # a callback to be overridden (remember to call super), called with our #jsi_schema_module_connection when that is created
871
+ # @param mod [SchemaModule::Connection]
647
872
  # @return [void]
873
+ def jsi_schema_module_connection_created(mod)
874
+ end
875
+
876
+ # @private
877
+ # @return [Boolean]
878
+ def jsi_schema_module_connection_defined?
879
+ @memos.key?(:schema_module_connection)
880
+ end
881
+
882
+ # @param dynamic_anchor_map [Schema::DynamicAnchorMap]
883
+ # @return [Base]
884
+ private def jsi_dynamic_root_descendent(dynamic_anchor_map)
885
+ root = jsi_dynamic_root_map[
886
+ ptr: jsi_resource_root.jsi_ptr,
887
+ dynamic_anchor_map: dynamic_anchor_map,
888
+ ]
889
+ root.jsi_descendent_node(jsi_ptr.relative_to(jsi_resource_root.jsi_ptr))
890
+ end
891
+
892
+ # This instantiates a new root node (its #jsi_root_node is itself).
893
+ # When the resource at `ptr` is not the document root, the new root node has the
894
+ # unusual property that its jsi_ptr is not the root ptr; it is the given `ptr`.
895
+ # (Calling this a 'root node' is questionable, but that is the name we use.)
896
+ # From the new root node, no JSI node represents locations in the document above it.
897
+ # @param ptr [Ptr]
898
+ # @param dynamic_anchor_map [Schema::DynamicAnchorMap]
899
+ # @return [Base]
900
+ private def jsi_dynamic_root_compute(ptr: , dynamic_anchor_map: )
901
+ # self is always the originally instantiated root node (with jsi_ptr = Ptr[])
902
+ resource_root = jsi_descendent_node(ptr)
903
+ if resource_root.jsi_schema_dynamic_anchor_map == dynamic_anchor_map
904
+ return resource_root
905
+ end
906
+
907
+ resource_root.jsi_dynamic_root_instantiate(
908
+ jsi_document: resource_root.jsi_document,
909
+ jsi_ptr: ptr,
910
+ jsi_base_uri: resource_root.jsi_base_uri,
911
+ #jsi_schema_resource_ancestors: none (new root),
912
+ jsi_schema_dynamic_anchor_map: dynamic_anchor_map,
913
+ jsi_dynamic_root_map: jsi_dynamic_root_map,
914
+ jsi_conf: resource_root.jsi_conf,
915
+ )
916
+ end
917
+
918
+ protected def jsi_dynamic_root_instantiate(**kw)
919
+ # self is a resource root being instantiated with overridden dynamic scope
920
+ self.class.new(
921
+ jsi_indicated_schemas: jsi_indicated_schemas,
922
+ **kw,
923
+ ).send(:jsi_initialized)
924
+ end
925
+
926
+ # A JSI whose node content is a duplicate of this JSI's (using its #dup).
927
+ #
928
+ # Note that immutable JSIs are not made mutable with #dup.
929
+ # The content's #dup may return an unfrozen copy, but instantiating a modified
930
+ # copy of this JSI involves transforming the content to immutable again
931
+ # (using {Base::Conf conf} {Base::Conf#to_immutable to_immutable}).
932
+ # @return [Base]
933
+ def dup
934
+ jsi_modified_copy(&:dup)
935
+ end
936
+
937
+ # Prints a string indicating this JSI's schemas, briefly, and its content.
938
+ #
939
+ # If described by a schema with a named schema module, that is shown.
940
+ # The number of schemas describing this JSI is indicated.
941
+ #
942
+ # If this JSI is a simple type, the node's content is inspected; if complex, its children are inspected.
648
943
  def pretty_print(q)
649
- q.text '#<'
650
- q.text jsi_object_group_text.join(' ')
651
- q.group(2) {
652
- q.breakable ' '
944
+ jsi_pp_object_group(q, jsi_object_group_text) do
653
945
  q.pp jsi_instance
654
- }
655
- q.breakable ''
656
- q.text '>'
946
+ end
657
947
  end
658
948
 
659
949
  # @private
660
950
  # @return [Array<String>]
661
951
  def jsi_object_group_text
662
- schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name_from_ancestor || schema.schema_uri }.compact
952
+ jsi_schemas = self.jsi_schemas || Util::EMPTY_SET # for debug during MSN initialize, may not be set yet
953
+ schemas_priorities = jsi_schemas.each_with_index.map do |schema, i|
954
+ priority = [
955
+ schema.describes_schema? ? 0 : 1,
956
+
957
+ schema.jsi_schema_module_name ?
958
+ 0
959
+ : schema.jsi_schema_module_name_from_ancestor ?
960
+ 1
961
+ : schema.jsi_resource_uri ?
962
+ 2
963
+ : schema.schema_uri ?
964
+ 3
965
+ : 5,
966
+
967
+ !schema.respond_to?(:to_hash) ?
968
+ # boolean
969
+ 9
970
+ : schema.empty? ?
971
+ 8
972
+ : schema.all? { |k, _| k == '$ref' || k == '$dynamicRef' } ?
973
+ 7
974
+ : 5,
975
+ ]
976
+ [priority, i, schema]
977
+ end.sort
978
+
979
+ schema_names = []
980
+ schemas_priorities.each do |(priority, _idx, schema)|
981
+ if priority[0] == 0 || (priority == schemas_priorities.first.first && schema_names.size < 2)
982
+ name = schema.jsi_schema_identifier(required: priority[0] == 0)
983
+ schema_names << name if name
984
+ end
985
+ end
986
+
663
987
  if schema_names.empty?
664
- class_txt = "JSI"
988
+ schemas_txt = -"*#{self.jsi_schemas ? jsi_schemas.size : '?'}"
989
+ elsif schema_names.size == jsi_schemas.size
990
+ schemas_txt = -" (#{schema_names.join(' + ')})"
665
991
  else
666
- class_txt = -"JSI (#{schema_names.join(', ')})"
992
+ schemas_txt = -" (#{schema_names.join(' + ')} + #{jsi_schemas.size - schema_names.size})"
667
993
  end
668
994
 
669
995
  if (is_a?(ArrayNode) || is_a?(HashNode)) && ![Array, Hash].include?(jsi_node_content.class)
@@ -677,8 +1003,9 @@ module JSI
677
1003
  end
678
1004
 
679
1005
  [
680
- class_txt,
1006
+ -"JSI#{is_a?(MetaSchemaNode) ? ":MSN" : ""}#{schemas_txt}",
681
1007
  is_a?(Schema::MetaSchema) ? "Meta-Schema" : is_a?(Schema) ? "Schema" : nil,
1008
+ is_a?(Schema) && !jsi_schema_dynamic_anchor_map.empty? ? jsi_schema_dynamic_anchor_map.anchor_schemas_identifier : nil,
682
1009
  *content_txt,
683
1010
  ].compact.freeze
684
1011
  end
@@ -693,35 +1020,50 @@ module JSI
693
1020
  # Calls {Util.to_json} with the instance and any given options.
694
1021
  # @return [String]
695
1022
  def to_json(options = {})
696
- Util.to_json(jsi_instance, **options)
1023
+ Util.to_json(jsi_instance, options)
1024
+ end
1025
+
1026
+ # Psych/YAML .dump calls this method; dumping a JSI as YAML will dump its instance.
1027
+ # @param coder [Psych::Coder]
1028
+ def encode_with(coder)
1029
+ coder.represent_object(nil, jsi_node_content)
1030
+ nil
697
1031
  end
698
1032
 
699
1033
  # see {Util::Private::FingerprintHash}
700
1034
  # @api private
701
1035
  def jsi_fingerprint
1036
+ jsi_fingerprint_no_schemas.merge({
1037
+ jsi_schemas: jsi_schemas,
1038
+ }).freeze
1039
+ end
1040
+
1041
+ private def jsi_fingerprint_no_schemas
702
1042
  {
703
1043
  class: JSI::Base,
704
- jsi_schemas: jsi_schemas,
705
1044
  jsi_document: jsi_document,
706
1045
  jsi_ptr: jsi_ptr,
707
1046
  # for instances in documents with schemas:
708
- jsi_resource_ancestor_uri: jsi_resource_ancestor_uri,
709
- # different registries mean references may resolve to different resources so must not be equal
710
- jsi_schema_registry: jsi_schema_registry,
1047
+ jsi_base_uri: jsi_base_uri,
1048
+ jsi_root_uri: jsi_conf.root_uri,
1049
+ # different dynamic anchor map means dynamic references may resolve to different resources so must not be equal
1050
+ jsi_schema_dynamic_anchor_map: jsi_schema_dynamic_anchor_map,
1051
+ **jsi_conf.for_fingerprint,
711
1052
  }.freeze
712
1053
  end
713
1054
 
1055
+ # @private
1056
+ def jsi_next_schema_dynamic_anchor_map
1057
+ jsi_schema_dynamic_anchor_map
1058
+ end
1059
+
714
1060
  private
715
1061
 
716
- def jsi_memomaps_initialize
717
- @child_indicated_schemas_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_indicated_schemas_compute))
718
- @child_applied_schemas_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_applied_schemas_compute))
719
- @child_node_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_node_compute))
720
- end
1062
+ BY_TOKEN = proc { |i| i[:token] }
721
1063
 
722
- def jsi_indicated_schemas=(jsi_indicated_schemas)
723
- #chkbug raise(Bug) unless jsi_indicated_schemas.is_a?(SchemaSet)
724
- @jsi_indicated_schemas = jsi_indicated_schemas
1064
+ def jsi_memomaps_initialize
1065
+ @child_indicated_schemas_map = jsi_memomap(key_by: BY_TOKEN, &method(:jsi_child_indicated_schemas_compute))
1066
+ @child_applied_schemas_map = jsi_memomap(key_by: BY_TOKEN, &method(:jsi_child_applied_schemas_compute))
725
1067
  end
726
1068
 
727
1069
  def jsi_child_node_compute(token: , child_indicated_schemas: , child_applied_schemas: , includes: )
@@ -729,40 +1071,70 @@ module JSI
729
1071
  includes: includes,
730
1072
  mutable: jsi_mutable?,
731
1073
  )
732
- jsi_class.new(@jsi_document,
1074
+ jsi_class.new(
1075
+ jsi_document: @jsi_document,
733
1076
  jsi_ptr: @jsi_ptr[token],
734
1077
  jsi_indicated_schemas: child_indicated_schemas,
735
- jsi_schema_base_uri: jsi_resource_ancestor_uri,
1078
+ jsi_base_uri: jsi_next_base_uri,
736
1079
  jsi_schema_resource_ancestors: is_a?(Schema) ? jsi_subschema_resource_ancestors : jsi_schema_resource_ancestors,
737
- jsi_schema_registry: jsi_schema_registry,
738
- jsi_content_to_immutable: @jsi_content_to_immutable,
1080
+ jsi_schema_dynamic_anchor_map: jsi_child_dynamic_anchor_map(token: token, child_schemas: child_applied_schemas),
739
1081
  jsi_root_node: @jsi_root_node,
740
- )
1082
+ ).send(:jsi_initialized)
1083
+ end
1084
+
1085
+ def jsi_child_dynamic_anchor_map(token: , child_schemas: )
1086
+ metaschema = child_schemas.detect(&:describes_schema?)
1087
+ # note: no memoized dialect.bootstrap_schema; this is only used once.
1088
+ if metaschema && MetaSchemaNode::BootstrapSchema.new(dialect: metaschema.dialect, jsi_document: jsi_document, jsi_ptr: jsi_ptr[token], jsi_base_uri: jsi_next_base_uri).jsi_is_resource_root?
1089
+ # if the child is a resource root, compute dynamic scope for it.
1090
+ # TODO if without_node removes the child, that probably means the child is equal to a descendent
1091
+ # of another dynamic_root. not sure if this should find/return that child - on one hand
1092
+ # instantiating multiple equal nodes via different dynamic scopes isn't ideal; on the other
1093
+ # hand having child_node return a node with a different jsi_root_node may also be problematic.
1094
+ jsi_next_schema_dynamic_anchor_map.without_node(self, ptr: jsi_ptr[token])
1095
+ else
1096
+ # child is not a resource, has the same dynamic scope as self.
1097
+ jsi_schema_dynamic_anchor_map
1098
+ end
741
1099
  end
742
1100
 
743
1101
  def jsi_child_indicated_schemas_compute(token: , content: )
744
- jsi_schemas.child_applicator_schemas(token, content)
1102
+ if jsi_schemas.any?(&:application_requires_evaluated)
1103
+ # if application_requires_evaluated, in-place application needs to collect token evaluation
1104
+ # recursively to inform child application, so must be recomputed.
1105
+ jsi_indicated_schemas.each_yield_set do |is, y|
1106
+ is.each_inplace_child_applicator_schema(token, content,
1107
+ collect_evaluated_validate: jsi_conf.application_collect_evaluated_validate,
1108
+ &y
1109
+ )
1110
+ end
1111
+ else
1112
+ # if token evaluation does not need to be collected, use our already-computed #jsi_schemas.
1113
+ jsi_schemas.each_yield_set do |s, y|
1114
+ s.each_child_applicator_schema(token, content, &y)
1115
+ end
1116
+ end
745
1117
  end
746
1118
 
747
1119
  def jsi_child_applied_schemas_compute(token: , child_indicated_schemas: , child_content: )
748
- child_indicated_schemas.inplace_applicator_schemas(child_content)
1120
+ child_indicated_schemas.each_yield_set do |cis, y|
1121
+ cis.each_inplace_applicator_schema(child_content, &y)
1122
+ end
749
1123
  end
750
1124
 
751
- def jsi_child_as_jsi(child_content, child_schemas, as_jsi)
1125
+ def jsi_child_as_jsi(child_node, as_jsi)
752
1126
  if [true, false].include?(as_jsi)
753
1127
  child_as_jsi = as_jsi
754
1128
  elsif as_jsi == :auto
755
- child_is_complex = child_content.respond_to?(:to_hash) || child_content.respond_to?(:to_ary)
756
- child_is_schema = child_schemas.any?(&:describes_schema?)
757
- child_as_jsi = child_is_complex || child_is_schema
1129
+ child_as_jsi = child_node.jsi_as_child_default_as_jsi
758
1130
  else
759
1131
  raise(ArgumentError, "as_jsi must be one of: :auto, true, false")
760
1132
  end
761
1133
 
762
1134
  if child_as_jsi
763
- yield
1135
+ child_node
764
1136
  else
765
- child_content
1137
+ child_node.jsi_node_content
766
1138
  end
767
1139
  end
768
1140
 
@@ -773,5 +1145,16 @@ module JSI
773
1145
  "instance: #{jsi_instance.pretty_inspect.chomp}",
774
1146
  ].join("\n"))
775
1147
  end
1148
+
1149
+ # Note that #initialize does not invoke the jsi_conf.after_initialize callback, because
1150
+ # the node is not finished initializing when #initialize returns; other included modules
1151
+ # or subclasses with #initialize methods calling super still need to finish.
1152
+ # Places where Base is directly instantiated must call #jsi_initialized.
1153
+ # @return [self]
1154
+ def jsi_initialized
1155
+ #chkbug fail if defined?(super) # this should be the last jsi_initialized in class ancestry
1156
+ jsi_conf.after_initialize.call(self) if jsi_conf.after_initialize
1157
+ self
1158
+ end
776
1159
  end
777
1160
  end