Class: JSI::Base
- Inherits:
-
Object
- Object
- JSI::Base
- Includes:
- Enumerable, PathedNode, Util::FingerprintHash, Util::Memoize
- 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.
Defined Under Namespace
Classes: CannotSubscriptError
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, indicating the schemas represented by their module name or a URI.
-
.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 constant name or schema id of each schema this class represents.
- .to_rb ⇒ Object
-
.to_s ⇒ String
A string representing the class, indicating the schemas represented by their module name or a URI.
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 PathedHashNode or PathedArrayNode 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 Util::FingerprintHash
Methods included from PathedNode
#node_content, #node_ptr_deref
Methods included from Util::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.
124 125 126 127 128 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 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/jsi/base.rb', line 124 def initialize(instance, jsi_document: nil, jsi_ptr: nil, jsi_root_node: nil) 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 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[] @jsi_root_node = self end if self.jsi_instance.respond_to?(:to_hash) extend PathedHashNode elsif self.jsi_instance.respond_to?(:to_ary) extend PathedArrayNode end jsi_schemas.each do |schema| if schema.describes_schema? extend JSI::Schema end end end |
Instance Attribute Details
#jsi_document ⇒ Object (readonly) Also known as: node_document
document containing the instance of this JSI
174 175 176 |
# File 'lib/jsi/base.rb', line 174 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
177 178 179 |
# File 'lib/jsi/base.rb', line 177 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
180 181 182 |
# File 'lib/jsi/base.rb', line 180 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.)
29 30 31 32 33 |
# File 'lib/jsi/base.rb', line 29 def in_schema_classes # #name sets @in_schema_classes name @in_schema_classes end |
.inspect ⇒ String
Returns a string representing the class, indicating the schemas represented by their module name or a URI.
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 65 66 67 68 |
# File 'lib/jsi/base.rb', line 37 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_id "#{mod.name} (#{schema.schema_id})" elsif mod.name mod.name elsif schema.schema_id schema.schema_id else schema.node_ptr.uri 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 |
.name ⇒ String
Returns a constant name of this class.
93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/jsi/base.rb', line 93 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 |
.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.
23 24 25 |
# File 'lib/jsi/base.rb', line 23 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 constant name or schema id of each schema this class represents. nil if any represented schema has no constant name or schema id.
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/jsi/base.rb', line 75 def schema_classes_const_name if respond_to?(:jsi_class_schemas) schema_names = jsi_class_schemas.map do |schema| if schema.jsi_schema_module.name schema.jsi_schema_module.name elsif schema.schema_id schema.schema_id else nil end end if !schema_names.any?(&:nil?) && !schema_names.empty? schema_names.sort.map { |n| 'X' + n.gsub(/[^\w]/, '_') }.join('') end end 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, indicating the schemas represented by their module name or a URI.
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 |
# File 'lib/jsi/base.rb', line 70 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_id "#{mod.name} (#{schema.schema_id})" elsif mod.name mod.name elsif schema.schema_id schema.schema_id else schema.node_ptr.uri 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) ⇒ JSI::Base, Object
The instance's subscript value at the given token. if this JSI's schemas define subschemas which apply for the given token, and the value is complex, returns the subscript value as a JSI instantiation of those subschemas. otherwise, the plain instance value is returned.
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 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 275 276 277 278 |
# File 'lib/jsi/base.rb', line 228 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(CannotSubscriptError, "cannot subcript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}") end result = jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range| if respond_to?(:to_ary) token_schemas = jsi_schemas.map { |schema| schema.subschemas_for_index(token) }.inject(Set.new, &:|) else token_schemas = jsi_schemas.map { |schema| schema.subschemas_for_property_name(token) }.inject(Set.new, &:|) end token_schemas = token_schemas.map { |schema| schema.match_to_instance(value) }.inject(Set.new, &:|) if token_in_range complex_value = token_schemas.any? && (value.respond_to?(:to_hash) || value.respond_to?(:to_ary)) schema_value = token_schemas.any? { |token_schema| token_schema.describes_schema? } if complex_value || schema_value JSI::SchemaClasses.class_for_schemas(token_schemas).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 token_schemas.each do |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 result 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.
285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/jsi/base.rb', line 285 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.
414 415 416 |
# File 'lib/jsi/base.rb', line 414 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.
304 305 306 307 308 309 |
# File 'lib/jsi/base.rb', line 304 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
349 350 351 |
# File 'lib/jsi/base.rb', line 349 def dup 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.
192 193 194 |
# File 'lib/jsi/base.rb', line 192 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.
331 332 333 |
# File 'lib/jsi/base.rb', line 331 def fully_validate(errors_as_objects: false) jsi_schemas.map { |schema| schema.fully_validate_instance(jsi_instance, errors_as_objects: errors_as_objects) }.inject([], &:+) end |
#inspect ⇒ String
Returns a string representing this JSI, indicating its class and inspecting its instance.
355 356 357 |
# File 'lib/jsi/base.rb', line 355 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.
420 421 422 |
# File 'lib/jsi/base.rb', line 420 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.
318 319 320 321 322 323 324 325 326 327 328 |
# File 'lib/jsi/base.rb', line 318 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>
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 410 411 |
# File 'lib/jsi/base.rb', line 375 def object_group_text class_name = self.class.name unless self.class.in_schema_classes class_txt = begin if class_name # ignore ID schema_module_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name }.compact if schema_module_names.empty? class_name else "#{class_name} (#{schema_module_names.join(', ')})" end else schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name || schema.schema_id }.compact if schema_names.empty? "JSI" else "JSI (#{schema_names.join(', ')})" 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.
212 213 214 |
# File 'lib/jsi/base.rb', line 212 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.
199 200 201 202 203 204 205 206 207 |
# File 'lib/jsi/base.rb', line 199 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
361 362 363 364 365 366 367 368 369 370 371 372 |
# File 'lib/jsi/base.rb', line 361 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.
336 337 338 |
# File 'lib/jsi/base.rb', line 336 def validate jsi_schemas.all? { |schema| schema.validate_instance(jsi_instance) } end |
#validate! ⇒ true
Returns if this method does not raise, it returns true to indicate a valid instance.
344 345 346 347 |
# File 'lib/jsi/base.rb', line 344 def validate! jsi_schemas.each { |schema| schema.validate_instance!(jsi_instance) } true end |