Module: JSI::Schema

Included in:
Draft04, Draft06, Draft07
Defined in:
lib/jsi/schema.rb,
lib/jsi/schema/issue.rb,
lib/jsi/schema/draft04.rb,
lib/jsi/schema/draft06.rb,
lib/jsi/schema/draft07.rb

Overview

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

the content of an instance which is a JSI::Schema (referred to in this context as schema_content) is expected to be a Hash (JSON object) or a Boolean.

Defined Under Namespace

Modules: Application, BigMoneyId, DescribesSchema, Draft04, Draft06, Draft07, IdWithAnchor, OldId, SchemaAncestorNode, Validation Classes: Error, Issue, NotASchemaError, Ref, ReferenceError

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.default_metaschemanil, #new_schema

an application-wide default metaschema set by default_metaschema=, used by JSI.new_schema

Returns:


177
178
179
180
# File 'lib/jsi/schema.rb', line 177

def default_metaschema
  return @default_metaschema if instance_variable_defined?(:@default_metaschema)
  return nil
end

.default_metaschema=(default_metaschema) ⇒ Object

sets an application-wide default metaschema used by JSI.new_schema

Parameters:

  • default_metaschema (#new_schema)

    the default metaschema. this may be a metaschema or a metaschema's schema module (e.g. JSI::JSONSchemaOrgDraft07).


186
187
188
189
190
191
# File 'lib/jsi/schema.rb', line 186

def default_metaschema=(default_metaschema)
  unless default_metaschema.respond_to?(:new_schema)
    raise(TypeError, "given default_metaschema does not respond to #new_schema")
  end
  @default_metaschema = default_metaschema
end

.ensure_schema(schema, msg: "indicated object is not a 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

  • msg (#to_s, #to_ary) (defaults to: "indicated object is not a schema:")

    lines of the error message preceding the pretty-printed schema param if the schema param is not a schema

Returns:

  • (Schema)

    the given schema

Raises:


271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/jsi/schema.rb', line 271

def ensure_schema(schema, msg: "indicated object is not a schema:", reinstantiate_as: nil)
  if schema.is_a?(Schema)
    schema
  else
    if reinstantiate_as
      # TODO warn; behavior is undefined and I hate this implementation

      result_schema_schemas = schema.jsi_schemas + reinstantiate_as

      result_schema_class = JSI::SchemaClasses.class_for_schemas(result_schema_schemas,
        includes: SchemaClasses.includes_for(schema.jsi_node_content)
      )

      result_schema_class.new(schema.jsi_document,
        jsi_ptr: schema.jsi_ptr,
        jsi_root_node: schema.jsi_root_node,
        jsi_schema_base_uri: schema.jsi_schema_base_uri,
        jsi_schema_resource_ancestors: schema.jsi_schema_resource_ancestors,
      )
    else
      raise(NotASchemaError, [
        *msg,
        schema.pretty_inspect.chomp,
      ].join("\n"))
    end
  end
end

.new_schema(schema_object, default_metaschema: nil, **kw) ⇒ JSI::Base

instantiates a given schema object as a JSI Schema.

the metaschema to use to instantiate the schema must be indicated.

  • if the schema object has a $schema property, that URI is resolved using the JSI.schema_registry, and that metaschema is used.
  • if no $schema property is present, the default_metaschema param is used, if the caller specifies it.
  • if no default_metaschema param is specified, the application-wide default JSI::Schema.default_metaschema is used, if the application has set it.

an ArgumentError is raised if none of these indicate a metaschema to use.

note that if you are instantiating a schema known to have no $schema property, an alternative to passing the default_metaschema param is to use .new_schema on the metaschema or its module, e.g. JSI::JSONSchemaOrgDraft07.new_schema(my_schema_object)

if the given schema_object is a JSI::Base but not already a JSI::Schema, an error will be raised. schemas which describe schemas must include JSI::Schema in their #jsi_schema_module.

Parameters:

  • schema_object (#to_hash, Boolean, JSI::Schema)

    an object to be instantiated as a schema. if it's already a JSI::Schema, it is returned as-is.

  • default_metaschema (#new_schema) (defaults to: nil)

    the metaschema to use if the schema_object does not have a '$schema' property. this may be a metaschema or a metaschema's schema module (e.g. JSI::JSONSchemaOrgDraft07).

  • uri (nil, #to_str, Addressable::URI)

    the URI of the schema document. relative URIs within the document are resolved using this uri as their base. the result schema will be registered with this URI in the JSI.schema_registry.

Returns:

  • (JSI::Base)

    a JSI which is a JSI::Schema whose instance is the given schema_object and whose schemas are the metaschema's inplace applicators.


223
224
225
226
227
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
# File 'lib/jsi/schema.rb', line 223

def new_schema(schema_object, default_metaschema: nil, **kw)
  default_metaschema_new_schema = -> {
    default_metaschema ||= JSI::Schema.default_metaschema
    if default_metaschema.nil?
      raise(ArgumentError, [
        "when instantiating a schema with no `$schema` property, you must specify the metaschema.",
        "you may pass the `default_metaschema` param to this method.",
        "JSI::Schema.default_metaschema may be set to an application-wide default metaschema.",
        "you may alternatively use new_schema on the appropriate metaschema or its schema module.",
        "instantiating schema_object: #{schema_object.pretty_inspect.chomp}",
      ].join("\n"))
    end
    if !default_metaschema.respond_to?(:new_schema)
      raise(TypeError, "given default_metaschema does not respond to #new_schema: #{default_metaschema.pretty_inspect.chomp}")
    end
    default_metaschema.new_schema(schema_object, **kw)
  }
  if schema_object.is_a?(Schema)
    schema_object
  elsif schema_object.is_a?(JSI::Base)
    raise(NotASchemaError, "the given schema_object is a JSI::Base, but is not a JSI::Schema: #{schema_object.pretty_inspect.chomp}")
  elsif schema_object.respond_to?(:to_hash)
    if schema_object.key?('$schema')
      unless schema_object['$schema'].respond_to?(:to_str)
        raise(ArgumentError, "given schema_object keyword `$schema` is not a string")
      end
      metaschema = Schema::Ref.new(schema_object['$schema']).deref_schema
      unless metaschema.describes_schema?
        raise(Schema::ReferenceError, "given schema_object contains a $schema but the resource it identifies does not describe a schema")
      end
      metaschema.new_schema(schema_object, **kw)
    else
      default_metaschema_new_schema.call
    end
  elsif [true, false].include?(schema_object)
    default_metaschema_new_schema.call
  else
    raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
  end
end

Instance Method Details

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

562
563
564
565
566
567
568
569
570
571
572
573
# File 'lib/jsi/schema.rb', line 562

def described_object_property_names
  jsi_memoize(:described_object_property_names) do
    Set.new.tap do |property_names|
      if schema_content.respond_to?(:to_hash) && schema_content['properties'].respond_to?(:to_hash)
        property_names.merge(schema_content['properties'].keys)
      end
      if schema_content.respond_to?(:to_hash) && schema_content['required'].respond_to?(:to_ary)
        property_names.merge(schema_content['required'].to_ary)
      end
    end.freeze
  end
end

#describes_schema!(schema_implementation_modules) ⇒ void

This method returns an undefined value.

indicates that this schema describes a schema. this schema is extended with DescribesSchema and its #jsi_schema_module is extended with DescribesSchemaModule, and the JSI Schema Module will include the given modules.

Parameters:

  • schema_implementation_modules (Enumerable<Module>)

    modules which implement the functionality of the schema to extend schemas described by this schema. this must include JSI::Schema (usually indirectly).


448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/jsi/schema.rb', line 448

def describes_schema!(schema_implementation_modules)
  schema_implementation_modules = Util.ensure_module_set(schema_implementation_modules)

  unless schema_implementation_modules.any? { |mod| mod <= Schema }
    raise(ArgumentError, "schema_implementation_modules for a schema must include #{Schema}")
  end

  if describes_schema?
    # this schema, or one equal to it, 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 schema_implementation_modules, though.
    unless jsi_schema_module.schema_implementation_modules == schema_implementation_modules
      raise(ArgumentError, "this schema already describes a schema with different schema_implementation_modules")
    end
  else
    schema_implementation_modules.each do |mod|
      jsi_schema_module.include(mod)
    end
    jsi_schema_module.extend(DescribesSchemaModule)
    jsi_schema_module.instance_variable_set(:@schema_implementation_modules, schema_implementation_modules)
  end

  extend(DescribesSchema)

  nil
end

#describes_schema?Boolean

does this schema itself describe a schema?

Returns:

  • (Boolean)

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

def describes_schema?
  jsi_schema_module <= JSI::Schema ||
    # deprecated
    jsi_schema_instance_modules.any? { |m| m <= JSI::Schema }
end

#each_schema_uri {|Addressable::URI| ... } ⇒ Enumerator?

Yields:

  • (Addressable::URI)

Returns:

  • (Enumerator, nil)

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
# File 'lib/jsi/schema.rb', line 348

def each_schema_uri
  return to_enum(__method__) unless block_given?

  yield schema_absolute_uri if schema_absolute_uri

  parent_schemas = jsi_subschema_resource_ancestors.reverse_each.select do |resource|
    resource.schema_absolute_uri
  end

  anchored = respond_to?(:anchor) ? anchor : nil
  parent_schemas.each do |parent_schema|
    if anchored
      if parent_schema.jsi_anchor_subschema(anchor) == self
        yield parent_schema.schema_absolute_uri.merge(fragment: anchor)
      else
        anchored = false
      end
    end

    relative_ptr = jsi_ptr.relative_to(parent_schema.jsi_ptr)
    yield parent_schema.schema_absolute_uri.merge(fragment: relative_ptr.fragment)
  end

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

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

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

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

validates the given instance against this schema

Parameters:

  • instance (Object)

    the instance to validate against this schema

Returns:


579
580
581
582
583
584
585
586
587
588
# File 'lib/jsi/schema.rb', line 579

def instance_validate(instance)
  if instance.is_a?(Base)
    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_schema_instance_modulesSet<Module>

Deprecated.

reopen the jsi_schema_module to include such modules instead, or use describes_schema! if this schema describes a schema

modules to apply to instances described by this schema. these modules are included on this schema's #jsi_schema_module

Returns:

  • (Set<Module>)

427
428
429
430
# File 'lib/jsi/schema.rb', line 427

def jsi_schema_instance_modules
  return @jsi_schema_instance_modules if instance_variable_defined?(:@jsi_schema_instance_modules)
  return Util::EMPTY_SET
end

#jsi_schema_instance_modules=(jsi_schema_instance_modules) ⇒ void

Deprecated.

This method returns an undefined value.

see #jsi_schema_instance_modules


436
437
438
# File 'lib/jsi/schema.rb', line 436

def jsi_schema_instance_modules=(jsi_schema_instance_modules)
  @jsi_schema_instance_modules = Util.ensure_module_set(jsi_schema_instance_modules)
end

#jsi_schema_moduleModule

a module which extends all instances of this schema. this may be opened by the application to add methods to schema instances.

this module includes accessor methods for object property names this schema describes (see #described_object_property_names). these accessors wrap Base#[] and Base#[]=.

some functionality is also defined on the module itself (its singleton class, not for its instances):

  • the module is extended with JSI::SchemaModule, which defines .new_jsi to instantiate instances of this schema (see #new_jsi).
  • properties described by this schema's metaschema are defined as methods to get subschemas' schema modules, so for example schema.jsi_schema_module.items returns the same module as schema.items.jsi_schema_module.
  • method .schema which returns this schema.

Returns:

  • (Module)

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

def jsi_schema_module
  JSI::SchemaClasses.module_for_schema(self)
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


400
401
402
# File 'lib/jsi/schema.rb', line 400

def jsi_schema_module_exec(*a, **kw, &block)
  jsi_schema_module.module_exec(*a, **kw, &block)
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:


604
605
606
607
608
609
610
# File 'lib/jsi/schema.rb', line 604

def jsi_subschema_resource_ancestors
  if schema_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)

309
310
311
312
# File 'lib/jsi/schema.rb', line 309

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

#new_jsi(instance, **kw) ⇒ JSI::Base subclass

instantiates the given instance as a JSI::Base class for schemas matched from this schema to the instance.

Parameters:

  • instance (Object)

    the JSON Schema instance to be represented as a JSI

  • uri (nil, #to_str, Addressable::URI)

    for an instance document containing schemas, this is the URI of the document, whether or not the document is itself a schema. in the normal case where the document does not contain any schemas, uri has no effect. schemas within the document use this uri as the base URI to resolve relative URIs. the resulting JSI may be registered with a JSI::SchemaRegistry (see JSI.schema_registry).

Returns:

  • (JSI::Base subclass)

    a JSI whose instance is the given instance and whose schemas are inplace applicator schemas matched from this schema.


411
412
413
# File 'lib/jsi/schema.rb', line 411

def new_jsi(instance, **kw)
  SchemaSet[self].new_jsi(instance, **kw)
end

#resource_root_subschema(ptr) ⇒ JSI::Schema

a schema in the same schema resource as this one (see #schema_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:


527
528
529
# File 'lib/jsi/schema.rb', line 527

def resource_root_subschema(ptr)
  resource_root_subschema_map[ptr: Ptr.ary_ptr(ptr)]
end

#schema_absolute_uriAddressable::URI?

the URI of this schema, calculated from our #id, resolved against our #jsi_schema_base_uri

Returns:

  • (Addressable::URI, nil)

316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/jsi/schema.rb', line 316

def schema_absolute_uri
  if respond_to?(:id_without_fragment) && id_without_fragment
    if jsi_schema_base_uri
      jsi_schema_base_uri.join(id_without_fragment)
    elsif id_without_fragment.absolute?
      id_without_fragment
    else
      # TODO warn / schema_error
      nil
    end
  end
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.


303
304
305
# File 'lib/jsi/schema.rb', line 303

def schema_content
  jsi_node_content
end

#schema_resource_rootJSI::Base

a resource containing this schema.

if any parent, 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 parent schema has an absolute uri, the schema_resource_root is the root of the document (our #jsi_root_node). in this case, the resource root may or may not be a schema itself.

Returns:

  • (JSI::Base)

    resource containing this schema


484
485
486
# File 'lib/jsi/schema.rb', line 484

def schema_resource_root
  jsi_subschema_resource_ancestors.reverse_each.detect(&:schema_resource_root?) || jsi_root_node
end

#schema_resource_root?Boolean

is this schema the root of a schema resource?

Returns:

  • (Boolean)

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

def schema_resource_root?
  jsi_ptr.root? || !!schema_absolute_uri
end

#schema_uriAddressable::URI?

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

Returns:

  • (Addressable::URI, nil)

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

def schema_uri
  schema_uris.first
end

#schema_urisArray<Addressable::URI>

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

Returns:

  • (Array<Addressable::URI>)

339
340
341
342
343
# File 'lib/jsi/schema.rb', line 339

def schema_uris
  jsi_memoize(:schema_uris) do
    each_schema_uri.to_a
  end
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.


498
499
500
# File 'lib/jsi/schema.rb', line 498

def subschema(subptr)
  subschema_map[subptr: Ptr.ary_ptr(subptr)]
end