Module: JSI::Schema

Includes:
Memoize
Defined in:
lib/jsi/schema.rb

Overview

JSI::Schema represents a JSON Schema. initialized from a Hash-like schema object, JSI::Schema is a relatively simple class to abstract useful methods applied to a JSON Schema.

Defined Under Namespace

Modules: DescribesSchema Classes: Error, NotASchemaError

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Memoize

#jsi_clear_memo, #jsi_memoize

Class Method Details

.default_metaschemaJSI::Schema

Returns the default metaschema.

Returns:



28
29
30
# File 'lib/jsi/schema.rb', line 28

def default_metaschema
  JSI::JSONSchemaOrgDraft06.schema
end

.from_object(schema_object) ⇒ JSI::Schema Also known as: new

instantiates a given schema object as a JSI::Schema.

schemas are instantiated according to their '$schema' property if specified. otherwise their schema will be the default_metaschema.

if the given schema_object is a JSI::Base but not already a JSI::Schema, an error will be raised. JSI::Base should already extend a given instance with JSI::Schema when its schema describes a schema (by extending with JSI::Schema::DescribesSchema).

Parameters:

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

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

Returns:

  • (JSI::Schema)

    a JSI::Schema representing the given schema_object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/jsi/schema.rb', line 52

def from_object(schema_object)
  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)
    schema_object = JSI.deep_stringify_symbol_keys(schema_object)
    if schema_object.key?('$schema') && schema_object['$schema'].respond_to?(:to_str)
      if schema_object['$schema'] == schema_object['$id'] || schema_object['$schema'] == schema_object['id']
        MetaschemaNode.new(schema_object)
      else
        metaschema = supported_metaschemas.detect { |ms| schema_object['$schema'] == ms['$id'] || schema_object['$schema'] == ms['id'] }
        unless metaschema
          raise(NotImplementedError, "metaschema not supported: #{schema_object['$schema']}")
        end
        metaschema.new_jsi(schema_object)
      end
    else
      default_metaschema.new_jsi(schema_object)
    end
  elsif [true, false].include?(schema_object)
    default_metaschema.new_jsi(schema_object)
  else
    raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
  end
end

.supported_metaschemasArray<JSI::Schema>

Returns supported metaschemas.

Returns:



33
34
35
36
37
38
# File 'lib/jsi/schema.rb', line 33

def supported_metaschemas
  [
    JSI::JSONSchemaOrgDraft04.schema,
    JSI::JSONSchemaOrgDraft06.schema,
  ]
end

Instance Method Details

#described_object_property_namesSet

Returns any object property names this schema indicates may be present on its instances. this includes, if present: keys of this schema's "properties" object; entries of this schema's array of "required" property keys. if this schema has allOf subschemas, those schemas are checked (recursively) for their described object property names.

Returns:

  • (Set)

    any object property names this schema indicates may be present on its instances. this includes, if present: keys of this schema's "properties" object; entries of this schema's array of "required" property keys. if this schema has allOf subschemas, those schemas are checked (recursively) for their described object property names.



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/jsi/schema.rb', line 205

def described_object_property_names
  jsi_memoize(:described_object_property_names) do
    Set.new.tap do |property_names|
      if node_content.respond_to?(:to_hash) && node_content['properties'].respond_to?(:to_hash)
        property_names.merge(node_content['properties'].keys)
      end
      if node_content.respond_to?(:to_hash) && node_content['required'].respond_to?(:to_ary)
        property_names.merge(node_content['required'].to_ary)
      end
      # we should look at dependencies (TODO).
      if respond_to?(:to_hash) && self['allOf'].respond_to?(:to_ary)
        self['allOf'].select{ |s| s.is_a?(JSI::Schema) }.map(&:deref).map do |allOf_schema|
          property_names.merge(allOf_schema.described_object_property_names)
        end
      end
    end
  end
end

#describes_schema?Boolean

Returns does this schema itself describe a schema?.

Returns:

  • (Boolean)

    does this schema itself describe a schema?



147
148
149
# File 'lib/jsi/schema.rb', line 147

def describes_schema?
  is_a?(JSI::Schema::DescribesSchema)
