Module: JSI::Schema

Included in:
MetaSchemaNode::BootstrapSchema
Defined in:
lib/jsi/schema.rb,
lib/jsi/schema.rb,
lib/jsi/schema/cxt.rb,
lib/jsi/schema/issue.rb,
lib/jsi/schema/dialect.rb,
lib/jsi/schema/draft04.rb,
lib/jsi/schema/draft06.rb,
lib/jsi/schema/draft07.rb,
lib/jsi/schema/element.rb,
lib/jsi/schema/vocabulary.rb

Overview

JSI::Schema is a module which extends Base instances which represent JSON schemas.

This module is included on the JSI Schema module of any schema that describes other schemas, i.e. is a meta-schema (a MetaSchema). Therefore, any JSI instance described by a schema which is a MetaSchema is a schema and is extended by this module.

The content of an instance which is a JSI::Schema (referred to in this context as schema_content) is typically a Hash (JSON object) or a boolean.

Defined Under Namespace

Modules: Draft04, Draft06, Draft07, Draft202012, Elements, MetaSchema, SchemaAncestorNode Classes: Cxt, Dialect, DynamicAnchorMap, Element, Error, Issue, NotAMetaSchemaError, NotASchemaError, Ref, Vocabulary

Constant Summary collapse

ReferenceError =
Deprecated.

alias after v0.8

an exception raised when we are unable to resolve a schema reference

ResolutionError

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.ensure_metaschema(metaschema, name: nil, registry: JSI.registry) ⇒ Base + Schema + Schema::MetaSchema

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Ensures the given param identifies a meta-schema and returns that meta-schema.

Parameters:

Returns:

Raises:

  • (TypeError)

    if the param does not indicate a meta-schema



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/jsi/schema.rb', line 345

def ensure_metaschema(metaschema, name: nil, registry: JSI.registry)
  if metaschema.respond_to?(:to_str)
    schema = Schema::Ref.new(metaschema, registry: registry).resolve
    if !schema.describes_schema?
      raise(NotAMetaSchemaError, [name, "URI indicates a schema that is not a meta-schema: #{metaschema.pretty_inspect.chomp}"].compact.join(" "))
    end
    schema
  elsif metaschema.is_a?(SchemaModule::MetaSchemaModule)
    metaschema.schema
  elsif metaschema.is_a?(Schema::MetaSchema)
    metaschema
  else
    raise(NotAMetaSchemaError, "#{name || "param"} does not indicate a meta-schema: #{metaschema.pretty_inspect.chomp}")
  end
end

.ensure_schema(schema, reinstantiate_as: nil) ⇒ Schema

ensure the given object is a JSI Schema

Parameters:

  • schema (Object)

    the thing the caller wishes to ensure is a Schema

