jsi-dev 0.0.8 → 0.0.10

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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +3 -4
  3. data/CHANGELOG.md +19 -0
  4. data/LICENSE.md +2 -3
  5. data/README.md +87 -43
  6. data/docs/Glossary.md +313 -0
  7. data/jsi.gemspec +1 -1
  8. data/lib/jsi/base/mutability.rb +48 -0
  9. data/lib/jsi/base/node.rb +66 -52
  10. data/lib/jsi/base.rb +593 -176
  11. data/lib/jsi/jsi_coder.rb +4 -2
  12. data/lib/jsi/metaschema_node/bootstrap_schema.rb +118 -59
  13. data/lib/jsi/metaschema_node.rb +245 -154
  14. data/lib/jsi/ptr.rb +45 -17
  15. data/lib/jsi/ref.rb +197 -0
  16. data/lib/jsi/registry.rb +311 -0
  17. data/lib/jsi/schema/cxt/child_application.rb +35 -0
  18. data/lib/jsi/schema/cxt/inplace_application.rb +37 -0
  19. data/lib/jsi/schema/cxt.rb +80 -0
  20. data/lib/jsi/schema/dialect.rb +137 -0
  21. data/lib/jsi/schema/draft04.rb +113 -5
  22. data/lib/jsi/schema/draft06.rb +123 -5
  23. data/lib/jsi/schema/draft07.rb +157 -5
  24. data/lib/jsi/schema/draft202012.rb +303 -0
  25. data/lib/jsi/schema/dynamic_anchor_map.rb +63 -0
  26. data/lib/jsi/schema/element.rb +69 -0
  27. data/lib/jsi/schema/elements/anchor.rb +13 -0
  28. data/lib/jsi/schema/elements/array_validation.rb +82 -0
  29. data/lib/jsi/schema/elements/comment.rb +10 -0
  30. data/lib/jsi/schema/{validation → elements}/const.rb +11 -7
  31. data/lib/jsi/schema/elements/contains.rb +59 -0
  32. data/lib/jsi/schema/elements/contains_minmax.rb +91 -0
  33. data/lib/jsi/schema/elements/content_encoding.rb +10 -0
  34. data/lib/jsi/schema/elements/content_media_type.rb +10 -0
  35. data/lib/jsi/schema/elements/content_schema.rb +16 -0
  36. data/lib/jsi/schema/elements/default.rb +11 -0
  37. data/lib/jsi/schema/elements/definitions.rb +19 -0
  38. data/lib/jsi/schema/elements/dependencies.rb +99 -0
  39. data/lib/jsi/schema/elements/dependent_required.rb +49 -0
  40. data/lib/jsi/schema/elements/dependent_schemas.rb +69 -0
  41. data/lib/jsi/schema/elements/dynamic_ref.rb +69 -0
  42. data/lib/jsi/schema/elements/enum.rb +26 -0
  43. data/lib/jsi/schema/elements/examples.rb +10 -0
  44. data/lib/jsi/schema/elements/format.rb +10 -0
  45. data/lib/jsi/schema/elements/id.rb +30 -0
  46. data/lib/jsi/schema/elements/if_then_else.rb +82 -0
  47. data/lib/jsi/schema/elements/info_bool.rb +10 -0
  48. data/lib/jsi/schema/elements/info_string.rb +10 -0
  49. data/lib/jsi/schema/elements/items.rb +93 -0
  50. data/lib/jsi/schema/elements/items_prefixed.rb +96 -0
  51. data/lib/jsi/schema/elements/not.rb +31 -0
  52. data/lib/jsi/schema/elements/numeric.rb +137 -0
  53. data/lib/jsi/schema/elements/numeric_draft04.rb +77 -0
  54. data/lib/jsi/schema/elements/object_validation.rb +55 -0
  55. data/lib/jsi/schema/elements/pattern.rb +35 -0
  56. data/lib/jsi/schema/elements/properties.rb +145 -0
  57. data/lib/jsi/schema/elements/property_names.rb +48 -0
  58. data/lib/jsi/schema/elements/ref.rb +62 -0
  59. data/lib/jsi/schema/elements/required.rb +34 -0
  60. data/lib/jsi/schema/elements/self.rb +24 -0
  61. data/lib/jsi/schema/elements/some_of.rb +180 -0
  62. data/lib/jsi/schema/elements/string_validation.rb +57 -0
  63. data/lib/jsi/schema/elements/type.rb +43 -0
  64. data/lib/jsi/schema/elements/unevaluated_items.rb +54 -0
  65. data/lib/jsi/schema/elements/unevaluated_properties.rb +54 -0
  66. data/lib/jsi/schema/elements/xschema.rb +10 -0
  67. data/lib/jsi/schema/elements/xvocabulary.rb +10 -0
  68. data/lib/jsi/schema/elements.rb +101 -0
  69. data/lib/jsi/schema/issue.rb +3 -4
  70. data/lib/jsi/schema/schema_ancestor_node.rb +105 -52
  71. data/lib/jsi/schema/vocabulary.rb +36 -0
  72. data/lib/jsi/schema.rb +598 -383
  73. data/lib/jsi/schema_classes.rb +195 -141
  74. data/lib/jsi/schema_set.rb +85 -128
  75. data/lib/jsi/set.rb +23 -0
  76. data/lib/jsi/simple_wrap.rb +14 -17
  77. data/lib/jsi/struct.rb +57 -0
  78. data/lib/jsi/uri.rb +40 -0
  79. data/lib/jsi/util/private/memo_map.rb +9 -13
  80. data/lib/jsi/util/private.rb +59 -31
  81. data/lib/jsi/util/typelike.rb +19 -60
  82. data/lib/jsi/util.rb +53 -34
  83. data/lib/jsi/validation/error.rb +45 -2
  84. data/lib/jsi/validation/result.rb +121 -90
  85. data/lib/jsi/validation.rb +1 -6
  86. data/lib/jsi/version.rb +1 -1
  87. data/lib/jsi.rb +170 -36
  88. data/lib/schemas/json-schema.org/draft/2020-12/schema.rb +62 -0
  89. data/lib/schemas/json-schema.org/draft-04/schema.rb +60 -109
  90. data/lib/schemas/json-schema.org/draft-06/schema.rb +53 -108
  91. data/lib/schemas/json-schema.org/draft-07/schema.rb +63 -127
  92. data/readme.rb +4 -4
  93. data/{resources}/schemas/2020-12_strict.json +19 -0
  94. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/applicator.json +48 -0
  95. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/content.json +17 -0
  96. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/core.json +51 -0
  97. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/format-annotation.json +14 -0
  98. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/format-assertion.json +14 -0
  99. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/meta-data.json +37 -0
  100. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/unevaluated.json +15 -0
  101. data/{resources}/schemas/json-schema.org/draft/2020-12/meta/validation.json +98 -0
  102. data/{resources}/schemas/json-schema.org/draft/2020-12/schema.json +58 -0
  103. metadata +73 -52
  104. data/docs/glossary.md +0 -281
  105. data/lib/jsi/metaschema.rb +0 -6
  106. data/lib/jsi/schema/application/child_application/contains.rb +0 -25
  107. data/lib/jsi/schema/application/child_application/draft04.rb +0 -21
  108. data/lib/jsi/schema/application/child_application/draft06.rb +0 -28
  109. data/lib/jsi/schema/application/child_application/draft07.rb +0 -28
  110. data/lib/jsi/schema/application/child_application/items.rb +0 -18
  111. data/lib/jsi/schema/application/child_application/properties.rb +0 -25
  112. data/lib/jsi/schema/application/child_application.rb +0 -13
  113. data/lib/jsi/schema/application/draft04.rb +0 -8
  114. data/lib/jsi/schema/application/draft06.rb +0 -8
  115. data/lib/jsi/schema/application/draft07.rb +0 -8
  116. data/lib/jsi/schema/application/inplace_application/dependencies.rb +0 -28
  117. data/lib/jsi/schema/application/inplace_application/draft04.rb +0 -25
  118. data/lib/jsi/schema/application/inplace_application/draft06.rb +0 -26
  119. data/lib/jsi/schema/application/inplace_application/draft07.rb +0 -32
  120. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +0 -20
  121. data/lib/jsi/schema/application/inplace_application/ref.rb +0 -18
  122. data/lib/jsi/schema/application/inplace_application/someof.rb +0 -44
  123. data/lib/jsi/schema/application/inplace_application.rb +0 -14
  124. data/lib/jsi/schema/application.rb +0 -12
  125. data/lib/jsi/schema/ref.rb +0 -183
  126. data/lib/jsi/schema/validation/array.rb +0 -69
  127. data/lib/jsi/schema/validation/contains.rb +0 -25
  128. data/lib/jsi/schema/validation/dependencies.rb +0 -49
  129. data/lib/jsi/schema/validation/draft04/minmax.rb +0 -91
  130. data/lib/jsi/schema/validation/draft04.rb +0 -110
  131. data/lib/jsi/schema/validation/draft06.rb +0 -120
  132. data/lib/jsi/schema/validation/draft07.rb +0 -157
  133. data/lib/jsi/schema/validation/enum.rb +0 -25
  134. data/lib/jsi/schema/validation/ifthenelse.rb +0 -46
  135. data/lib/jsi/schema/validation/items.rb +0 -54
  136. data/lib/jsi/schema/validation/not.rb +0 -20
  137. data/lib/jsi/schema/validation/numeric.rb +0 -121
  138. data/lib/jsi/schema/validation/object.rb +0 -45
  139. data/lib/jsi/schema/validation/pattern.rb +0 -34
  140. data/lib/jsi/schema/validation/properties.rb +0 -101
  141. data/lib/jsi/schema/validation/property_names.rb +0 -32
  142. data/lib/jsi/schema/validation/ref.rb +0 -40
  143. data/lib/jsi/schema/validation/required.rb +0 -27
  144. data/lib/jsi/schema/validation/someof.rb +0 -90
  145. data/lib/jsi/schema/validation/string.rb +0 -47
  146. data/lib/jsi/schema/validation/type.rb +0 -49
  147. data/lib/jsi/schema/validation.rb +0 -49
  148. data/lib/jsi/schema_registry.rb +0 -190
  149. data/lib/jsi/util/private/attr_struct.rb +0 -130
