Class: JSI::Base

Inherits:
Object
  • Object
show all
Includes:
Enumerable, FingerprintHash, Memoize, PathedNode
Defined in:
lib/jsi/base.rb,
lib/jsi/base/to_rb.rb

Overview

the base class for representing and instantiating a JSON Schema.

a class inheriting from JSI::Base represents a JSON Schema. an instance of that class represents a JSON schema instance.

as such, JSI::Base itself is not intended to be instantiated - subclasses are dynamically created for schemas using class_for_schema, and these are what are used to instantiate and represent JSON schema instances.

Constant Summary collapse

NOINSTANCE =

NOINSTANCE is a magic value passed to #initialize when instantiating a JSI from a document and JSON Pointer.

Object.new.tap { |o| [:inspect, :to_s].each(&(-> (s, m) { o.define_singleton_method(m) { s } }.curry.([JSI::Base.name, 'NOINSTANCE'].join('::')))) }

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from FingerprintHash

#==, #hash

Methods included from PathedNode

#node_content, #node_ptr_deref

Methods included from Memoize

#jsi_clear_memo, #jsi_memoize

Constructor Details

#initialize(instance, jsi_document: nil, jsi_ptr: nil, jsi_root_node: nil) ⇒ 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 being represented

  • 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::JSON::Pointer) (defaults to: nil)

    for internal use. a JSON 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



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/jsi/base.rb', line 97

def initialize(instance, jsi_document: nil, jsi_ptr: nil, jsi_root_node: nil)
  unless respond_to?(:schema)
    raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #schema. please use JSI.class_for_schema")
  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

  if instance == NOINSTANCE
    @jsi_document = jsi_document
    unless jsi_ptr.is_a?(JSI::JSON::Pointer)
      raise(TypeError, "jsi_ptr must be a JSI::JSON::Pointer; got: #{jsi_ptr.inspect}")
    end
    @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 = JSI::JSON::Pointer.new([])
    @jsi_root_node = self
  end

  if self.jsi_instance.respond_to?(:to_hash)
    extend BaseHash
  elsif self.jsi_instance.respond_to?(:to_ary)
    extend BaseArray
  end
  if self.schema.describes_schema?
    extend JSI::Schema
  end
end

Instance Attribute Details

#jsi_documentObject (readonly) Also known as: node_document

document containing the instance of this JSI



144
145
146
# File 'lib/jsi/base.rb', line 144

def jsi_document
  @jsi_document
end

#jsi_ptrObject (readonly) Also known as: node_ptr

JSI::JSON::Pointer pointing to this JSI's instance within the jsi_document



147
148
149
# File 'lib/jsi/base.rb', line 147

def jsi_ptr
  @jsi_ptr
end

#jsi_root_nodeObject (readonly) Also known as: document_root_node

the JSI at the root of this JSI's document



150
151
152
# File 'lib/jsi/base.rb', line 150

def jsi_root_node
  @jsi_root_node
end

Class Method Details

.class_commentObject



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
# File 'lib/jsi/base/to_rb.rb', line 6

def class_comment
  lines = []

  description = schema &&
    schema['description'].respond_to?(:to_str) &&
    schema['description'].to_str
  if description
    description.split("\n", -1).each do |descline|
      lines << "# " + descline
    end
    lines << "#"
  end

  schema.described_object_property_names.each_with_index do |propname, i|
    lines << "#" unless i == 0
    lines << "# @!attribute [rw] #{propname}"

    property_schema = schema['properties'].respond_to?(:to_hash) &&
      schema['properties'][propname].respond_to?(:to_hash) &&
      schema['properties'][propname]

    required = property_schema && property_schema['required']
    required ||= schema['required'].respond_to?(:to_ary) && schema['required'].include?(propname)
    lines << "#   @required" if required

    type = property_schema &&
      property_schema['type'].respond_to?(:to_str) &&
      property_schema['type'].to_str
    simple = {'string' => 'String', 'number' => 'Numeric', 'boolean' => 'Boolean', 'null' => 'nil'}
    rettypes = []
    if simple.key?(type)
      rettypes << simple[type]
    elsif type == 'object' || type == 'array'
      rettypes = []
      schema_class = JSI.class_for_schema(property_schema)
      unless schema_class.name =~ /\AJSI::SchemaClasses::/
        rettypes << schema_class.name
      end
      rettypes << {'object' => '#to_hash', 'array' => '#to_ary'}[type]
    elsif type
      # not really valid, but there's some information in there. whatever it is.
      rettypes << type
    end
    # we'll add Object to all because the accessor methods have no enforcement that their value is
    # of the specified type, and may return anything really. TODO: consider if this is of any value?
    rettypes << 'Object'
    lines << "#   @return [#{rettypes.join(', ')}]"

    description = property_schema &&
      property_schema['description'].respond_to?(:to_str) &&
      property_schema['description'].to_str
    if description
      description.split("\n", -1).each do |descline|
        lines << "#     " + descline
      end
    end
  end
  lines.join("\n")
