Class: JSI::Base

Inherits:
Object
  • Object
show all
Includes:
Schema::SchemaAncestorNode, Util::FingerprintHash, Util::Memoize
Defined in:
lib/jsi/base.rb

Overview

JSI::Base is the base class of every JSI instance of a JSON schema.

instances are described by a set of one or more JSON schemas. JSI dynamically creates a subclass of JSI::Base for each set of JSON schemas which describe an instance that is to be instantiated.

a JSI instance of such a subclass represents a JSON schema instance described by that set of schemas.

this subclass includes the JSI Schema Module of each schema it represents.

the method #jsi_schemas is defined to indicate the schemas the class represents.

the JSI::Base class itself is not intended to be instantiated.

Direct Known Subclasses

MetaschemaNode

Defined Under Namespace

Modules: ArrayNode, Enumerable, HashNode Classes: CannotSubscriptError

Instance Attribute Summary collapse

Attributes included from Schema::SchemaAncestorNode

#jsi_schema_base_uri

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Schema::SchemaAncestorNode

#jsi_anchor_subschema, #jsi_anchor_subschemas, #jsi_resource_ancestor_uri, #jsi_schema_resource_ancestors

Constructor Details

#initialize(jsi_document, jsi_ptr: , jsi_root_node: nil, jsi_schema_base_uri: nil, jsi_schema_resource_ancestors: Util::EMPTY_ARY) ⇒ Base

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

initializes a JSI whose instance is in the given document at the given pointer.

this is a private api - users should look elsewhere to instantiate JSIs, in particular:

Parameters:

  • jsi_document (Object)

    the document containing the instance

  • jsi_ptr (JSI::Ptr) (defaults to: )

    a pointer pointing to the JSI's instance in the document

  • jsi_root_node (JSI::Base) (defaults to: nil)

    the JSI of the root of the document containing this JSI

  • jsi_schema_base_uri (Addressable::URI) (defaults to: nil)

    see SchemaSet#new_jsi param uri

  • jsi_schema_resource_ancestors (Array<JSI::Base<JSI::Schema>>) (defaults to: Util::EMPTY_ARY)

Raises:



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/jsi/base.rb', line 129

def initialize(jsi_document,
    jsi_ptr: Ptr[],
    jsi_root_node: nil,
    jsi_schema_base_uri: nil,
    jsi_schema_resource_ancestors: Util::EMPTY_ARY
)
  raise(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)

  jsi_initialize_memos

  self.jsi_document = jsi_document
  self.jsi_ptr = jsi_ptr
  if @jsi_ptr.root?
    raise(Bug, "jsi_root_node specified for root JSI") if jsi_root_node
    @jsi_root_node = self
  else
    raise(Bug, "jsi_root_node is not JSI::Base") if !jsi_root_node.is_a?(JSI::Base)
    raise(Bug, "jsi_root_node ptr is not root") if !jsi_root_node.jsi_ptr.root?
    @jsi_root_node = jsi_root_node
  end
  self.jsi_schema_base_uri = jsi_schema_base_uri
  self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors

  if jsi_instance.is_a?(JSI::Base)
    raise(TypeError, "a JSI::Base instance must not be another JSI::Base. received: #{jsi_instance.pretty_inspect.chomp}")
  end
end

Instance Attribute Details

#jsi_documentObject (readonly)

document containing the instance of this JSI at our #jsi_ptr



164
165
166
# File 'lib/jsi/base.rb', line 164

def jsi_document
  @jsi_document
end

#jsi_ptrJSI::Ptr (readonly)

Ptr pointing to this JSI's instance within our #jsi_document

Returns:



168
169
170
# File 'lib/jsi/base.rb', line 168

def jsi_ptr
  @jsi_ptr
end

#jsi_root_nodeJSI::Base (readonly)

the JSI at the root of this JSI's document

Returns:



172
173
174
# File 'lib/jsi/base.rb', line 172

def jsi_root_node
  @jsi_root_node
end

Class Method Details

.inspectString

a string indicating a class name if one is defined, as well as the schema module name and/or schema URI of each schema the class represents.