data/lib/jsi/base.rb CHANGED
@@ -10,52 +10,158 @@ 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 which 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'
19
20
  autoload :HashNode, 'jsi/base/node'
20
21
  autoload :StringNode, 'jsi/base/node'
22
+ autoload(:Mutable, 'jsi/base/mutability')
23
+ autoload(:Immutable, 'jsi/base/mutability')
21
24
 
22
25
  include Schema::SchemaAncestorNode
26
+ include(Util::Pretty)
23
27
 
24
28
  # An exception raised when attempting to access a child of a node which cannot have children.
25
29
  # A complex node can have children, a simple node cannot.
26
30
  class SimpleNodeChildError < StandardError
27
31
  end
28
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
+
29
147
  class << self
30
148
  # A string indicating the schema module name
31
149
  # and/or schema URI of each schema the class represents.
32
150
  # @return [String]
33
151
  def inspect
34
- if !respond_to?(:jsi_class_schemas)
35
- super
36
- else
37
- schema_names = jsi_class_schemas.map do |schema|
38
- mod_name = schema.jsi_schema_module.name_from_ancestor
39
- if mod_name && schema.schema_absolute_uri
40
- "#{mod_name} <#{schema.schema_absolute_uri}>"
41
- elsif mod_name
42
- mod_name
43
- elsif schema.schema_uri
44
- schema.schema_uri.to_s
45
- else
46
- schema.jsi_ptr.uri.to_s
47
- end
48
- end
49
-
50
- if schema_names.empty?
51
- "(JSI Schema Class for 0 schemas)"
52
- else
53
- -"(JSI Schema Class: #{schema_names.join(' + ')})"
54
- 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}>"
55
157
  end
