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 JSONSchemaOrgDraft07
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:


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

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)

      result_schema_class.new(Base::NOINSTANCE,
        jsi_document: 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::Schema Also known as: new, from_object

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 have JSI::Schema in their Schema#jsi_schema_instance_modules.

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::Schema)

    a JSI::Schema representing the given schema_object


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
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/jsi/schema.rb', line 222

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') && schema_object['$schema'].respond_to?(:to_str)
      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)

526
527
528
529
530
531
532
533
534
535
536
537
# File 'lib/jsi/schema.rb', line 526

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?Boolean

does this schema itself describe a schema?

Returns:

  • (Boolean)

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

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

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

Yields:

  • (Addressable::URI)

Returns:

  • (Enumerator, nil)

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

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.is_a?(Schema) && resource.schema_absolute_uri
  end

  anchored = self.anchor
  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 = self.jsi_ptr.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)

557
558
559
560
561
562
# File 'lib/jsi/schema.rb', line 557

def instance_valid?(instance)
  if instance.is_a?(JSI::PathedNode)
    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:


543
544
545
546
547
548
549
550
551
552
# File 'lib/jsi/schema.rb', line 543

def instance_validate(instance)
  if instance.is_a?(JSI::PathedNode)
    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>

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

Returns:

  • (Set<Module>)

425
426
427
428
# File 'lib/jsi/schema.rb', line 425

def jsi_schema_instance_modules
  return @jsi_schema_instance_modules if instance_variable_defined?(:@jsi_schema_instance_modules)
  return Set[].freeze
end

#jsi_schema_instance_modules=(jsi_schema_instance_modules) ⇒ void

This method returns an undefined value.

see #jsi_schema_instance_modules


433
434
435
# File 'lib/jsi/schema.rb', line 433

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)

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

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


394
395
396
# File 'lib/jsi/schema.rb', line 394

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


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

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:


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

def resource_root_subschema(ptr)
  resource_root_subschema_map[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)

310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/jsi/schema.rb', line 310

def schema_absolute_uri
  if respond_to?(:id_without_fragment) && id_without_fragment
    if jsi_schema_base_uri
      Addressable::URI.parse(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 PathedNode#jsi_node_content, named for clarity in the context of working with a schema.


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

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


446
447
448
# File 'lib/jsi/schema.rb', line 446

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)

452
453
454
# File 'lib/jsi/schema.rb', line 452

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)

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

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

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

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.


460
461
462
# File 'lib/jsi/schema.rb', line 460

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