Returns:

  • (String)


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/jsi/base.rb', line 40

def inspect
  if !respond_to?(:jsi_class_schemas)
    super
  else
    schema_names = jsi_class_schemas.map do |schema|
      mod = schema.jsi_schema_module
      if mod.name && schema.schema_uri
        "#{mod.name} (#{schema.schema_uri})"
      elsif mod.name
        mod.name
      elsif schema.schema_uri
        schema.schema_uri.to_s
      else
        schema.jsi_ptr.uri.to_s
      end
    end

    if name && !in_schema_classes
      if jsi_class_schemas.empty?
        "#{name} (0 schemas)"
      else
        "#{name} (#{schema_names.join(', ')})"
      end
    else
      if schema_names.empty?
        "(JSI Schema Class for 0 schemas)"
      else
        "(JSI Schema Class: #{schema_names.join(', ')})"
      end
    end
  end
end

.nameString

a constant name of this class. this is generated from the schema module name or URI of each schema this class represents. nil if any represented schema has no schema module name or schema URI.

this generated name is not too pretty but can be more helpful than an anonymous class, especially in error messages.

Returns:

  • (String)


102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/jsi/base.rb', line 102

def name
  unless instance_variable_defined?(:@in_schema_classes)
    const_name = schema_classes_const_name
    if super || !const_name || SchemaClasses.const_defined?(const_name)
      @in_schema_classes = false
    else
      SchemaClasses.const_set(const_name, self)
      @in_schema_classes = true
    end
  end
  super
end

.to_sString

a string indicating a class name if one is defined, as well as the schema module name and/or schema URI of each schema the class represents.

Returns:

  • (String)


73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/jsi/base.rb', line 73

def inspect
  if !respond_to?(:jsi_class_schemas)
    super
  else
    schema_names = jsi_class_schemas.map do |schema|
      mod = schema.jsi_schema_module
      if mod.name && schema.schema_uri
        "#{mod.name} (#{schema.schema_uri})"
      elsif mod.name
        mod.name
      elsif schema.schema_uri
        schema.schema_uri.to_s
      else
        schema.jsi_ptr.uri.to_s
      end
    end

    if name && !in_schema_classes
      if jsi_class_schemas.empty?
        "#{name} (0 schemas)"
      else
        "#{name} (#{schema_names.join(', ')})"
      end
    else
      if schema_names.empty?
        "(JSI Schema Class for 0 schemas)"
      else
        "(JSI Schema Class: #{schema_names.join(', ')})"
      end
    end
  end
end

Instance Method Details

#[](token, as_jsi: :auto, use_default: true) ⇒ JSI::Base, Object

subscripts to return a child value identified by the given token.