158
+ "(#{[superclass, *schema_names, *jsi_class_includes].join(' + ')})"
159
+ end
160
+
161
+ def to_s
162
+ inspect
56
163
  end
57
164
 
58
- alias_method :to_s, :inspect
59
165
  # A constant name of this class. This is generated from any schema module name or URI of each schema
60
166
  # this class represents, or random characters.
61
167
  #
@@ -69,9 +175,9 @@ module JSI
69
175
  return super unless respond_to?(:jsi_class_schemas)
70
176
  alnum = proc { |id| (id % 36**4).to_s(36).rjust(4, '0').upcase }
71
177
  schema_names = jsi_class_schemas.map do |schema|
72
- named_ancestor_schema, tokens = schema.jsi_schema_module.send(:named_ancestor_schema_tokens)
73
- if named_ancestor_schema
74
- [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('_')
75
181
  elsif schema.schema_uri
76
182
  schema.schema_uri.to_s
77
183
  else
@@ -97,43 +203,53 @@ module JSI
97
203
  #
98
204
  # this is a private api - users should look elsewhere to instantiate JSIs, in particular:
99
205
  #
100
- # - {JSI.new_schema} and {Schema::DescribesSchema#new_schema} to instantiate schemas
206
+ # - {JSI.new_schema} and {Schema::MetaSchema#new_schema} to instantiate schemas
101
207
  # - {Schema#new_jsi} to instantiate schema instances
102
208
  #
103
209
  # @api private
104
210
  # @param jsi_document [Object] the document containing the instance
105
211
  # @param jsi_ptr [JSI::Ptr] a pointer pointing to the JSI's instance in the document
106
- # @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`
107
213
  # @param jsi_schema_resource_ancestors [Array<JSI::Base + JSI::Schema>]
108
- # @param jsi_root_node [JSI::Base] the JSI of the root of the document containing this JSI
109
- 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: ,
110
220
  jsi_ptr: Ptr[],
111
221
  jsi_indicated_schemas: ,
112
- jsi_schema_base_uri: nil,
222
+ jsi_base_uri: nil,
113
223
  jsi_schema_resource_ancestors: Util::EMPTY_ARY,
114
- jsi_schema_registry: ,
224
+ jsi_schema_dynamic_anchor_map: Schema::DynamicAnchorMap::EMPTY,
225
+ jsi_dynamic_root_map: nil,
226
+ jsi_conf: nil,
115
227
  jsi_root_node: nil
116
228
  )
117
- raise(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)
118
-
119
- super()
120
-
121
- self.jsi_document = jsi_document
122
- self.jsi_ptr = jsi_ptr
123
- self.jsi_indicated_schemas = jsi_indicated_schemas
124
- 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
125
240
  self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors
126
- self.jsi_schema_registry = jsi_schema_registry
127
- if @jsi_ptr.root?
128
- raise(Bug, "jsi_root_node specified for root JSI") if jsi_root_node
129
- @jsi_root_node = self
130
- else
131
- raise(Bug, "jsi_root_node is not JSI::Base") if !jsi_root_node.is_a?(JSI::Base)
132
- raise(Bug, "jsi_root_node ptr is not root") if !jsi_root_node.jsi_ptr.root?
133
- @jsi_root_node = jsi_root_node
134
- end
135
-
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 = {}
136
249
  jsi_memomaps_initialize
250
+ jsi_mutability_initialize
251
+
252
+ super()
137
253
 
138
254
  if jsi_instance.is_a?(JSI::Base)
139
255
  raise(TypeError, "a JSI::Base instance must not be another JSI::Base. received: #{jsi_instance.pretty_inspect.chomp}")
