Class: JSI::Base

Inherits:
Object
  • Object
show all
Includes:
Enumerable, PathedNode, 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

Classes: CannotSubscriptError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Util::FingerprintHash

#==, #hash

Methods included from Schema::SchemaAncestorNode

#jsi_anchor_subschema, #jsi_anchor_subschemas, #jsi_resource_ancestor_uri

Methods included from PathedNode

#jsi_node_content

Constructor Details

#initialize(instance, jsi_document: nil, jsi_ptr: nil, jsi_root_node: nil, jsi_schema_base_uri: nil, jsi_schema_resource_ancestors: []) ⇒ Base

initializes this JSI from the given instance - instance is most commonly a parsed JSON document consisting of Hash, Array, or sometimes a basic type, but this is in no way enforced and a JSI may wrap any object.

Parameters:

  • instance (Object)

    the JSON Schema instance to be represented as a JSI

  • jsi_document (Object) (defaults to: nil)

    for internal use. the instance may be specified as a node in the jsi_document param, pointed to by jsi_ptr. the param instance MUST be NOINSTANCE to use the jsi_document + jsi_ptr form. jsi_document MUST NOT be passed if instance is anything other than NOINSTANCE.

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

    for internal use. a pointer specifying the path of this instance in the jsi_document param. jsi_ptr must be passed iff jsi_document is passed, i.e. when instance is NOINSTANCE

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

    for internal use, specifies the JSI at the root of the document

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

    see SchemaSet#new_jsi param uri

  • jsi_schema_resource_ancestors (Array<JSI::Base>) (defaults to: [])

145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/jsi/base.rb', line 145

def initialize(instance,
    jsi_document: nil,
    jsi_ptr: nil,
    jsi_root_node: nil,
    jsi_schema_base_uri: nil,
    jsi_schema_resource_ancestors: []
)
  unless respond_to?(:jsi_schemas)
    raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #jsi_schemas. it is recommended to instantiate JSIs from a schema using JSI::Schema#new_jsi.")
  end

  if instance.is_a?(JSI::Schema)
    raise(TypeError, "assigning a schema to a #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
  elsif instance.is_a?(JSI::Base)
    raise(TypeError, "assigning another JSI::Base instance to a #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
  end

  jsi_initialize_memos

  if instance == NOINSTANCE
    self.jsi_document = jsi_document
    self.jsi_ptr = jsi_ptr
    if @jsi_ptr.root?
      raise(Bug, "jsi_root_node cannot be specified for root JSI") if jsi_root_node
      @jsi_root_node = self
    else
      if !jsi_root_node.is_a?(JSI::Base)
        raise(TypeError, "jsi_root_node must be a JSI::Base; got: #{jsi_root_node.inspect}")
      end
      if !jsi_root_node.jsi_ptr.root?
        raise(Bug, "jsi_root_node ptr #{jsi_root_node.jsi_ptr.inspect} is not root")
      end
      @jsi_root_node = jsi_root_node
    end
  else
    raise(Bug, 'incorrect usage') if jsi_document || jsi_ptr || jsi_root_node
    @jsi_document = instance
    @jsi_ptr = Ptr[]
    @jsi_root_node = self
  end

  self.jsi_schema_base_uri = jsi_schema_base_uri
  self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors

  if self.jsi_instance.respond_to?(:to_hash)
    extend PathedHashNode
  end
  if self.jsi_instance.respond_to?(:to_ary)
    extend PathedArrayNode
  end
end

Instance Attribute Details

#jsi_documentObject (readonly)

document containing the instance of this JSI at our #jsi_ptr


204
205
206
# File 'lib/jsi/base.rb', line 204

def jsi_document
  @jsi_document
end

#jsi_ptrJSI::Ptr (readonly)

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

Returns:


208
209
210
# File 'lib/jsi/base.rb', line 208

def jsi_ptr
  @jsi_ptr
end

#jsi_root_nodeJSI::Base (readonly)

the JSI at the root of this JSI's document

Returns:


212
213
214
# File 'lib/jsi/base.rb', line 212

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)

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
72
73
74
75
76
77
78
# File 'lib/jsi/base.rb', line 47

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)