Yield Returns:

  • (#to_s, #to_ary)

    first line(s) of the error message, overriding the default

Returns:

  • (Schema)

    the given schema

Raises:



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/jsi/schema.rb', line 300

def ensure_schema(schema, reinstantiate_as: nil)
  if schema.is_a?(Schema)
    schema
  else
    if reinstantiate_as && schema.is_a?(JSI::Base)
      # TODO warn; behavior is undefined and I hate this implementation

      result_schema_indicated_schemas = SchemaSet.new(schema.jsi_indicated_schemas + reinstantiate_as)
      result_schema_applied_schemas = result_schema_indicated_schemas.each_yield_set do |is, y|
        is.each_inplace_applicator_schema(schema.jsi_node_content, &y)
      end

      result_schema_class = JSI::SchemaClasses.class_for_schemas(result_schema_applied_schemas,
        includes: SchemaClasses.includes_for(schema.jsi_node_content),
        mutable: schema.jsi_mutable?,
      )

      result_schema_class.new(
        jsi_document: schema.jsi_document,
        jsi_ptr: schema.jsi_ptr,
        jsi_indicated_schemas: result_schema_indicated_schemas,
        jsi_base_uri: schema.jsi_base_uri,
        jsi_schema_resource_ancestors: schema.jsi_schema_resource_ancestors,
        jsi_schema_dynamic_anchor_map: schema.jsi_schema_dynamic_anchor_map,
        jsi_conf: schema.equal?(schema.jsi_root_node) ? schema.jsi_conf : nil,
        jsi_root_node: schema.equal?(schema.jsi_root_node) ? nil : schema.jsi_root_node, # bad
      ).send(:jsi_initialized)
    else
      msg = []
      msg.concat([*(block_given? ? yield : "indicated object is not a schema:")])
      msg << schema.pretty_inspect.chomp
      if schema.is_a?(Base)
        msg << "its schemas (which should include a Meta-Schema): #{schema.jsi_schemas.pretty_inspect.chomp}"
      end
      raise(NotASchemaError, msg.compact.join("\n"))
    end
  end
end

Instance Method Details

#anchorsEnumerable<String>

Returns:

  • (Enumerable<String>)


407
408
409
410
411
412
# File 'lib/jsi/schema.rb', line 407

def anchors
  anchors = Set[]
  anchors.merge(dialect_invoke_each(:anchor))
  anchors.merge(dialect_invoke_each(:dynamicAnchor))
  anchors.freeze
end

#described_object_property_namesSet

any object property names this schema indicates may be present on its instances. this includes any keys of this schema's "properties" object and any entries of this schema's array of "required" property keys.

Returns:

  • (Set)


805
806
807
# File 'lib/jsi/schema.rb', line 805

def described_object_property_names
  @described_object_property_names_map[schema_content: schema_content]
end

#describes_schema!(dialect = nil)

This method returns an undefined value.

Indicates that this schema describes schemas, i.e. it is a meta-schema. this schema is extended with MetaSchema and its #jsi_schema_module is extended with JSI::SchemaModule::MetaSchemaModule, and the JSI Schema Module will include JSI::Schema.

Parameters:

  • dialect (Schema::Dialect, nil) (defaults to: nil)

    dialect may be passed, or inferred from $vocabulary

Raises:

  • (TypeError)


542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
# File 'lib/jsi/schema.rb', line 542

def describes_schema!(dialect = nil)
  # TODO rm bridge code hax
  dialect = dialect.first::DIALECT if dialect.is_a?(Array) && dialect.size == 1

  if !dialect
    raise(ArgumentError, "no dialect given and no $vocabulary hash/object") if !schema_content['$vocabulary'].respond_to?(:to_hash)
    dialect = Schema::Dialect.from_xvocabulary(schema_content['$vocabulary'], registry: jsi_registry)
  end

  raise(TypeError) if !dialect.is_a?(Schema::Dialect)

  if jsi_schema_module <= Schema
    # this schema has already had describes_schema! called on it.
    # this is to be avoided, but is not particularly a problem.
    # it is a bug if it was called different times with different dialect, though.
    if @described_dialect != dialect
      raise(ArgumentError, "this schema already describes a schema with different dialect")
    end
  else
    jsi_schema_module.include(Schema)
    jsi_schema_module.send(:define_method, :dialect) { dialect }
    proc { |metaschema| jsi_schema_module.send(:define_method, :metaschema) { metaschema } }[self]
    jsi_schema_module.extend(SchemaModule::MetaSchemaModule)
  end

  @described_dialect = dialect
  extend(Schema::MetaSchema)

  nil
end

#describes_schema?Boolean

Does this schema itself describe a schema? I.e. is this schema a meta-schema?

Returns:

  • (Boolean)


525
526
527
# File 'lib/jsi/schema.rb', line 525

def describes_schema?
  is_a?(Schema::MetaSchema)
end

#dialectSchema::Dialect

The dialect of this schema

Returns:



# File 'lib/jsi/schema.rb', line 374

#dialect_invoke_each(action_name, cxt_class = Cxt::Block, **cxt_param) { ... } ⇒ Object

Parameters:

  • action_name (Symbol)
  • cxt_class (Class) (defaults to: Cxt::Block)

Yields:



887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
# File 'lib/jsi/schema.rb', line 887

def dialect_invoke_each(
    action_name,
    cxt_class = Cxt::Block,
    **cxt_param,
    &block
)
  return(to_enum(__method__, action_name, cxt_class, **cxt_param)) unless block_given?

  cxt = cxt_class.new(
    schema: self,
    abort: false,
    block: block,
    **cxt_param,
  )
  dialect.invoke(action_name, cxt)

  nil
end

#each_child_applicator_schema(token, instance) {|JSI::Schema| ... } ⇒ nil, Enumerator

yields each child applicator subschema (from properties, items, etc.) which applies to the child of the given instance on the given token.

Parameters:

  • token (Object)

    the array index or object property name for the child instance

  • instance (Object)

    the instance to check any child applicators against

Yields:

Returns:

  • (nil, Enumerator)

    an Enumerator if invoked without a block; otherwise nil



721
722
723
724
725
726
727
728
729
730
731
# File 'lib/jsi/schema.rb', line 721

def each_child_applicator_schema(token, instance, &block)
  dialect_invoke_each(:child_applicate,
    Cxt::ChildApplication,
    instance: instance,
    token: token,
    collect_evaluated: false,
    collect_evaluated_validate: false,
    evaluated: false,
    &block
  )
end

#each_immediate_subschema_ptr {|Ptr| ... } ⇒ Object

Yields:



640
641
642
643
644
# File 'lib/jsi/schema.rb', line 640

def each_immediate_subschema_ptr
  return(to_enum(__method__)) unless block_given?

  dialect_invoke_each(:subschema) { |ptr| yield(Ptr.ary_ptr(ptr)) }
end

#each_inplace_applicator_schema(instance, visited_refs: Util::EMPTY_ARY) {|JSI::Schema| ... } ⇒ nil

Yields each in-place applicator schema which applies to the given instance.

Parameters:

  • instance (Object)

    the instance to check any applicators against

  • visited_refs (Enumerable<JSI::Schema::Ref>) (defaults to: Util::EMPTY_ARY)

Yields:

Returns:

  • (nil)


692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
# File 'lib/jsi/schema.rb', line 692

def each_inplace_applicator_schema(
    instance,
    visited_refs: Util::EMPTY_ARY,
    &block
)
  each_immediate_inplace_applicator_schema(
    instance: instance,
    visited_refs: visited_refs,
    collect_evaluated: false, # child application is not invoked so no evaluated children to collect
  ) do |schema, ref: nil, applicate: true|
    if schema.equal?(self) && !ref
      yield(self)
    elsif applicate
      schema.each_inplace_applicator_schema(
        instance,
        visited_refs: Util.add_visited_ref(visited_refs, ref),
        &block
      )
    end
  end
end

#each_inplace_child_applicator_schema(token, instance, visited_refs: Util::EMPTY_ARY, collect_evaluated: false, collect_evaluated_validate: false) {|Schema| ... } ⇒ Boolean

For each in-place applicator schema that applies to the given instance, yields each child applicator of that schema that applies to the child of the instance on the given token.

This method handles collection of whether the child was evaluated by any applicator when that evaluation is needed by either this schema or the caller (per param collect_evaluated). This is relevant to schemas containing unevaluatedProperties or unevaluatedItems.

Parameters:

  • token (Object)

    array index or hash/object property name

  • instance (Object)
  • collect_evaluated (Boolean) (defaults to: false)

    Does the caller need this method to collect successful child evaluation? Note: this method will still collect child evaluation if this schema needs it; this only needs to be passed true when called by an in-place applicator schema that needs it (i.e. contains unevaluated*).

  • collect_evaluated_validate (Boolean) (defaults to: false)

Yields:

Returns:

  • (Boolean)

    if collect_evaluated is true, whether the child was successfully evaluated by a child applicator schema. if collect_evaluated is false, undefined/void.



749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
# File 'lib/jsi/schema.rb', line 749

def each_inplace_child_applicator_schema(
    token,
    instance,
    visited_refs: Util::EMPTY_ARY,
    collect_evaluated: false,
    collect_evaluated_validate: false,
    &block
)
  collect_evaluated ||= application_requires_evaluated
  inplace_child_evaluated = false
  applicate_self = false

  each_immediate_inplace_applicator_schema(
    instance: instance,
    visited_refs: visited_refs,
    collect_evaluated: collect_evaluated,
  ) do |schema, ref: nil, applicate: true|
    if schema.equal?(self) && !ref
      applicate_self = true
    elsif applicate || (collect_evaluated && !inplace_child_evaluated)
      schema_evaluated = schema.each_inplace_child_applicator_schema(
        token,
        instance,
        visited_refs: Util.add_visited_ref(visited_refs, ref),
        collect_evaluated: collect_evaluated && !inplace_child_evaluated,
        collect_evaluated_validate: collect_evaluated_validate,
        # the `if` keyword needs to yield to here because it does affect `evaluated`,
        # but it does not applicate itself/its applicators, so does not yield to the given block.
        &(applicate ? block : proc { })
      )
      inplace_child_evaluated ||= collect_evaluated && schema_evaluated && (!collect_evaluated_validate || schema.instance_valid?(instance))
    end
  end

  if applicate_self
    child_application = dialect.invoke(:child_applicate, Cxt::ChildApplication.new(
      schema: self,
      abort: false,
      token: token,
      instance: instance,
      collect_evaluated: collect_evaluated,
      collect_evaluated_validate: collect_evaluated_validate,
      evaluated: inplace_child_evaluated,
      block: block,
    ))

    child_application.evaluated
  else
    inplace_child_evaluated
  end
end

#id#to_str?

the string contents of an $id/id keyword, or nil

Returns:

  • (#to_str, nil)


402
403
404
# File 'lib/jsi/schema.rb', line 402

def id
  dialect_invoke_each(:id).first
end

#initializeObject



363
364
365
366
# File 'lib/jsi/schema.rb', line 363

def initialize(*)
  super
  jsi_schema_initialize
end

#instance_valid!(instance) ⇒ nil

Asserts that the given instance is valid against this schema. Error::Invalid is raised if it is not.

Returns:

  • (nil)

Raises:



839
840
841
# File 'lib/jsi/schema.rb', line 839

def instance_valid!(instance)
  instance_validate(instance).valid!
end

#instance_valid?(instance) ⇒ Boolean

whether the given instance is valid against this schema

Parameters:

  • instance (Object)

    the instance to validate against this schema

Returns:

  • (Boolean)


827
828
829
830
831
832
# File 'lib/jsi/schema.rb', line 827

def instance_valid?(instance)
  if instance.is_a?(SchemaAncestorNode)
    instance = instance.jsi_node_content
  end
  internal_validate_instance(Ptr[], instance, validate_only: true).valid?
end

#instance_validate(instance) ⇒ JSI::Validation::Result::Full

Validates the given instance against this schema, returning a result with each validation error.

Parameters:

  • instance (Object)

    the instance to validate against this schema

Returns:



813
814
815
816
817
818
819
820
821
822
# File 'lib/jsi/schema.rb', line 813

def instance_validate(instance)
  if instance.is_a?(SchemaAncestorNode)
    instance_ptr = instance.jsi_ptr
    instance_document = instance.jsi_document
  else
    instance_ptr = Ptr[]
    instance_document = instance
  end
  internal_validate_instance(instance_ptr, instance_document)
end

#jsi_as_child_default_as_jsiObject

See Base#jsi_as_child_default_as_jsi. true for Schema, including boolean schemas.



880
881
882
# File 'lib/jsi/schema.rb', line 880

def jsi_as_child_default_as_jsi
  true
end

#jsi_each_descendent_schema {|Schema| ... } ⇒ Object

Yields:



618
619
620
621
622
623
# File 'lib/jsi/schema.rb', line 618

def jsi_each_descendent_schema(&block)
  return(to_enum(__method__)) unless block_given?

  yield(self)
  dialect_invoke_each(:subschema) { |ptr| subschema(ptr).jsi_each_descendent_schema(&block) }
end

#jsi_each_descendent_schema_same_resource {|Schema| ... } ⇒ Object

yields each descendent of this node (including itself) within the same resource that is a Schema

Yields:



627
628
629
630
631
632
633
634
635
636
637
# File 'lib/jsi/schema.rb', line 627

def jsi_each_descendent_schema_same_resource(&block)
  return(to_enum(__method__)) unless block_given?

  yield(self)
  dialect_invoke_each(:subschema) do |ptr|
    desc = subschema(ptr)
    if !desc.jsi_is_resource_root?
      desc.jsi_each_descendent_schema_same_resource(&block)
    end
  end
end

#jsi_is_resource_root?Boolean

is this schema the root of a schema resource?

Returns:

  • (Boolean)


589
590
591
# File 'lib/jsi/schema.rb', line 589

def jsi_is_resource_root?
  super || jsi_resource_uris.any?
end

#jsi_is_schema?Boolean

Is this a JSI Schema?

Returns:

  • (Boolean)


531
532
533
# File 'lib/jsi/schema.rb', line 531

def jsi_is_schema?
  true
end

#jsi_schema_moduleSchemaModule

The JSI Schema Module for this schema. JSI instances described by this schema are instances of this module.

Returns:



479
480
481
# File 'lib/jsi/schema.rb', line 479

def jsi_schema_module
  jsi_schema_module_connection
end

#jsi_schema_module_exec(*a, **kw, &block) ⇒ Object

Evaluates the given block in the context of this schema's JSI schema module. Any arguments passed to this method will be passed to the block. shortcut to invoke Module#module_exec on our #jsi_schema_module.

Returns:

  • the result of evaluating the block



489
490
491
# File 'lib/jsi/schema.rb', line 489

def jsi_schema_module_exec(*a, **kw, &block)
  jsi_schema_module.module_exec(*a, **kw, &block)
end

#jsi_schema_module_nameString?

Returns:

  • (String, nil)


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

def jsi_schema_module_name
  # don't hit #jsi_schema_module - avoid creating module, avoid erroring for MSN::BootstrapSchema
  @memos[:schema_module_connection] && @memos[:schema_module_connection].name
end

#jsi_schema_module_name_from_ancestorString?

Returns:

  • (String, nil)


500
501
502
# File 'lib/jsi/schema.rb', line 500

def jsi_schema_module_name_from_ancestor
  is_a?(Base) ? jsi_schema_module.name_from_ancestor : nil
end

#jsi_subschema_resource_ancestorsArray<JSI::Schema>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

schema resources which are ancestors of any subschemas below this schema. this may include this schema if this is a schema resource root.

Returns:



910
911
912
913
914
915
916
# File 'lib/jsi/schema.rb', line 910

def jsi_subschema_resource_ancestors
  if jsi_is_resource_root?
    jsi_schema_resource_ancestors.dup.push(self).freeze
  else
    jsi_schema_resource_ancestors
  end
end

#keyword?(keyword) ⇒ Boolean

does this schema contain the given keyword?

Returns:

  • (Boolean)


389
390
391
392
# File 'lib/jsi/schema.rb', line 389

def keyword?(keyword)
  schema_content = jsi_node_content
  schema_content.respond_to?(:to_hash) && schema_content.key?(keyword)
end

#keyword_value?(keyword, value) ⇒ Boolean

Does this schema contain the given keyword with the given value?

Returns:

  • (Boolean)


396
397
398
# File 'lib/jsi/schema.rb', line 396

def keyword_value?(keyword, value)
  keyword?(keyword) && schema_content[keyword] == value
end

#new_jsi(instance, **kw) ⇒ Base

Instantiates a new JSI whose content comes from the given instance param. This schema indicates the schemas of the JSI - its schemas are in-place applicators of this schema which apply to the given instance.

All parameters are passed to JSI::SchemaSet#new_jsi.

Returns:

  • (Base)

    a JSI whose content comes from the given instance and whose schemas are in-place applicators of this schema.

Raises:



512
513
514
515
# File 'lib/jsi/schema.rb', line 512

def new_jsi(instance, **kw)
  raise(BlockGivenError) if block_given?
  SchemaSet[self].new_jsi(instance, **kw)
end

#resource_root_subschema(ptr) ⇒ JSI::Schema

A schema in the same schema resource as this one (see JSI::Schema::SchemaAncestorNode#jsi_resource_root) at the given pointer relative to the root of the schema resource.

Parameters:

  • ptr (JSI::Ptr, #to_ary)

    a pointer to a schema from our schema resource root

Returns:



611
612
613
614
615
# File 'lib/jsi/schema.rb', line 611

def resource_root_subschema(ptr)
      Schema.ensure_schema(jsi_resource_root.jsi_descendent_node(ptr),
        reinstantiate_as: jsi_conf.reinstantiate_nonschemas && jsi_schemas.select(&:describes_schema?),
      )
end

#schema_absolute_uriURI?

Deprecated.

after v0.8 - use #jsi_resource_uri

the URI of this schema, from an $id keyword, resolved against our #jsi_base_uri

Returns:

  • (URI, nil)


417
418
419
# File 'lib/jsi/schema.rb', line 417

def schema_absolute_uri
  jsi_resource_uri
end

#schema_absolute_urisEnumerable<URI>

Deprecated.

after v0.8 - use #jsi_resource_uris

Returns:

  • (Enumerable<URI>)


423
424
425
# File 'lib/jsi/schema.rb', line 423

def schema_absolute_uris
  jsi_resource_uris
end

#schema_contentObject

the underlying JSON data used to instantiate this JSI::Schema. this is an alias for Base#jsi_node_content, named for clarity in the context of working with a schema.



383
384
385
# File 'lib/jsi/schema.rb', line 383

def schema_content
  jsi_node_content
end

#schema_ref(ref = ) ⇒ Schema::Ref

Parameters:

  • ref (#to_str) (defaults to: )

    ref URI

Returns:



519
520
521
# File 'lib/jsi/schema.rb', line 519

def schema_ref(ref = schema_content["$ref"])
  @schema_ref_map[ref]
end

#schema_resource_rootJSI::Base

Deprecated.

after v0.8

a resource containing this schema.

If any ancestor, or this schema itself, is a schema with an absolute uri (see #schema_absolute_uri), the resource root is the closest schema with an absolute uri.

If no ancestor schema has an absolute uri, the schema_resource_root is the document's root node. In this case, the resource root may or may not be a schema itself.

Returns:

  • (JSI::Base)

    resource containing this schema



583
584
585
# File 'lib/jsi/schema.rb', line 583

def schema_resource_root
  jsi_resource_root
end

#schema_resource_root?Boolean

Deprecated.

after v0.8

Returns:

  • (Boolean)


594
595
596
# File 'lib/jsi/schema.rb', line 594

def schema_resource_root?
  jsi_is_resource_root?
end

#schema_uriURI?

a nonrelative URI which refers to this schema. nil if no ancestor of this schema defines an id. see #schema_uris for all URIs known to refer to this schema.

Returns:

  • (URI, nil)


443
444
445
# File 'lib/jsi/schema.rb', line 443

def schema_uri
  schema_uris.first
end

#schema_urisArray<URI>

nonrelative URIs (that is, absolute, but possibly with a fragment) which refer to this schema

Returns:

  • (Array<URI>)


449
450
451
# File 'lib/jsi/schema.rb', line 449

def schema_uris
  @schema_uris_map[schema_content: schema_content]
end

#subschema(subptr) ⇒ JSI::Schema

a subschema of this Schema

Parameters:

  • subptr (JSI::Ptr, #to_ary)

    a relative pointer, or array of tokens, pointing to the subschema

Returns:

  • (JSI::Schema)

    the subschema at the location indicated by subptr. self if subptr is empty.



602
603
604
# File 'lib/jsi/schema.rb', line 602

def subschema(subptr)
  Schema.ensure_schema(jsi_descendent_node(subptr)) { "subschema is not a schema at pointer: #{Ptr.ary_ptr(subptr).pointer}" }
end