jsi-dev 0.0.8 → 0.0.9

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