108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/jsi/base.rb', line 108

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)

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
105
106
107
108
109
110
111
# File 'lib/jsi/base.rb', line 80

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) and is described by some schemas
      • 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


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
404
405
406
407
408
409
# File 'lib/jsi/base.rb', line 368

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]
      end
    else
      if use_default
        defaults = Set.new
        subinstance_schemas.each do |subinstance_schema|
          if subinstance_schema.respond_to?(:to_hash) && subinstance_schema.key?('default')
            defaults << subinstance_schema['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


416
417
418
419
420
421
422
423
424
425
# File 'lib/jsi/base.rb', line 416

def []=(token, value)
  unless respond_to?(:to_hash) || respond_to?(:to_ary)
    raise(NoMethodError, "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)

555
556
557
# File 'lib/jsi/base.rb', line 555

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

#dupObject


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

def dup
  jsi_modified_copy(&:dup)
end

#each(*_) ⇒ Object

each is overridden by PathedHashNode or PathedArrayNode when appropriate. the base #each is not actually implemented, along with all the methods of Enumerable.

Raises:

  • (NoMethodError)

219
220
221
# File 'lib/jsi/base.rb', line 219

def each(*_)
  raise NoMethodError, "Enumerable methods and #each not implemented for instance that is not like a hash or array: #{jsi_instance.pretty_inspect.chomp}"
end

#inspectString

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

Returns:

  • (String)

494
495
496
# File 'lib/jsi/base.rb', line 494

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

#jsi_each_child_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 node in the document, starting with self

Returns:

  • (nil, Enumerator)

    an Enumerator if invoked without a block; otherwise nil


229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/jsi/base.rb', line 229

def jsi_each_child_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_child_node(&block)
    end
  elsif respond_to?(:to_ary)
    each_index do |i|
      self[i, as_jsi: true].jsi_each_child_node(&block)
    end
  end
  nil
end

#jsi_fingerprintObject

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


560
561
562
563
564
565
566
567
568
569
570
# File 'lib/jsi/base.rb', line 560

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 destructively modifying it) which will be used to instantiate 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


443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# File 'lib/jsi/base.rb', line 443

def jsi_modified_copy(&block)
  if @jsi_ptr.root?
    modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
    self.class.new(Base::NOINSTANCE,
      jsi_document: modified_document,
      jsi_ptr: @jsi_ptr,
      jsi_schema_base_uri: @jsi_schema_base_uri,
      jsi_schema_resource_ancestors: @jsi_schema_resource_ancestors, # this can only be empty but included for consistency
    )
  else
    modified_jsi_root_node = @jsi_root_node.jsi_modified_copy do |root|
      @jsi_ptr.modified_document_copy(root, &block)
    end
    @jsi_ptr.evaluate(modified_jsi_root_node, as_jsi: true)
  end
end

#jsi_parent_nodeJSI::Base?

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

Returns:


329
330
331
# File 'lib/jsi/base.rb', line 329

def jsi_parent_node
  jsi_parent_nodes.first
end

#jsi_parent_nodesArray<JSI::Base>

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

Returns:


316
317
318
319
320
321
322
323
324
# File 'lib/jsi/base.rb', line 316

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>)

429
430
431
# File 'lib/jsi/base.rb', line 429

def jsi_schema_modules
  jsi_schemas.map(&:jsi_schema_module).to_set.freeze
end

#jsi_schemasJSI::SchemaSet

the set of schemas which describe this instance

Returns:


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

#jsi_select_children_leaf_first {|JSI::Base| ... } ⇒ JSI::Base

recursively selects child nodes of this JSI, returning a modified copy of self containing only child 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:

Returns:

  • (JSI::Base)

    modified copy of self containing only the selected nodes


287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/jsi/base.rb', line 287

def jsi_select_children_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_children_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_children_leaf_first(&block)
        if yield(e)
          res << e.jsi_node_content
        end
      end
      res
    else
      instance
    end
  end
end

#jsi_select_children_node_first {|JSI::Base| ... } ⇒ JSI::Base

recursively selects child nodes of this JSI, returning a modified copy of self containing only child 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 children are never recursed.

Yields:

Returns:

  • (JSI::Base)

    modified copy of self containing only the selected nodes


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

def jsi_select_children_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_children_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_children_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)

469
470
471
# File 'lib/jsi/base.rb', line 469

def jsi_valid?
  jsi_schemas.instance_valid?(self)
end

#jsi_validateJSI::Validation::FullResult

validates this JSI's instance against its schemas


463
464
465
# File 'lib/jsi/base.rb', line 463

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


500
501
502
503
504
505
506
507
508
509
510
511
# File 'lib/jsi/base.rb', line 500

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