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('::')))) }

Class Attribute Summary collapse

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

#clear_memo, #memoize

Constructor Details

#initialize(instance, jsi_document: nil, jsi_ptr: nil, ancestor_jsi: 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.



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
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/jsi/base.rb', line 92

def initialize(instance, jsi_document: nil, jsi_ptr: nil, ancestor_jsi: 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::Base)
    raise(TypeError, "assigning another JSI::Base instance to #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
  elsif instance.is_a?(JSI::Schema)
    raise(TypeError, "assigning a schema to #{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
  else
    raise(Bug, 'incorrect usage') if jsi_document || jsi_ptr || ancestor_jsi
    if instance.is_a?(PathedNode)
      @jsi_document = instance.document_root_node
      # this can result in the unusual situation where ancestor_jsi is nil, though jsi_ptr is not root.
      # #document_root_node will then return a JSI::JSON::Pointer instead of a root JSI.
      @jsi_ptr = instance.node_ptr
    else
      @jsi_document = instance
      @jsi_ptr = JSI::JSON::Pointer.new([])
    end
  end
  if ancestor_jsi
    if !ancestor_jsi.is_a?(JSI::Base)
      raise(TypeError, "ancestor_jsi must be a JSI::Base; got: #{ancestor_jsi.inspect}")
    end
    if !ancestor_jsi.jsi_ptr.contains?(@jsi_ptr)
      raise(Bug, "ancestor_jsi ptr #{ancestor_jsi.jsi_ptr.inspect} is not ancestor of #{@jsi_ptr.inspect}")
    end
  end
  @ancestor_jsi = ancestor_jsi

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

Class Attribute Details

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



21
22
23
# File 'lib/jsi/base.rb', line 21

def in_schema_classes
  @in_schema_classes
end

Instance Attribute Details

#ancestor_jsiObject (readonly)

a JSI which is an ancestor_jsi of this



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

def ancestor_jsi
  @ancestor_jsi
end

#jsi_documentObject (readonly) Also known as: node_document

document containing the instance of this JSI



139
140
141
# File 'lib/jsi/base.rb', line 139

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



142
143
144
# File 'lib/jsi/base.rb', line 142

def jsi_ptr
  @jsi_ptr
end

Class Method Details

.class_commentObject



4
5
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
# File 'lib/jsi/base/to_rb.rb', line 4

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

.inspectString



30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/jsi/base.rb', line 30

def inspect
  name # see #name for side effects
  if !respond_to?(:schema)
    super
  elsif in_schema_classes
    %Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
  elsif !name
    %Q(#<Class for Schema: #{schema_id}>)
  else
    %Q(#{name} (#{schema_id}))
  end
end

.nameString



65
66
67
68
69
70
71
# File 'lib/jsi/base.rb', line 65

def name
  unless super || SchemaClasses.const_defined?(schema_classes_const_name)
    SchemaClasses.const_set(schema_classes_const_name, self)
    self.in_schema_classes = true
  end
  super
end

.schema_classes_const_nameString



57
58
59
60
61
62
# File 'lib/jsi/base.rb', line 57

def schema_classes_const_name
  name = schema.schema_id.gsub(/[^\w]/, '_')
  name = 'X' + name unless name[/\A[a-zA-Z]/]
  name = name[0].upcase + name[1..-1]
  name
end

.schema_idString



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

def schema_id
  schema.schema_id
end

.to_rbObject



64
65
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
# File 'lib/jsi/base/to_rb.rb', line 64

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



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

def to_s
  if !respond_to?(:schema)
    super
  elsif !name || name =~ /\AJSI::SchemaClasses::/
    %Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
  else
    name
  end
end

Instance Method Details

#as_json(*opt) ⇒ Object



327
328
329
# File 'lib/jsi/base.rb', line 327

def as_json(*opt)
  Typelike.as_json(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.



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/jsi/base.rb', line 231

def deref(&block)
  node_ptr_deref do |deref_ptr|
    jsi_from_root = deref_ptr.evaluate(document_root_node)
    if jsi_from_root.is_a?(JSI::Base)
      return jsi_from_root.tap(&(block || Util::NOOP))
    else
      # TODO I want to get rid of this ... just return jsi_from_root whatever it is
      # NOTE when I get rid of this, simplify #parent_jsis too
      if @ancestor_jsi && @ancestor_jsi.jsi_ptr.contains?(deref_ptr)
        derefed = self.class.new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: deref_ptr, ancestor_jsi: @ancestor_jsi)
      else
        derefed = self.class.new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: deref_ptr)
      end
      return derefed.tap(&(block || Util::NOOP))
    end
  end
  return self
end

#document_root_nodeJSI::PathedNode



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

def document_root_node
  if @jsi_ptr.root?
    self
  elsif @ancestor_jsi
    @ancestor_jsi.document_root_node
  elsif instance.is_a?(PathedNode)
    instance.document_root_node
  else
    JSI::JSON::Node.new_doc(@jsi_document)
  end
end

#dupObject



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

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)


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

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

#fingerprintObject



333
334
335
# File 'lib/jsi/base.rb', line 333

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

#fragmentString



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

def fragment
  @jsi_ptr.fragment
end

#fully_validateArray<String>



278
279
280
# File 'lib/jsi/base.rb', line 278

def fully_validate
  schema.fully_validate_instance(instance)
end

#inspectString



301
302
303
# File 'lib/jsi/base.rb', line 301

def inspect
  "\#<#{self.class.to_s} #{instance.inspect}>"
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.



257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/jsi/base.rb', line 257

def modified_copy(&block)
  if @ancestor_jsi
    raise(Bug, 'bad @ancestor_jsi') if @ancestor_jsi.object_id == self.object_id

    modified_ancestor = @ancestor_jsi.modified_copy do |anc|
      mod_anc = @jsi_ptr.ptr_relative_to(@ancestor_jsi.jsi_ptr).modified_document_copy(anc, &block)
      mod_anc
    end
    self.class.new(Base::NOINSTANCE, jsi_document: modified_ancestor.jsi_document, jsi_ptr: @jsi_ptr, ancestor_jsi: modified_ancestor)
  else
    modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
    self.class.new(Base::NOINSTANCE, jsi_document: modified_document, jsi_ptr: @jsi_ptr)
  end
end

#object_group_textArray<String>



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

def object_group_text
  instance.respond_to?(:object_group_text) ? instance.object_group_text : [instance.class.inspect]
end

#parent_jsiJSI::Base? Also known as: parent

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



185
186
187
# File 'lib/jsi/base.rb', line 185

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. empty if this JSI does not have a known ancestor.



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/jsi/base.rb', line 164

def parent_jsis
  ancestor_jsi = @ancestor_jsi || self
  parent = ancestor_jsi

  (ancestor_jsi.jsi_ptr.reference_tokens.size...self.jsi_ptr.reference_tokens.size).map do |i|
    current = parent
    parent = parent[self.jsi_ptr.reference_tokens[i]]
    if current.is_a?(JSI::Base)
      current
    else
      # sometimes after a deref, we may end up with parents whose schema we do not know.
      # TODO this is kinda crap; hopefully we can remove it along with deref instantiating
      # a deref ptr as the same JSI class it is
      SimpleWrap.new(NOINSTANCE, jsi_document: jsi_document, jsi_ptr: jsi_ptr.take(i), ancestor_jsi: @ancestor_jsi)
    end
  end.reverse
end

#parent_nodeJSI::PathedNode



204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/jsi/base.rb', line 204

def parent_node
  if @jsi_ptr.root?
    nil
  elsif @ancestor_jsi
    parent_jsis.first.tap do |parent_node|
      raise(Bug, 'is @ancestor_jsi == self? it should not be') if parent_node.nil?
      raise(Bug, "parent_node not PathedNode: #{parent_node.pretty_inspect.chomp}") unless parent_node.is_a?(JSI::PathedNode)
    end
  elsif instance.is_a?(PathedNode)
    instance.parent_node
  else
    JSI::JSON::Node.new_by_type(@jsi_document, @jsi_ptr.parent)
  end
end

#pretty_print(q) ⇒ void

This method returns an undefined value.

pretty-prints a representation this JSI to the given printer



307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/jsi/base.rb', line 307

def pretty_print(q)
  q.instance_exec(self) do |obj|
    text "\#<#{obj.class.to_s}"
    group_sub {
      nest(2) {
        breakable ' '
        pp obj.instance
      }
    }
    breakable ''
    text '>'
  end
end

#validatetrue, false



283
284
285
# File 'lib/jsi/base.rb', line 283

def validate
  schema.validate_instance(instance)
end

#validate!true

Returns 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



291
292
293
# File 'lib/jsi/base.rb', line 291

def validate!
  schema.validate_instance!(instance)
end