Class: JSI::Base
- Inherits:
-
Object
- Object
- JSI::Base
- 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
-
#jsi_document ⇒ Object
(also: #node_document)
readonly
document containing the instance of this JSI.
-
#jsi_ptr ⇒ Object
(also: #node_ptr)
readonly
JSI::JSON::Pointer pointing to this JSI's instance within the jsi_document.
-
#jsi_root_node ⇒ Object
(also: #document_root_node)
readonly
the JSI at the root of this JSI's document.
Class Method Summary collapse
- .class_comment ⇒ Object
-
.in_schema_classes ⇒ Object
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.).
-
.inspect ⇒ String
A string representing the class, with schema_id or schema ptr fragment.
-
.name ⇒ String
A constant name of this class.
-
.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.
-
.schema_classes_const_name ⇒ String
A name for a constant for this class, generated from the schema_id.
-
.schema_id ⇒ String
Absolute schema_id of the schema this class represents.
- .to_rb ⇒ Object
-
.to_s ⇒ String
A string representing the class, with schema_id or schema ptr fragment.
Instance Method Summary collapse
-
#[](token) ⇒ JSI::Base, Object
The instance's subscript value at the given token.
-
#[]=(token, value) ⇒ Object
assigns the subscript of the instance identified by the given token to the given value.
-
#as_json(*opt) ⇒ Object
A jsonifiable representation of the instance.
-
#deref {|JSI::Base| ... } ⇒ JSI::Base, self
if this JSI is a $ref then the $ref is followed.
- #dup ⇒ Object
-
#each ⇒ Object
each is overridden by BaseHash or BaseArray when appropriate.
-
#fully_validate(errors_as_objects: false) ⇒ Array
Array of schema validation errors for this instance.
-
#initialize(instance, jsi_document: nil, jsi_ptr: nil, jsi_root_node: nil) ⇒ Base
constructor
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.
-
#inspect ⇒ String
A string representing this JSI, indicating its class and inspecting its instance.
-
#jsi_fingerprint ⇒ Object
An opaque fingerprint of this JSI for FingerprintHash.
-
#modified_copy {|Object| ... } ⇒ JSI::Base subclass the same as self
yields the content of the underlying instance.
- #object_group_text ⇒ Array<String>
-
#parent_jsi ⇒ JSI::Base?
(also: #parent_node, #parent)
the immediate parent of this JSI.
-
#parent_jsis ⇒ Array<JSI::Base>
(also: #parents)
an array of JSI instances above this one in the document.
-
#pretty_print(q) ⇒ void
pretty-prints a representation this JSI to the given printer.
-
#validate ⇒ true, false
Whether the instance validates against its schema.
-
#validate! ⇒ true
If this method does not raise, it returns true to indicate a valid instance.
Methods included from FingerprintHash
Methods included from PathedNode
#node_content, #node_ptr_deref
Methods included from 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.
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_document ⇒ Object (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_ptr ⇒ Object (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_node ⇒ Object (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_comment ⇒ Object
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_classes ⇒ Object
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 |
.inspect ⇒ String
Returns 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 |
.name ⇒ String
Returns 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.
24 25 26 |
# File 'lib/jsi/base.rb', line 24 def new_jsi(instance, *a, &b) new(instance, *a, &b) end |
.schema_classes_const_name ⇒ String
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.
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_id ⇒ String
Returns 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_rb ⇒ Object
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_s ⇒ String
Returns 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.
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.
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.
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.
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 |
#dup ⇒ Object
316 317 318 |
# File 'lib/jsi/base.rb', line 316 def dup modified_copy(&:dup) end |
#each ⇒ Object
each is overridden by BaseHash or BaseArray when appropriate. the base
each is not actually implemented, along with all the methods of Enumerable.
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.
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 |
#inspect ⇒ String
Returns 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_fingerprint ⇒ Object
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.
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.
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_text ⇒ 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_jsi ⇒ JSI::Base? Also known as: parent_node, parent
the immediate parent of this JSI. nil if there is no parent.
182 183 184 |
# File 'lib/jsi/base.rb', line 182 def parent_jsi parent_jsis.first end |
#parent_jsis ⇒ Array<JSI::Base> Also known as: parents
an array of JSI instances above this one in the document.
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 |
#validate ⇒ true, false
Returns 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.
312 313 314 |
# File 'lib/jsi/base.rb', line 312 def validate! schema.validate_instance!(jsi_instance) end |