@@ -142,7 +258,7 @@ module JSI
142
258
 
143
259
  # @!method jsi_schemas
144
260
  # The set of schemas that describe this instance.
145
- # 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
146
262
  # of our {#jsi_indicated_schemas}.
147
263
  # @return [JSI::SchemaSet]
148
264
  # note: defined on subclasses by JSI::SchemaClasses.class_for_schemas
@@ -155,25 +271,41 @@ module JSI
155
271
  # @return [JSI::Ptr]
156
272
  attr_reader :jsi_ptr
157
273
 
158
- # the JSI at the root of this JSI's document
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)
285
+
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.
159
288
  # @return [JSI::Base]
160
289
  attr_reader :jsi_root_node
161
290
 
162
291
  # the content of this node in our {#jsi_document} at our {#jsi_ptr}. the same as {#jsi_instance}.
163
292
  def jsi_node_content
164
- content = jsi_ptr.evaluate(jsi_document)
165
- content
293
+ # stub method for doc, overridden by Mutable/Immutable
166
294
  end
167
295
 
168
- # the JSON schema instance this JSI represents - the underlying JSON data used to instantiate this JSI
169
- alias_method :jsi_instance, :jsi_node_content
296
+ # The JSON schema instance this JSI represents - the underlying JSON data used to instantiate this JSI.
297
+ # The same as {#jsi_node_content} - 'node content' is usually preferable terminology, to avoid
298
+ # ambiguity in the heavily overloaded term 'instance'.
299
+ def jsi_instance
300
+ jsi_node_content
301
+ end
170
302
 
171
- # the schemas indicated as describing this instance, prior to inplace application.
303
+ # The schemas indicated as describing this instance, prior to in-place application.
172
304
  #
173
- # 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
174
306
  # which describe this instance. for most purposes, `#jsi_schemas` is more relevant.
175
307
  #
176
- # `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
177
309
  # subschemas of `allOf`, whereas `#jsi_schemas` does.
178
310
  #
179
311
  # this does include indicated schemas which do not apply themselves, such as `$ref`
@@ -192,7 +324,22 @@ module JSI
192
324
  # @yield [JSI::Base] each descendent node, starting with self
193
325
  # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
194
326
  def jsi_each_descendent_node(propertyNames: false, &block)
195
- 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
196
343
 
197
344
  yield self
198
345
 
@@ -203,12 +350,38 @@ module JSI
203
350
  end
204
351
 
205
352
  jsi_each_child_token do |token|
206
- jsi_child(token, as_jsi: true).jsi_each_descendent_node(propertyNames: propertyNames, &block)
353
+ jsi_child_node(token).jsi_each_descendent_node(propertyNames: propertyNames, &block)
207
354
  end
208
355
 
209
356
  nil
210
357
  end
211
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
+
212
385
  # recursively selects descendent nodes of this JSI, returning a modified copy of self containing only
213
386
  # descendent nodes for which the given block had a true-ish result.
214
387
  #
@@ -222,7 +395,7 @@ module JSI
222
395
  if jsi_array? || jsi_hash?
223
396
  res = instance.class.new
224
397
  jsi_each_child_token do |token|
225
- v = jsi_child(token, as_jsi: true)
398
+ v = jsi_child_node(token)
226
399
  if yield(v)
227
400
  res_v = v.jsi_select_descendents_node_first(&block).jsi_node_content
228
401
  if jsi_array?
@@ -252,7 +425,7 @@ module JSI
252
425
  if jsi_array? || jsi_hash?
253
426
  res = instance.class.new
254
427
  jsi_each_child_token do |token|
255
- v = jsi_child(token, as_jsi: true).jsi_select_descendents_leaf_first(&block)
428
+ v = jsi_child_node(token).jsi_select_descendents_leaf_first(&block)
256
429
  if yield(v)
257
430
  res_v = v.jsi_node_content
258
431
  if jsi_array?
@@ -269,24 +442,24 @@ module JSI
269
442
  end
270
443
  end
271
444
 
272
- # an array of JSI instances above this one in the document.
445
+ # JSI nodes above this one in the document.
273
446
  #
274
447
  # @return [Array<JSI::Base>]
275
448
  def jsi_parent_nodes
276
- parent = jsi_root_node
277
-
278
- jsi_ptr.tokens.map do |token|
279
- parent.tap do
280
- parent = parent[token, as_jsi: true]
281
- end
282
- 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
283
456
  end
284
457
 
285
458
  # the immediate parent of this JSI. nil if there is no parent.
286
459
  #
287
460
  # @return [JSI::Base, nil]
288
461
  def jsi_parent_node
289
- 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)
290
463
  end
291
464
 
292
465
  # ancestor JSI instances from this node up to the root. this node itself is always its own first ancestor.
@@ -297,8 +470,8 @@ module JSI
297
470
  ancestor = jsi_root_node
298
471
  ancestors << ancestor
299
472
 
300
- jsi_ptr.tokens.each do |token|
301
- ancestor = ancestor[token, as_jsi: true]
473
+ @root_rel_ptr.tokens.each do |token|
474
+ ancestor = ancestor.jsi_child_node(token)
302
475
  ancestors << ancestor
303
476
  end
