Class: JSI::Schema

Inherits:
Object
  • Object
show all
Includes:
FingerprintHash, 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.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from FingerprintHash

#==, #hash

Methods included from Memoize

#clear_memo, #memoize

Constructor Details

#initialize(schema_object) ⇒ Schema

initializes a schema from a given JSI::Base, JSI::JSON::Node, or hash. Boolean schemas are instantiated as their equivalent hash ({} for true and => {} for false).

Parameters:

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

    the schema



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/jsi/schema.rb', line 27

def initialize(schema_object)
  if schema_object.is_a?(JSI::Schema)
    raise(TypeError, "will not instantiate Schema from another Schema: #{schema_object.pretty_inspect.chomp}")
  elsif schema_object.is_a?(JSI::PathedNode)
    @schema_node = JSI.deep_stringify_symbol_keys(schema_object.deref)
  elsif schema_object.respond_to?(:to_hash)
    @schema_node = JSI::JSON::Node.new_doc(JSI.deep_stringify_symbol_keys(schema_object))
  elsif schema_object == true
    @schema_node = JSI::JSON::Node.new_doc({})
  elsif schema_object == false
    @schema_node = JSI::JSON::Node.new_doc({"not" => {}})
  else
    raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
  end
end

Instance Attribute Details

#schema_nodeJSI::PathedNode (readonly) Also known as: schema_object

Returns a JSI::PathedNode (JSI::JSON::Node or JSI::Base) for the schema.

Returns:

  • (JSI::PathedNode)

    a JSI::PathedNode (JSI::JSON::Node or JSI::Base) for the schema



44
45
46
# File 'lib/jsi/schema.rb', line 44

def schema_node
  @schema_node
end

Class Method Details

.from_object(schema_object) ⇒ JSI::Schema

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:



14
15
16
17
18
19
20
# File 'lib/jsi/schema.rb', line 14

def from_object(schema_object)
  if schema_object.is_a?(Schema)
    schema_object
  else
    new(schema_object)
  end
end

Instance Method Details

#[](property_name) ⇒ JSI::Base, ...

Returns property value from the schema_object.

Parameters:

  • property_name (String, Object)

    property name to access from the schema_object

Returns:



50
51
52
# File 'lib/jsi/schema.rb', line 50

def [](property_name)
  schema_object[property_name]
end

#as_json(*opt) ⇒ Object

Returns a jsonifiable representation of this schema

Returns:

  • (Object)

    returns a jsonifiable representation of this schema



301
302
303
# File 'lib/jsi/schema.rb', line 301

def as_json(*opt)
  Typelike.as_json(schema_object, *opt)
end

#default_valueObject



218
219
220
221
222
223
224
225
226
227
228
# File 'lib/jsi/schema.rb', line 218

def default_value
  if schema_node.key?('default')
    if schema_node['default'].respond_to?(:to_ary) || schema_node['default'].respond_to?(:to_hash)
      schema_class.new(schema_node['default'])
    else
      schema_node['default']
    end
  else
    nil
  end
end

#default_value?Boolean

Returns:

  • (Boolean)


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

def default_value?
  schema_node.key?('default')
end

#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 oneOf/allOf/anyOf 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 oneOf/allOf/anyOf subschemas, those schemas are checked (recursively) for their described object property names.



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/jsi/schema.rb', line 198

def described_object_property_names
  memoize(:described_object_property_names) do
    Set.new.tap do |property_names|
      if schema_node['properties'].respond_to?(:to_hash)
        property_names.merge(schema_node['properties'].keys)
      end
      if schema_node['required'].respond_to?(:to_ary)
        property_names.merge(schema_node['required'].to_ary)
      end
      # we _could_ look at the properties of 'default' and each 'enum' but ... nah.
      # we should look at dependencies (TODO).
      %w(oneOf allOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |schemas_key|
        schema_node[schemas_key].map(&:deref).map do |someof_node|
          property_names.merge(self.class.new(someof_node).described_object_property_names)
        end
      end
    end
  end
end

#fingerprintObject

Returns an opaque fingerprint of this Schema for FingerprintHash.

Returns:

  • (Object)

    an opaque fingerprint of this Schema for FingerprintHash



306
307
308
# File 'lib/jsi/schema.rb', line 306

def fingerprint
  {class: self.class, schema_ptr: schema_node.node_ptr, schema_document: JSI::Typelike.as_json(schema_node.node_document)}
end

#fully_validate_instance(other_instance) ⇒ Array<String>

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

Returns:

  • (Array<String>)

    array of schema validation error messages for the given instance against this schema



236
237
238
# File 'lib/jsi/schema.rb', line 236

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

#fully_validate_schemaArray<String>

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

Returns:

  • (Array<String>)

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



256
257
258
# File 'lib/jsi/schema.rb', line 256

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

#inspectString Also known as: to_s

Returns a string representing this Schema.

Returns:

  • (String)

    a string representing this Schema



279
280
281
# File 'lib/jsi/schema.rb', line 279

def inspect
  "\#<#{self.class.inspect} #{object_group_text} #{schema_object.inspect}>"
end

#jsi_schema_classClass subclassing JSI::Base Also known as: schema_class

Returns shortcut for JSI.class_for_schema(schema).

Returns:

  • (Class subclassing JSI::Base)

    shortcut for JSI.class_for_schema(schema)



111
112
113
# File 'lib/jsi/schema.rb', line 111

def jsi_schema_class
  JSI.class_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)