Parameters:

  • token (String, Integer, Object)

    an array index or hash key (JSON object property name) of the instance identifying the child value

  • as_jsi (:auto, true, false) (defaults to: :auto)

    whether to return the result value as a JSI. one of:

    • :auto (default): by default a JSI will be returned when either:

      • the result is a complex value (responds to #to_ary or #to_hash)
      • the result is a schema (including true/false schemas)

    a plain value is returned when no schemas are known to describe the instance, or when the value is a simple type (anything unresponsive to #to_ary / #to_hash).

    • true: the result value will always be returned as a JSI. the #jsi_schemas of the result may be empty if no schemas describe the instance.
    • false: the result value will always be the plain instance.

    note that nil is returned (regardless of as_jsi) when there is no value to return because the token is not a hash key or array index of the instance and no default value applies. (one exception is when this JSI's instance is a Hash with a default or default_proc, which has unspecified behavior.)

  • use_default (true, false) (defaults to: true)

    whether to return a schema default value when the token is not in range. if the token is not an array index or hash key of the instance, and one schema for the child instance specifies a default value, that default is returned.

    if the result with the default value is a JSI (per the as_jsi param), that JSI is not a child of this JSI - this JSI is not modified to fill in the default value. the result is a JSI within a new document containing the filled-in default.

    if the child instance's schemas do not indicate a single default value (that is, if zero or multiple defaults are specified across those schemas), nil is returned. (one exception is when this JSI's instance is a Hash with a default or default_proc, which has unspecified behavior.)

Returns:

  • (JSI::Base, Object)

    the child value identified by the subscript token



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/jsi/base.rb', line 358

def [](token, as_jsi: :auto, use_default: true)
  if respond_to?(:to_hash)
    token_in_range = jsi_node_content_hash_pubsend(:key?, token)
    value = jsi_node_content_hash_pubsend(:[], token)
  elsif respond_to?(:to_ary)
    token_in_range = jsi_node_content_ary_pubsend(:each_index).include?(token)
    value = jsi_node_content_ary_pubsend(:[], token)
  else
    raise(CannotSubscriptError, "cannot subscript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
  end

  begin
    subinstance_schemas = jsi_subinstance_schemas_memos[token: token, instance: jsi_node_content, subinstance: value]

    if token_in_range
      jsi_subinstance_as_jsi(value, subinstance_schemas, as_jsi) do
        jsi_subinstance_memos[
          token: token,
          subinstance_schemas: subinstance_schemas,
          includes: SchemaClasses.includes_for(value),
        ]
      end
    else
      if use_default
        defaults = Set.new
        subinstance_schemas.each do |subinstance_schema|
          if subinstance_schema.keyword?('default')
            defaults << subinstance_schema.jsi_node_content['default']
          end
        end
      end

      if use_default && defaults.size == 1
        # use the default value
        # we are using #dup so that we get a modified copy of self, in which we set dup[token]=default.
        dup.tap { |o| o[token] = defaults.first }[token, as_jsi: as_jsi]
      else
        # I kind of want to just return nil here. the preferred mechanism for
        # a JSI's default value should be its schema. but returning nil ignores
        # any value returned by Hash#default/#default_proc. there's no compelling
        # reason not to support both, so I'll return that.
        value
      end
    end
  end
end

#[]=(token, value) ⇒ Object

assigns the subscript of the instance identified by the given token to the given value. if the value is a JSI, its instance is assigned instead of the JSI value itself.

Parameters:

  • token (String, Integer, Object)

    token identifying the subscript to assign

  • value (JSI::Base, Object)

    the value to be assigned



410
411
412
413
414
415
416
417
418
419
# File 'lib/jsi/base.rb', line 410

def []=(token, value)
  unless respond_to?(:to_hash) || respond_to?(:to_ary)
    raise(CannotSubscriptError, "cannot assign subscript (using token: #{token.inspect}) to instance: #{jsi_instance.pretty_inspect.chomp}")
  end
  if value.is_a?(Base)
    self[token] = value.jsi_instance
  else
    jsi_instance[token] = value
  end
end

#as_json(*opt) ⇒ Object

a jsonifiable representation of the instance

Returns:

  • (Object)


537
538
539
# File 'lib/jsi/base.rb', line 537

def as_json(*opt)
  Util.as_json(jsi_instance, *opt)
end

#dupObject



481
482
483
# File 'lib/jsi/base.rb', line 481

def dup
  jsi_modified_copy(&:dup)
end

#inspectString Also known as: to_s

a string representing this JSI, indicating any named schemas and inspecting its instance

Returns:

  • (String)


487
488
489
# File 'lib/jsi/base.rb', line 487

def inspect
  "\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
end

#jmespath_search(expression, **runtime_options) ⇒ Array, ...

queries this JSI using the JMESPath Ruby gem. see https://jmespath.org/ to learn the JMESPath query language.

the JMESPath gem is not a dependency of JSI, so must be installed / added to your Gemfile to use. e.g. gem 'jmespath', '~> 1.5'. note that versions below 1.5 are not compatible with JSI.

Parameters:

  • expression (String)

    a JMESPath expression

  • runtime_options

    passed to JMESPath.search, though no runtime_options are publicly documented or normally used.

Returns:



475
476
477
478
479
# File 'lib/jsi/base.rb', line 475

def jmespath_search(expression, **runtime_options)
  Util.require_jmespath

  JMESPath.search(expression, self, **runtime_options)
end

#jsi_ancestor_nodesArray<JSI::Base>

ancestor JSI instances from this node up to the root. this node itself is always its own first ancestor.

Returns:



302
303
304
305
306
307
308
309
310
311
312
# File 'lib/jsi/base.rb', line 302

def jsi_ancestor_nodes
  ancestors = []
  ancestor = jsi_root_node
  ancestors << ancestor

  jsi_ptr.tokens.each do |token|
    ancestor = ancestor[token, as_jsi: true]
    ancestors << ancestor
  end
  ancestors.reverse!.freeze
end

#jsi_descendent_node(ptr) ⇒ JSI::Base

the descendent node at the given pointer

Parameters:

Returns:



318
319
320
321
# File 'lib/jsi/base.rb', line 318

def jsi_descendent_node(ptr)
  descendent = Ptr.ary_ptr(ptr).evaluate(self, as_jsi: true)
  descendent
end

#jsi_each_descendent_node {|JSI::Base| ... } ⇒ nil, Enumerator

yields a JSI of each node at or below this one in this JSI's document.

returns an Enumerator if no block is given.

Yields:

  • (JSI::Base)

    each descendent node, starting with self

Returns:

  • (nil, Enumerator)

    an Enumerator if invoked without a block; otherwise nil



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/jsi/base.rb', line 189

def jsi_each_descendent_node(&block)
  return to_enum(__method__) unless block

  yield self
  if respond_to?(:to_hash)
    each_key do |k|
      self[k, as_jsi: true].jsi_each_descendent_node(&block)
    end
  elsif respond_to?(:to_ary)
    each_index do |i|
      self[i, as_jsi: true].jsi_each_descendent_node(&block)
    end
  end
  nil
end

#jsi_fingerprintObject

an opaque fingerprint of this JSI for Util::Private::FingerprintHash.



542
543
544
545
546
547
548
549
550
551
552
# File 'lib/jsi/base.rb', line 542

def jsi_fingerprint
  {
    class: jsi_class,
    jsi_document: jsi_document,
    jsi_ptr: jsi_ptr,
    # for instances in documents with schemas:
    jsi_resource_ancestor_uri: jsi_resource_ancestor_uri,
    # only defined for JSI::Schema instances:
    jsi_schema_instance_modules: is_a?(Schema) ? jsi_schema_instance_modules : nil,
  }
end

#jsi_modified_copy {|Object| ... } ⇒ JSI::Base subclass

yields the content of this JSI's instance. the block must result in a modified copy of the yielded instance (not modified in place, which would alter this JSI as well) which will be used to instantiate and return a new JSI with the modified content.

the result may have different schemas which describe it than this JSI's schemas, if conditional applicator schemas apply differently to the modified instance.

Yields:

  • (Object)

    this JSI's instance. the block should result in a nondestructively modified copy of this.

Returns:

  • (JSI::Base subclass)

    the modified copy of self



437
438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/jsi/base.rb', line 437

def jsi_modified_copy(&block)
  if @jsi_ptr.root?
    modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
    jsi_schemas.new_jsi(modified_document,
      uri: jsi_schema_base_uri,
    )
  else
    modified_jsi_root_node = @jsi_root_node.jsi_modified_copy do |root|
      @jsi_ptr.modified_document_copy(root, &block)
    end
    modified_jsi_root_node.jsi_descendent_node(@jsi_ptr)
  end
end

#jsi_node_contentObject Also known as: jsi_instance

the content of this node in our #jsi_document at our #jsi_ptr. the same as #jsi_instance.



175
176
177
178
# File 'lib/jsi/base.rb', line 175

def jsi_node_content
  content = jsi_ptr.evaluate(jsi_document)
  content
end

#jsi_parent_nodeJSI::Base?

the immediate parent of this JSI. nil if there is no parent.

Returns:



295
296
297
# File 'lib/jsi/base.rb', line 295

def jsi_parent_node
  jsi_ptr.root? ? nil : jsi_root_node.jsi_descendent_node(jsi_ptr.parent)
end

#jsi_parent_nodesArray<JSI::Base>

an array of JSI instances above this one in the document.

Returns:



282
283
284
285
286
287
288
289
290
# File 'lib/jsi/base.rb', line 282

def jsi_parent_nodes
  parent = jsi_root_node

  jsi_ptr.tokens.map do |token|
    parent.tap do
      parent = parent[token, as_jsi: true]
    end
  end.reverse
end

#jsi_schema_modulesSet<Module>

the set of JSI schema modules corresponding to the schemas that describe this JSI

Returns:

  • (Set<Module>)


423
424
425
# File 'lib/jsi/base.rb', line 423

def jsi_schema_modules
  Util.ensure_module_set(jsi_schemas.map(&:jsi_schema_module))
end

#jsi_schemasJSI::SchemaSet

the set of schemas which describe this instance

Returns:



# File 'lib/jsi/base.rb', line 157

#jsi_select_descendents_leaf_first {|JSI::Base| ... } ⇒ JSI::Base Also known as: jsi_select_children_leaf_first

recursively selects descendent nodes of this JSI, returning a modified copy of self containing only descendent nodes for which the given block had a true-ish result.

this method recursively descends child nodes before yielding each node, so leaf nodes are yielded before their parents.

Yields:

  • (JSI::Base)

    each descendent node below self

Returns:

  • (JSI::Base)

    modified copy of self containing only the selected nodes



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/jsi/base.rb', line 250

def jsi_select_descendents_leaf_first(&block)
  jsi_modified_copy do |instance|
    if respond_to?(:to_hash)
      res = instance.class.new
      each_key do |k|
        v = self[k, as_jsi: true].jsi_select_descendents_leaf_first(&block)
        if yield(v)
          res[k] = v.jsi_node_content
        end
      end
      res
    elsif respond_to?(:to_ary)
      res = instance.class.new
      each_index do |i|
        e = self[i, as_jsi: true].jsi_select_descendents_leaf_first(&block)
        if yield(e)
          res << e.jsi_node_content
        end
      end
      res
    else
      instance
    end
  end
end

#jsi_select_descendents_node_first {|JSI::Base| ... } ⇒ JSI::Base Also known as: jsi_select_children_node_first

recursively selects descendent nodes of this JSI, returning a modified copy of self containing only descendent nodes for which the given block had a true-ish result.

this method yields a node before recursively descending to its child nodes, so leaf nodes are yielded last, after their parents. if a node is not selected, its descendents are never recursed.

Yields:

  • (JSI::Base)

    each descendent node below self

Returns:

  • (JSI::Base)

    modified copy of self containing only the selected nodes



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/jsi/base.rb', line 213

def jsi_select_descendents_node_first(&block)
  jsi_modified_copy do |instance|
    if respond_to?(:to_hash)
      res = instance.class.new
      each_key do |k|
        v = self[k, as_jsi: true]
        if yield(v)
          res[k] = v.jsi_select_descendents_node_first(&block).jsi_node_content
        end
      end
      res
    elsif respond_to?(:to_ary)
      res = instance.class.new
      each_index do |i|
        e = self[i, as_jsi: true]
        if yield(e)
          res << e.jsi_select_descendents_node_first(&block).jsi_node_content
        end
      end
      res
    else
      instance
    end
  end
end

#jsi_valid?Boolean

whether this JSI's instance is valid against all of its schemas

Returns:

  • (Boolean)


460
461
462
# File 'lib/jsi/base.rb', line 460

def jsi_valid?
  jsi_schemas.instance_valid?(self)
end

#jsi_validateJSI::Validation::FullResult

validates this JSI's instance against its schemas



454
455
456
# File 'lib/jsi/base.rb', line 454

def jsi_validate
  jsi_schemas.instance_validate(self)
end

#pretty_print(q) ⇒ void

This method returns an undefined value.

pretty-prints a representation of this JSI to the given printer



495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/jsi/base.rb', line 495

def pretty_print(q)
  q.text '#<'
  q.text jsi_object_group_text.join(' ')
  q.group_sub {
    q.nest(2) {
      q.breakable ' '
      q.pp jsi_instance
    }
  }
  q.breakable ''
  q.text '>'
end