304
477
  ancestors.reverse!.freeze
@@ -309,11 +482,11 @@ module JSI
309
482
  # @param ptr [JSI::Ptr, #to_ary]
310
483
  # @return [JSI::Base]
311
484
  def jsi_descendent_node(ptr)
312
- descendent = Ptr.ary_ptr(ptr).evaluate(self, as_jsi: true)
313
- descendent
485
+ tokens = Ptr.ary_ptr(ptr).resolve_against(jsi_node_content).tokens
486
+ tokens.inject(self, &:jsi_child_node)
314
487
  end
315
488
 
316
- # A shorthand alias for {#jsi_descendent_node}.
489
+ # The descendent node at the given {Ptr}, token array, or pointer string.
317
490
  #
318
491
  # Note that, though more convenient to type, using an operator whose meaning may not be intuitive
319
492
  # to a reader could impair readability of code.
@@ -322,13 +495,14 @@ module JSI
322
495
  #
323
496
  # my_jsi / ['foo', 'bar']
324
497
  # my_jsi / %w(foo bar)
498
+ # my_jsi / '/foo/bar'
325
499
  # my_schema / JSI::Ptr['additionalProperties']
326
500
  # my_schema / %w(properties foo items additionalProperties)
327
501
  #
328
- # @param (see #jsi_descendent_node)
502
+ # @param ptr [JSI::Ptr, #to_ary, #to_str]
329
503
  # @return (see #jsi_descendent_node)
330
504
  def /(ptr)
331
- jsi_descendent_node(ptr)
505
+ jsi_descendent_node(ptr.respond_to?(:to_str) ? Ptr.from_pointer(ptr) : ptr)
332
506
  end
333
507
 
334
508
  # yields each token (array index or hash key) identifying a child node.
@@ -350,11 +524,20 @@ module JSI
350
524
  #
351
525
  # @param token [String, Integer]
352
526
  # @return [Boolean]
353
- def jsi_child_token_in_range?(token)
527
+ def jsi_child_token_present?(token)
354
528
  # note: overridden by Base::HashNode, Base::ArrayNode
355
529
  false
356
530
  end
357
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
+
358
541
  # The child of the {#jsi_node_content} identified by the given token,
359
542
  # or `nil` if the token does not identify an existing child.
360
543
  #
@@ -369,27 +552,37 @@ module JSI
369
552
  end
370
553
 
371
554
  # A child JSI node, or the child of our {#jsi_instance}, identified by the given token.
372
- # The token must identify an existing child; behavior if the child does not exist is undefined.
373
555
  #
374
556
  # @param token (see Base#[])
375
557
  # @param as_jsi (see Base#[])
376
558
  # @return [JSI::Base, Object]
559
+ # @raise [Base::ChildNotPresent]
377
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
+
378
575
  child_content = jsi_node_content_child(token)
379
576
 
380
577
  child_indicated_schemas = @child_indicated_schemas_map[token: token, content: jsi_node_content]
381
578
  child_applied_schemas = @child_applied_schemas_map[token: token, child_indicated_schemas: child_indicated_schemas, child_content: child_content]
382
-
383
- jsi_child_as_jsi(child_content, child_applied_schemas, as_jsi) do
384
- @child_node_map[
579
+ @child_node_map[
385
580
  token: token,
386
581
  child_indicated_schemas: child_indicated_schemas,
387
582
  child_applied_schemas: child_applied_schemas,
388
583
  includes: SchemaClasses.includes_for(child_content),
389
- ]
390
- end
584
+ ]
391
585
  end
392
- private :jsi_child # internals for #[] but idk, could be public
393
586
 
394
587
  # A default value for a child of this node identified by the given token, if schemas describing
395
588
  # that child define a default value.
@@ -410,41 +603,42 @@ module JSI
410
603
 
411
604
  defaults = Set.new
412
605
  child_applied_schemas.each do |child_schema|
413
- if child_schema.keyword?('default')
414
- defaults << child_schema.jsi_node_content['default']
415
- end
606
+ defaults.merge(child_schema.dialect_invoke_each(:default))
416
607
  end
417
608
 
418
609
  if defaults.size == 1
419
610
  # use the default value
420
- jsi_child_as_jsi(defaults.first, child_applied_schemas, as_jsi) do
611
+ default_child_node =
421
612
  jsi_modified_copy do |i|
422
613
  i.dup.tap { |i_dup| i_dup[token] = defaults.first }
423
- end[token, as_jsi: true]
424
- end
614
+ end.jsi_child_node(token)
615
+ jsi_child_as_jsi(default_child_node, as_jsi)
425
616
  else
426
617
  child_content
427
618
  end
428
619
  end
429
620
  private :jsi_default_child # internals for #[] but idk, could be public
430
621
 
431
- # 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})
432
630
  #
433
- # @param token [String, Integer, Object] an array index or hash key (JSON object property name)
434
- # of the instance identifying the child value
435
- # @param as_jsi [:auto, true, false] (default is `:auto`)
436
631
  # Whether to return the child as a JSI. One of:
437
632
  #
438
633
  # - `:auto`: By default a JSI will be returned when either:
439
634
  #
440
- # - the result is a complex value (responds to #to_ary or #to_hash)
441
- # - 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)
442
637
  #