end

#fully_validate_instance(other_instance, errors_as_objects: false) ⇒ Array

Returns array of schema validation errors for the given instance against this schema.

Returns:

  • (Array)

    array of schema validation errors for the given instance against this schema



226
227
228
# File 'lib/jsi/schema.rb', line 226

def fully_validate_instance(other_instance, errors_as_objects: false)
  ::JSON::Validator.fully_validate(JSI::Typelike.as_json(node_document), JSI::Typelike.as_json(other_instance), fragment: node_ptr.fragment, errors_as_objects: errors_as_objects)
end

#fully_validate_schema(errors_as_objects: false) ⇒ Array

Returns array of schema validation errors for this schema, validated against its metaschema. a default metaschema is assumed if the schema does not specify a $schema.

Returns:

  • (Array)

    array of schema validation errors for this schema, validated against its metaschema. a default metaschema is assumed if the schema does not specify a $schema.



246
247
248
# File 'lib/jsi/schema.rb', line 246

def fully_validate_schema(errors_as_objects: false)
  ::JSON::Validator.fully_validate(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true, errors_as_objects: errors_as_objects)
end

#jsi_schema_classClass subclassing JSI::Base

Returns shortcut for JSI.class_for_schema(schema).

Returns:

  • (Class subclassing JSI::Base)

    shortcut for JSI.class_for_schema(schema)



134
135
136
# File 'lib/jsi/schema.rb', line 134

def jsi_schema_class
  JSI.class_for_schema(self)
end

#jsi_schema_moduleModule

Returns a module representing this schema. see JSI::SchemaClasses.module_for_schema.

Returns:



129
130
131
# File 'lib/jsi/schema.rb', line 129

def jsi_schema_module
  JSI::SchemaClasses.module_for_schema(self)
end

#match_to_instance(other_instance) ⇒ JSI::Schema

if this schema is a oneOf, allOf, anyOf schema, #match_to_instance finds one of the subschemas that matches the given instance and returns it. if there are no matching *Of schemas, this schema is returned.

Parameters:

  • other_instance (Object)

    the instance to which to attempt to match *Of subschemas

Returns:

  • (JSI::Schema)

    a matched subschema, or this schema (self)



157
158
159
160
161
162
163
164
165
166
# File 'lib/jsi/schema.rb', line 157

def match_to_instance(other_instance)
  ptr = node_ptr
  ptr = ptr.deref(node_document)
  ptr = ptr.schema_match_ptr_to_instance(node_document, other_instance)
  if ptr
    ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
  else
    self
  end
end

#new_jsi(other_instance, *a, &b) ⇒ JSI::Base

calls #new on the class for this schema with the given arguments. for parameters, see JSI::Base#initialize documentation.

Returns:

  • (JSI::Base)

    a JSI whose schema is this schema and whose instance is the given instance



142
143
144
# File 'lib/jsi/schema.rb', line 142

def new_jsi(other_instance, *a, &b)
  JSI.class_for_schema(match_to_instance(other_instance)).new(other_instance, *a, &b)
end

#schema_idString?

Returns an absolute id for the schema, with a json pointer fragment. nil if no parent of this schema defines an id.

Returns:

  • (String, nil)

    an absolute id for the schema, with a json pointer fragment. nil if no parent of this schema defines an id.



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