end

.in_schema_classesObject

is the constant JSI::SchemaClasses::selfself.schema_classes_const_name defined? (if so, we will prefer to use something more human-readable than that ugly mess.)



30
31
32
33
34
# File 'lib/jsi/base.rb', line 30

def in_schema_classes
  # #name sets @in_schema_classes
  name
  @in_schema_classes
end

.inspectString

Returns a string representing the class, with schema_id or schema ptr fragment.

Returns:

  • (String)

    a string representing the class, with schema_id or schema ptr fragment



43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/jsi/base.rb', line 43

def inspect
  if !respond_to?(:schema)
    super
  else
    idfrag = schema_id || schema.node_ptr.fragment
    if name && !in_schema_classes
      "#{name} (#{idfrag})"
    else
      "(JSI Schema Class: #{idfrag})"
    end
  end
end

.nameString

Returns a constant name of this class.

Returns:

  • (String)

    a constant name of this class



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/jsi/base.rb', line 67

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

.new_jsi(instance, *a, &b) ⇒ JSI::Base

JSI::Base.new_jsi behaves the same as .new, and is defined for compatibility so you may call #new_jsi on any of a JSI::Schema, a JSI::SchemaModule, or a JSI schema class.

Returns:

  • (JSI::Base)

    a JSI whose instance is the given instance



24
25
26
# File 'lib/jsi/base.rb', line 24

def new_jsi(instance, *a, &b)
  new(instance, *a, &b)
end

.schema_classes_const_nameString

Returns a name for a constant for this class, generated from the schema_id. only used if the class is not assigned to another constant.

Returns:

  • (String)

    a name for a constant for this class, generated from the schema_id. only used if the class is not assigned to another constant.



60
61
62
63
64
# File 'lib/jsi/base.rb', line 60

def schema_classes_const_name
  if schema_id
    'X' + schema_id.gsub(/[^\w]/, '_')
  end
end

.schema_idString

Returns absolute schema_id of the schema this class represents. see Schema#schema_id.

Returns:

  • (String)

    absolute schema_id of the schema this class represents. see Schema#schema_id.



38
39
40
# File 'lib/jsi/base.rb', line 38

def schema_id
  schema.schema_id
end

.to_rbObject



66
67
68
69
70
71
72
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/jsi/base/to_rb.rb', line 66

def to_rb
  lines = []
  description = schema &&
    schema['description'].respond_to?(:to_str) &&
    schema['description'].to_str
  if description
    description.split("\n", -1).each do |descline|
      lines << "# " + descline
    end
  end
  lines << "class #{name}"
  schema.described_object_property_names.each_with_index do |propname, i|
    lines << "" unless i == 0
    property_schema = schema['properties'].respond_to?(:to_hash) &&
      schema['properties'][propname].respond_to?(:to_hash) &&
      schema['properties'][propname]
    description = property_schema &&
      property_schema['description'].respond_to?(:to_str) &&
      property_schema['description'].to_str
    if description
      description.split("\n", -1).each do |descline|
        lines << "  # " + descline
      end
      lines << "  #" # blank comment line between description and @return
    end

    required = property_schema && property_schema['required']
    required ||= schema['required'].respond_to?(:to_ary) && schema['required'].include?(propname)
    lines << "  # @required" if required

    type = property_schema &&
      property_schema['type'].respond_to?(:to_str) &&
      property_schema['type'].to_str
    simple = {'string' => 'String', 'number' => 'Numeric', 'boolean' => 'Boolean', 'null' => 'nil'}
    rettypes = []
    if simple.key?(type)
      rettypes << simple[type]
    elsif type == 'object' || type == 'array'
      rettypes = []
      schema_class = JSI.class_for_schema(property_schema)
      unless schema_class.name =~ /\AJSI::SchemaClasses::/
        rettypes << schema_class.name
      end
      rettypes << {'object' => '#to_hash', 'array' => '#to_ary'}[type]
    elsif type
      # not really valid, but there's some information in there. whatever it is.
      rettypes << type
    end
    # we'll add Object to all because the accessor methods have no enforcement that their value is
    # of the specified type, and may return anything really. TODO: consider if this is of any value?
    rettypes << 'Object'
    lines << "  # @return [#{rettypes.join(', ')}]"

    lines << "  def #{propname}"
    lines << "    super"
    lines << "  end"
  end
  lines << "end"
  lines.join("\n")