443
638
  # The plain content is returned when it is a simple type.
444
639
  #
445
- # - true: the result value will always be returned as a JSI. the {#jsi_schemas} of the result may be
446
- # empty if no schemas describe the instance.
447
- # - 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.
448
642
  #
449
643
  # note that nil is returned (regardless of as_jsi) when there is no value to return because the token
450
644
  # is not a hash key or array index of the instance and no default value applies.
@@ -461,15 +655,26 @@ module JSI
461
655
  #
462
656
  # if the child instance's schemas do not indicate a single default value (that is, if zero or multiple
463
657
  # defaults are specified across those schemas), nil is returned.
464
- # (one exception is when this JSI's instance is a Hash with a default or default_proc, which has
465
- # unspecified behavior.)
466
- # @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`
467
659
  def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default)
660
+ raise(BlockGivenError) if block_given?
468
661
  # note: overridden by Base::HashNode, Base::ArrayNode
469
662
  jsi_simple_node_child_error(token)
470
663
  end
471
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
+
472
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}.
473
678
  # @return [:auto, true, false] a valid value of the `as_jsi` param of {#[]}
474
679
  def jsi_child_as_jsi_default
475
680
  :auto
@@ -477,33 +682,28 @@ module JSI
477
682
 
478
683
  # The default value for the param `use_default` of {#[]}, controlling whether a schema default value is