131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/jsi/schema.rb', line 131

def match_to_instance(other_instance)
  # matching oneOf is good here. one schema for one instance.
  # matching anyOf is okay. there could be more than one schema matched. it's often just one. if more
  #   than one is a match, you just get the first one.
  %w(oneOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |someof_key|
    schema_node[someof_key].map(&:deref).map do |someof_node|
      someof_schema = self.class.new(someof_node)
      if someof_schema.validate_instance(other_instance)
        return someof_schema.match_to_instance(other_instance)
      end
    end
  end
  return self
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



121
122
123
# File 'lib/jsi/schema.rb', line 121

def new_jsi(other_instance, *a, &b)
  jsi_schema_class.new(other_instance, *a, &b)
end

#object_group_textString

Returns a string for #instance and #pretty_print including the schema_id.

Returns:

  • (String)

    a string for #instance and #pretty_print including the schema_id



274
275
276
# File 'lib/jsi/schema.rb', line 274

def object_group_text
  "schema_id=#{schema_id}"
end

#pretty_print(q) ⇒ void

This method returns an undefined value.

pretty-prints a representation this Schema to the given printer



286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/jsi/schema.rb', line 286

def pretty_print(q)
  q.instance_exec(self) do |obj|
    text "\#<#{obj.class.inspect} #{obj.object_group_text}"
    group_sub {
      nest(2) {
        breakable ' '
        pp obj.schema_object
      }
    }
    breakable ''
    text '>'
  end
end

#schema_idString

Returns an absolute id for the schema, with a json pointer fragment.

Returns:

  • (String)

    an absolute id for the schema, with a json pointer fragment



55
56
57
58
59
60
61
62
63
64
65
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
# File 'lib/jsi/schema.rb', line 55

def schema_id
  @schema_id ||= begin
    # start from schema_node 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 = schema_node
    path_from_id_node = []
    done = false

    while !done
      # TODO: track what parents are schemas. somehow.
      # look at 'id' if node_for_id is a schema, or the document root.
      # decide whether to look at '$id' for all parent nodes or also just schemas.
      if node_for_id.respond_to?(:to_hash)
        if node_for_id.node_ptr.root? || node_for_id.object_id == schema_node.object_id
          # I'm only looking at 'id' for the document root and the schema node
          # until I track what parents are schemas.
          parent_id = node_for_id['$id'] || node_for_id['id']
        else
          # will look at '$id' everywhere since it is less likely to show up outside schemas than
          # 'id', but it will be better to only look at parents that are schemas for this too.
          parent_id = node_for_id['$id']
        end
      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)
    else
      node_for_id = schema_node.document_root_node
      validator = ::JSON::Validator.new(Typelike.as_json(node_for_id), nil)
      # TODO not good instance_exec'ing into another library's ivars
      parent_auri = validator.instance_exec { @base_schema }.uri
    end
    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
  end
end

#subschema_for_index(index_) ⇒ JSI::Schema?

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

Parameters:

  • index_ (Integer)

    the index for which to find a subschema

Returns:

  • (JSI::Schema, nil)

    a subschema from items or additionalItems for the given index



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/jsi/schema.rb', line 176

def subschema_for_index(index_)
  memoize(:subschema_for_index, index_) do |index|
    if schema_object['items'].respond_to?(:to_ary)
      if index < schema_object['items'].size
        self.class.new(schema_object['items'][index])
      elsif schema_object['additionalItems'].respond_to?(:to_hash)
        self.class.new(schema_object['additionalItems'])
      end
    elsif schema_object['items'].respond_to?(:to_hash)
      self.class.new(schema_object['items'])
    else
      nil
    end
  end
end

#subschema_for_property(property_name_) ⇒ JSI::Schema?

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

Parameters:

  • property_name_ (String)

    the property for which to find a subschema

Returns:

  • (JSI::Schema, nil)

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



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/schema.rb', line 150

def subschema_for_property(property_name_)
  memoize(:subschema_for_property, property_name_) do |property_name|
    if schema_object['properties'].respond_to?(:to_hash) && schema_object['properties'][property_name].respond_to?(:to_hash)
      self.class.new(schema_object['properties'][property_name])
    else
      if schema_object['patternProperties'].respond_to?(:to_hash)
        _, pattern_schema_object = schema_object['patternProperties'].detect do |pattern, _|
          property_name.to_s =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
        end
      end
      if pattern_schema_object
        self.class.new(pattern_schema_object)
      else
        if schema_object['additionalProperties'].respond_to?(:to_hash)
          self.class.new(schema_object['additionalProperties'])
        else
          nil
        end
      end
    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



241
242
243
# File 'lib/jsi/schema.rb', line 241

def validate_instance(other_instance)
  ::JSON::Validator.validate(JSI::Typelike.as_json(schema_node.node_document), JSI::Typelike.as_json(other_instance), fragment: schema_node.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



249
250
251
# File 'lib/jsi/schema.rb', line 249

def validate_instance!(other_instance)
  ::JSON::Validator.validate!(JSI::Typelike.as_json(schema_node.node_document), JSI::Typelike.as_json(other_instance), fragment: schema_node.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



261
262
263
# File 'lib/jsi/schema.rb', line 261

def validate_schema
  ::JSON::Validator.validate(JSI::Typelike.as_json(schema_node.node_document), [], fragment: schema_node.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



269
270
271
# File 'lib/jsi/schema.rb', line 269

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