end

.to_sString

Returns a string representing the class, with schema_id or schema ptr fragment.

Returns:

  • (String)

    a string representing the class, with schema_id or schema ptr fragment



56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/jsi/base.rb', line 56

def inspect
  if !respond_to?(:schema)
    super
  else
    idfrag = schema_id || schema.node_ptr.fragment
    if name && !in_schema_classes
      "#{name} (#{idfrag})"
    else
      "(JSI Schema Class: #{idfrag})"
    end
  end
end

Instance Method Details

#[](token) ⇒ JSI::Base, Object

The instance's subscript value at the given token. if there is a subschema defined for that token on this JSI's schema, returns that value as a JSI instantiation of that subschema.

Parameters:

  • token (String, Integer, Object)

    the token to subscript

Returns:

  • (JSI::Base, Object)

    the instance's subscript value at the given token. if there is a subschema defined for that token on this JSI's schema, returns that value as a JSI instantiation of that subschema.



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
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
238
239
240
241
242
243
244
245
246
# File 'lib/jsi/base.rb', line 197

def [](token)
  if respond_to?(:to_hash)
    token_in_range = node_content_hash_pubsend(:key?, token)
    value = node_content_hash_pubsend(:[], token)
  elsif respond_to?(:to_ary)
    token_in_range = node_content_ary_pubsend(:each_index).include?(token)
    value = node_content_ary_pubsend(:[], token)
  else
    raise(NoMethodError, "cannot subcript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
  end

  jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range|
    if respond_to?(:to_ary)
      token_schema = schema.subschema_for_index(token)
    else
      token_schema = schema.subschema_for_property(token)
    end
    token_schema = token_schema && token_schema.match_to_instance(value)

    if token_in_range
      complex_value = token_schema && (value.respond_to?(:to_hash) || value.respond_to?(:to_ary))
      schema_value = token_schema && token_schema.describes_schema?

      if complex_value || schema_value
        class_for_schema(token_schema).new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: @jsi_ptr[token], jsi_root_node: @jsi_root_node)
      else
        value
      end
    else
      defaults = Set.new
      if token_schema
        if token_schema.respond_to?(:to_hash) && token_schema.key?('default')
          defaults << token_schema['default']
        end
      end

      if 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]
      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



253
254
255
256
257
258
259
260
261
262
263
# File 'lib/jsi/base.rb', line 253

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

#as_json(*opt) ⇒ Object

Returns a jsonifiable representation of the instance.

Returns:

  • (Object)

    a jsonifiable representation of the instance



381
382
383
# File 'lib/jsi/base.rb', line 381

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

#deref {|JSI::Base| ... } ⇒ JSI::Base, self

if this JSI is a $ref then the $ref is followed. otherwise this JSI is returned.

Yields:

  • (JSI::Base)

    if a block is given (optional), this will yield a deref'd JSI. if this JSI is not a $ref object, the block is not called. if we are a $ref which cannot be followed (e.g. a $ref to an external document, which is not yet supported), the block is not called.

Returns:



272
273
274
275
276
277
# File 'lib/jsi/base.rb', line 272

def deref(&block)
  node_ptr_deref do |deref_ptr|
    deref_ptr.evaluate(jsi_root_node).tap(&(block || Util::NOOP))
  end
  return self
end

#dupObject



316
317
318
# File 'lib/jsi/base.rb', line 316

def dup
  modified_copy(&:dup)
end

#eachObject

each is overridden by BaseHash or BaseArray when appropriate. the base

each is not actually implemented, along with all the methods of Enumerable.

Raises:

  • (NoMethodError)


162
163
164
# File 'lib/jsi/base.rb', line 162

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

#fully_validate(errors_as_objects: false) ⇒ Array

Returns array of schema validation errors for this instance.