479
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`}.
480
686
  # @return [true, false] a valid value of the `use_default` param of {#[]}
481
687
  def jsi_child_use_default_default
482
- false
688
+ jsi_conf.child_use_default
483
689
  end
484
690
 
485
- # assigns the subscript of the instance identified by the given token to the given value.
486
- # 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.
487
693
  #
488
- # @param token [String, Integer, Object] token identifying the subscript to assign
694
+ # @param token [String, Integer, Object] token identifying the child to assign
489
695
  # @param value [JSI::Base, Object] the value to be assigned
490
696
  def []=(token, value)
491
697
  unless jsi_array? || jsi_hash?
492
698
  jsi_simple_node_child_error(token)
493
699
  end
494
700
  if value.is_a?(Base)
495
- self[token] = value.jsi_instance
701
+ self[token] = value.jsi_node_content
496
702
  else
497
- jsi_instance[token] = value
703
+ jsi_node_content[token] = value
498
704
  end
499
705
  end
500
706
 
501
- # the set of JSI schema modules corresponding to the schemas that describe this JSI
502
- # @return [Set<Module>]
503
- def jsi_schema_modules
504
- Util.ensure_module_set(jsi_schemas.map(&:jsi_schema_module))
505
- end
506
-
507
707
  # Is this JSI described by the given schema (or schema module)?
508
708
  #
509
709
  # @param schema [Schema, SchemaModule]
@@ -512,12 +712,18 @@ module JSI
512
712
  if schema.is_a?(Schema)
513
713
  jsi_schemas.include?(schema)
514
714
  elsif schema.is_a?(SchemaModule)
515
- jsi_schema_modules.include?(schema)
715
+ jsi_schemas.include?(schema.schema)
516
716
  else
517
717
  raise(TypeError, "expected a Schema or Schema Module; got: #{schema.pretty_inspect.chomp}")
518
718
  end
519
719
  end
520
720
 
721
+ # Is this a JSI Schema?
722
+ # @return [Boolean]
723
+ def jsi_is_schema?
724
+ false
725
+ end
726
+
521
727
  # yields the content of this JSI's instance. the block must result in
522
728
  # a modified copy of the yielded instance (not modified in place, which would alter this JSI
523
729
  # as well) which will be used to instantiate and return a new JSI with the modified content.
@@ -527,15 +733,37 @@ module JSI
527
733
  #
528
734
  # @yield [Object] this JSI's instance. the block should result
529
735
  # in a nondestructively modified copy of this.
530
- # @return [JSI::Base subclass] the modified copy of self
531
- def jsi_modified_copy(&block)
736
+ # @return [Base] the modified copy of self
737
+ def jsi_modified_copy(**conf_kw, &block)
532
738
  modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
533
- modified_jsi_root_node = @jsi_root_node.jsi_indicated_schemas.new_jsi(modified_document,
534
- uri: @jsi_root_node.jsi_schema_base_uri,
535
- register: false, # default is already false but this is a place to be explicit
536
- 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),
755
+ mutable: jsi_mutable?,
537
756
  )
538
- 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)
539
767
  end
540
768
 
541
769
  # Is the instance an array?
@@ -562,9 +790,15 @@ module JSI
562
790
  false
563
791
  end
564
792
 
793
+ # Is this JSI mutable?
794
+ # @return [Boolean]
795
+ def jsi_mutable?
796
+ # note: overridden by Base::Mutable / Immutable
797
+ end
798
+
565
799
  # validates this JSI's instance against its schemas
566
800
  #
567
- # @return [JSI::Validation::FullResult]
801
+ # @return [JSI::Validation::Result::Full]
568
802
  def jsi_validate
569
803
  jsi_indicated_schemas.instance_validate(self)
570
804
  end
@@ -575,6 +809,15 @@ module JSI
575
809
  jsi_indicated_schemas.instance_valid?(self)
576
810
  end
577
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
+
578
821
  # queries this JSI using the [JMESPath Ruby](https://rubygems.org/gems/jmespath) gem.
579
822
  # see [https://jmespath.org/](https://jmespath.org/) to learn the JMESPath query language.
580
823
  #
@@ -592,41 +835,161 @@ module JSI
592
835
  JMESPath.search(expression, self, **runtime_options)
593
836
  end
594
837
 
595
- def dup
596
- 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
597
845
  end
598
846
 
599
- # a string representing this JSI, indicating any named schemas and inspecting its instance
600
- # @return [String]
601
- def inspect
602
- -"\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
847
+ # @private
848
+ # @return [URI, nil]
849
+ def jsi_root_uri
850
+ jsi_conf.root_uri
603
851
  end
604
852
 
605
- alias_method :to_s, :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
867
+ end
606
868
 
607
- # 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]
608
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.
609
943
  def pretty_print(q)
610
- q.text '#<'
611
- q.text jsi_object_group_text.join(' ')
612
- q.group_sub {
613
- q.nest(2) {
614
- q.breakable ' '
944
+ jsi_pp_object_group(q, jsi_object_group_text) do
615
945
  q.pp jsi_instance
616
- }
617
- }
618
- q.breakable ''
619
- q.text '>'
946
+ end
620
947
  end
621
948
 
622
949
  # @private
623
950
  # @return [Array<String>]
624
951
  def jsi_object_group_text
625
- 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
+
626
987
  if schema_names.empty?
627
- 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(' + ')})"
628
991
  else
629
- class_txt = -"JSI (#{schema_names.join(', ')})"
992
+ schemas_txt = -" (#{schema_names.join(' + ')} + #{jsi_schemas.size - schema_names.size})"
630
993
  end
631
994
 
632
995
  if (is_a?(ArrayNode) || is_a?(HashNode)) && ![Array, Hash].include?(jsi_node_content.class)
@@ -640,10 +1003,11 @@ module JSI
640
1003
  end
641
1004
 
642
1005
  [
643
- class_txt,
644
- is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
1006
+ -"JSI#{is_a?(MetaSchemaNode) ? ":MSN" : ""}#{schemas_txt}",
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,
645
1009
  *content_txt,
646
- ].compact
1010
+ ].compact.freeze
647
1011
  end
648
1012
 
649
1013
  # A structure coerced to JSONifiable types from the instance content.
@@ -656,74 +1020,116 @@ module JSI
656
1020
  # Calls {Util.to_json} with the instance and any given options.
657
1021
  # @return [String]
658
1022
  def to_json(options = {})
659
- 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
660
1031
  end
661
1032
 
662
1033
  # see {Util::Private::FingerprintHash}
663
1034
  # @api private
664
1035
  def jsi_fingerprint
665
1036
  {
666
- class: jsi_class,
1037
+ class: JSI::Base,
1038
+ jsi_schemas: jsi_schemas,
667
1039
  jsi_document: jsi_document,
668
1040
  jsi_ptr: jsi_ptr,
669
1041
  # for instances in documents with schemas:
670
- jsi_resource_ancestor_uri: jsi_resource_ancestor_uri,
671
- # different registries mean references may resolve to different resources so must not be equal
672
- jsi_schema_registry: jsi_schema_registry,
673
- }
1042
+ jsi_base_uri: jsi_base_uri,
1043
+ jsi_root_uri: jsi_conf.root_uri,
1044
+ # different dynamic anchor map means dynamic references may resolve to different resources so must not be equal
1045
+ jsi_schema_dynamic_anchor_map: jsi_schema_dynamic_anchor_map,
1046
+ **jsi_conf.for_fingerprint,
1047
+ }.freeze
1048
+ end
1049
+
1050
+ # @private
1051
+ def jsi_next_schema_dynamic_anchor_map
1052
+ jsi_schema_dynamic_anchor_map
674
1053
  end
675
- include Util::FingerprintHash
676
1054
 
677
1055
  private
678
1056
 
679
- def jsi_memomaps_initialize
680
- @child_indicated_schemas_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_indicated_schemas_compute))
681
- @child_applied_schemas_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_applied_schemas_compute))
682
- @child_node_map = jsi_memomap(key_by: proc { |i| i[:token] }, &method(:jsi_child_node_compute))
683
- end
1057
+ BY_TOKEN = proc { |i| i[:token] }
684
1058
 
685
- def jsi_indicated_schemas=(jsi_indicated_schemas)
686
- #chkbug raise(Bug) unless jsi_indicated_schemas.is_a?(SchemaSet)
687
- @jsi_indicated_schemas = jsi_indicated_schemas
1059
+ def jsi_memomaps_initialize
1060
+ @child_indicated_schemas_map = jsi_memomap(key_by: BY_TOKEN, &method(:jsi_child_indicated_schemas_compute))
1061
+ @child_applied_schemas_map = jsi_memomap(key_by: BY_TOKEN, &method(:jsi_child_applied_schemas_compute))
688
1062
  end
689
1063
 
690
1064
  def jsi_child_node_compute(token: , child_indicated_schemas: , child_applied_schemas: , includes: )
691
1065
  jsi_class = JSI::SchemaClasses.class_for_schemas(child_applied_schemas,
692
1066
  includes: includes,
1067
+ mutable: jsi_mutable?,
693
1068
  )
694
- jsi_class.new(@jsi_document,
1069
+ jsi_class.new(
1070
+ jsi_document: @jsi_document,
695
1071
  jsi_ptr: @jsi_ptr[token],
696
1072
  jsi_indicated_schemas: child_indicated_schemas,
697
- jsi_schema_base_uri: jsi_resource_ancestor_uri,
1073
+ jsi_base_uri: jsi_next_base_uri,
698
1074
  jsi_schema_resource_ancestors: is_a?(Schema) ? jsi_subschema_resource_ancestors : jsi_schema_resource_ancestors,
699
- jsi_schema_registry: jsi_schema_registry,
1075
+ jsi_schema_dynamic_anchor_map: jsi_child_dynamic_anchor_map(token: token, child_schemas: child_applied_schemas),
700
1076
  jsi_root_node: @jsi_root_node,
701
- )
1077
+ ).send(:jsi_initialized)
1078
+ end
1079
+
1080
+ def jsi_child_dynamic_anchor_map(token: , child_schemas: )
1081
+ metaschema = child_schemas.detect(&:describes_schema?)
1082
+ # note: no memoized dialect.bootstrap_schema; this is only used once.
1083
+ 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?
1084
+ # if the child is a resource root, compute dynamic scope for it
1085
+ # TODO if without_node removes the child, that probably means the child is equal to a descendent
1086
+ # of another dynamic_root. not sure if this should find/return that child - one one hand
1087
+ # instantiating multiple equal nodes via different dynamic scopes isn't ideal; on the other
1088
+ # hand having child_node return a node with a different jsi_root_node may also be problematic.
1089
+ jsi_next_schema_dynamic_anchor_map.without_node(self, ptr: jsi_ptr[token])
1090
+ else
1091
+ # child is not a resource, has the same dynamic scope as self
1092
+ jsi_schema_dynamic_anchor_map
1093
+ end
702
1094
  end
703
1095
 
704
1096
  def jsi_child_indicated_schemas_compute(token: , content: )
705
- jsi_schemas.child_applicator_schemas(token, content)
1097
+ if jsi_schemas.any?(&:application_requires_evaluated)
1098
+ # if application_requires_evaluated, in-place application needs to collect token evaluation
1099
+ # recursively to inform child application, so must be recomputed.
1100
+ jsi_indicated_schemas.each_yield_set do |is, y|
1101
+ is.each_inplace_child_applicator_schema(token, content,
1102
+ collect_evaluated_validate: jsi_conf.application_collect_evaluated_validate,
1103
+ &y
1104
+ )
1105
+ end
1106
+ else
1107
+ # if token evaluation does not need to be collected, use our already-computed #jsi_schemas.
1108
+ jsi_schemas.each_yield_set do |s, y|
1109
+ s.each_child_applicator_schema(token, content, &y)
1110
+ end
1111
+ end
706
1112
  end
707
1113
 
708
1114
  def jsi_child_applied_schemas_compute(token: , child_indicated_schemas: , child_content: )
709
- child_indicated_schemas.inplace_applicator_schemas(child_content)
1115
+ child_indicated_schemas.each_yield_set do |cis, y|
1116
+ cis.each_inplace_applicator_schema(child_content, &y)
1117
+ end
710
1118
  end
711
1119
 
712
- def jsi_child_as_jsi(child_content, child_schemas, as_jsi)
1120
+ def jsi_child_as_jsi(child_node, as_jsi)
713
1121
  if [true, false].include?(as_jsi)
714
1122
  child_as_jsi = as_jsi
715
1123
  elsif as_jsi == :auto
716
- child_is_complex = child_content.respond_to?(:to_hash) || child_content.respond_to?(:to_ary)
717
- child_is_schema = child_schemas.any?(&:describes_schema?)
718
- child_as_jsi = child_is_complex || child_is_schema
1124
+ child_as_jsi = child_node.jsi_as_child_default_as_jsi
719
1125
  else
720
1126
  raise(ArgumentError, "as_jsi must be one of: :auto, true, false")
721
1127
  end
722
1128
 
723
1129
  if child_as_jsi
724
- yield
1130
+ child_node
725
1131
  else
726
- child_content
1132
+ child_node.jsi_node_content
727
1133
  end
728
1134
  end
729
1135
 
@@ -734,5 +1140,16 @@ module JSI
734
1140
  "instance: #{jsi_instance.pretty_inspect.chomp}",
735
1141
  ].join("\n"))
736
1142
  end
1143
+
1144
+ # Note that #initialize does not invoke the jsi_conf.after_initialize callback, because
1145
+ # the node is not finished initializing when #initialize returns; other included modules
1146
+ # or subclasses with #initialize methods calling super still need to finish.
1147
+ # Places where Base is directly instantiated must call #jsi_initialized.
1148
+ # @return [self]
1149
+ def jsi_initialized
1150
+ #chkbug fail if defined?(super) # this should be the last jsi_initialized in class ancestry
1151
+ jsi_conf.after_initialize.call(self) if jsi_conf.after_initialize
1152
+ self
1153
+ end
737
1154
  end
738
1155
  end