def schema_id
  return @schema_id if instance_variable_defined?(:@schema_id)
  @schema_id = begin
    # start from self and ascend parents looking for an 'id' property.
    # append a fragment to that id (appending to an existing fragment if there
    # is one) consisting of the path from that parent to our schema_node.
    node_for_id = self
    path_from_id_node = []
    done = false

    while !done
      node_content_for_id = node_for_id.node_content
      if node_for_id.is_a?(JSI::Schema) && node_content_for_id.respond_to?(:to_hash)
        parent_id = node_content_for_id.key?('$id') && node_content_for_id['$id'].respond_to?(:to_str) ? node_content_for_id['$id'].to_str :
          node_content_for_id.key?('id') && node_content_for_id['id'].respond_to?(:to_str) ? node_content_for_id['id'].to_str : nil
      end

      if parent_id || node_for_id.node_ptr.root?
        done = true
      else
        path_from_id_node.unshift(node_for_id.node_ptr.reference_tokens.last)
        node_for_id = node_for_id.parent_node
      end
    end
    if parent_id
      parent_auri = Addressable::URI.parse(parent_id)
      if parent_auri.fragment
        # add onto the fragment
        parent_id_path = JSI::JSON::Pointer.from_fragment('#' + parent_auri.fragment).reference_tokens
        path_from_id_node = parent_id_path + path_from_id_node
        parent_auri.fragment = nil
      #else: no fragment so parent_id good as is
      end

      fragment = JSI::JSON::Pointer.new(path_from_id_node).fragment
      schema_id = parent_auri.to_s + fragment

      schema_id
    else
      nil
    end
  end
end

#subschema_for_index(index) ⇒ JSI::Schema?

Returns a subschema from items or additionalItems for the given token.

Parameters:

  • index (Integer)

    the array index for which to find a subschema

Returns:

  • (JSI::Schema, nil)

    a subschema from items or additionalItems for the given token



186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/jsi/schema.rb', line 186

def subschema_for_index(index)
  jsi_memoize(:subschema_for_index, index) do |index|
    ptr = node_ptr
    ptr = ptr.deref(node_document)
    ptr = ptr.schema_subschema_ptr_for_index(node_document, index)
    if ptr
      ptr = ptr.deref(node_document)
      ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
    else
      nil
    end
  end
end

#subschema_for_property(property_name) ⇒ JSI::Schema?

Returns a subschema from properties, patternProperties, or additionalProperties for the given token.

Parameters:

  • property_name (String)

    the property name for which to find a subschema

Returns:

  • (JSI::Schema, nil)

    a subschema from properties, patternProperties, or additionalProperties for the given token



170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/jsi/schema.rb', line 170

def subschema_for_property(property_name)
  jsi_memoize(:subschema_for_property, property_name) do |property_name|
    ptr = node_ptr
    ptr = ptr.deref(node_document)
    ptr = ptr.schema_subschema_ptr_for_property_name(node_document, property_name)
    if ptr
      ptr = ptr.deref(node_document)
      ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
    else
      nil
    end
  end
end

#validate_instance(other_instance) ⇒ true, false

Returns whether the given instance validates against this schema.

Returns:

  • (true, false)

    whether the given instance validates against this schema



231
232
233
# File 'lib/jsi/schema.rb', line 231

def validate_instance(other_instance)
  ::JSON::Validator.validate(JSI::Typelike.as_json(node_document), JSI::Typelike.as_json(other_instance), fragment: node_ptr.fragment)
end

#validate_instance!(other_instance) ⇒ true

Returns if this method does not raise, it returns true to indicate the instance is valid against this schema.

Returns:

  • (true)

    if this method does not raise, it returns true to indicate the instance is valid against this schema

Raises:

  • (::JSON::Schema::ValidationError)

    raises if the instance has validation errors against this schema



239
240
241
# File 'lib/jsi/schema.rb', line 239

def validate_instance!(other_instance)
  ::JSON::Validator.validate!(JSI::Typelike.as_json(node_document), JSI::Typelike.as_json(other_instance), fragment: node_ptr.fragment)
end

#validate_schematrue, false

Returns whether this schema validates against its metaschema.

Returns:

  • (true, false)

    whether this schema validates against its metaschema



251
252
253
# File 'lib/jsi/schema.rb', line 251

def validate_schema
  ::JSON::Validator.validate(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true)
end

#validate_schema!true

Returns if this method does not raise, it returns true to indicate this schema is valid against its metaschema.

Returns:

  • (true)

    if this method does not raise, it returns true to indicate this schema is valid against its metaschema

Raises:

  • (::JSON::Schema::ValidationError)

    raises if this schema has validation errors against its metaschema



259
260
261
# File 'lib/jsi/schema.rb', line 259

def validate_schema!
  ::JSON::Validator.validate!(JSI::Typelike.as_json(node_document), [], fragment: node_ptr.fragment, validate_schema: true, list: true)
end