Returns:

  • (Array)

    array of schema validation errors for this instance



299
300
301
# File 'lib/jsi/base.rb', line 299

def fully_validate(errors_as_objects: false)
  schema.fully_validate_instance(jsi_instance, errors_as_objects: errors_as_objects)
end

#inspectString

Returns a string representing this JSI, indicating its class and inspecting its instance.

Returns:

  • (String)

    a string representing this JSI, indicating its class and inspecting its instance



322
323
324
# File 'lib/jsi/base.rb', line 322

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

#jsi_fingerprintObject

Returns an opaque fingerprint of this JSI for FingerprintHash. JSIs are equal if their instances are equal, and if the JSIs are of the same JSI class or subclass.

Returns:

  • (Object)

    an opaque fingerprint of this JSI for FingerprintHash. JSIs are equal if their instances are equal, and if the JSIs are of the same JSI class or subclass.



387
388
389
# File 'lib/jsi/base.rb', line 387

def jsi_fingerprint
  {class: jsi_class, jsi_document: jsi_document, jsi_ptr: jsi_ptr}
end

#modified_copy {|Object| ... } ⇒ JSI::Base subclass the same as self

yields the content of the underlying instance. the block must result in a modified copy of that (not destructively modifying the yielded content) which will be used to instantiate a new instance of this JSI class with the modified content.

Yields:

  • (Object)

    the content of the instance. the block should result in a (nondestructively) modified copy of this.

Returns:

  • (JSI::Base subclass the same as self)

    the modified copy of self



286
287
288
289
290
291
292
293
294
295
296
# File 'lib/jsi/base.rb', line 286

def modified_copy(&block)
  if node_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)
  else
    modified_jsi_root_node = @jsi_root_node.modified_copy do |root|
      @jsi_ptr.modified_document_copy(root, &block)
    end
    self.class.new(Base::NOINSTANCE, jsi_document: modified_jsi_root_node.node_document, jsi_ptr: @jsi_ptr, jsi_root_node: modified_jsi_root_node)
  end
end

#object_group_textArray<String>

Returns:

  • (Array<String>)


342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/jsi/base.rb', line 342

def object_group_text
  class_name = self.class.name unless self.class.in_schema_classes
  class_txt = begin
    if class_name
      # ignore ID
      schema_name = schema.jsi_schema_module.name
      if !schema_name
        class_name
      else
        "#{class_name} (#{schema_name})"
      end
    else
      schema_name = schema.jsi_schema_module.name || schema.schema_id
      if !schema_name
        "JSI"
      else
        "JSI (#{schema_name})"
      end
    end
  end

  if (is_a?(PathedArrayNode) || is_a?(PathedHashNode)) && ![Array, Hash].include?(node_content.class)
    if node_content.respond_to?(:object_group_text)
      node_content_txt = node_content.object_group_text
    else
      node_content_txt = [node_content.class.to_s]
    end
  else
    node_content_txt = []
  end

  [
    class_txt,
    is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
    *node_content_txt,
  ].compact
end

#parent_jsiJSI::Base? Also known as: parent_node, parent

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

Returns:



182
183
184
# File 'lib/jsi/base.rb', line 182

def parent_jsi
  parent_jsis.first
end

#parent_jsisArray<JSI::Base> Also known as: parents

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

Returns:



169
170
171
172
173
174
175
176
177
# File 'lib/jsi/base.rb', line 169

def parent_jsis
  parent = jsi_root_node

  jsi_ptr.reference_tokens.map do |token|
    parent.tap do
      parent = parent[token]
    end
  end.reverse
end

#pretty_print(q) ⇒ void

This method returns an undefined value.

pretty-prints a representation this JSI to the given printer



328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/jsi/base.rb', line 328

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

#validatetrue, false

Returns whether the instance validates against its schema.

Returns:

  • (true, false)

    whether the instance validates against its schema



304
305
306
# File 'lib/jsi/base.rb', line 304

def validate
  schema.validate_instance(jsi_instance)
end

#validate!true

Returns if this method does not raise, it returns true to indicate a valid instance.

Returns:

  • (true)

    if this method does not raise, it returns true to indicate a valid instance.

Raises:

  • (::JSON::Schema::ValidationError)

    raises if the instance has validation errors



312
313
314
# File 'lib/jsi/base.rb', line 312

def validate!
  schema.validate_instance!(jsi_instance)
end