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

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.

Parameters:

  • schema_object (JSI::Base, #to_hash)

    the schema



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/jsi/schema.rb', line 12

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::Base)
    @schema_jsi = JSI.deep_stringify_symbol_keys(schema_object.deref)
    @schema_node = @schema_jsi.instance
  elsif schema_object.is_a?(JSI::JSON::HashNode)
    @schema_jsi = nil
    @schema_node = JSI.deep_stringify_symbol_keys(schema_object.deref)
  elsif schema_object.respond_to?(:to_hash)
    @schema_jsi = nil
    @schema_node = JSI::JSON::Node.new_doc(JSI.deep_stringify_symbol_keys(schema_object))
  else
    raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
  end
end

Instance Attribute Details

#schema_jsiJSI::Base? (readonly)

Returns a JSI for this schema, if a metaschema is known; otherwise nil.

Returns:

  • (JSI::Base, nil)

    a JSI for this schema, if a metaschema is known; otherwise nil



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

def schema_jsi
  @schema_jsi
end

#schema_nodeJSI::JSON::Node (readonly)

Returns a JSI::JSON::Node for the schema.

Returns:



30
31
32
# File 'lib/jsi/schema.rb', line 30

def schema_node
  @schema_node
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:



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

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



271
272
273
# File 'lib/jsi/schema.rb', line 271

def as_json(*opt)
  Typelike.as_json(schema_object, *opt)
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.



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

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



276
277
278
# File 'lib/jsi/schema.rb', line 276

def fingerprint
  {class: self.class, schema_node: schema_node}
end

#fully_validate(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



206
207
208
# File 'lib/jsi/schema.rb', line 206

def fully_validate(instance)
  ::JSON::Validator.fully_validate(JSI::Typelike.as_json(schema_node.document), JSI::Typelike.as_json(instance), fragment: schema_node.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.



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

def fully_validate_schema
  ::JSON::Validator.fully_validate(JSI::Typelike.as_json(schema_node.document), [], fragment: schema_node.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



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

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

#match_to_instance(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:

  • instance (Object)

    the instance to which to attempt to match *Of subschemas

Returns:

  • (JSI::Schema)

    a matched subschema, or this schema (self)



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/jsi/schema.rb', line 114

def match_to_instance(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, the problems of allOf occur.
  # matching allOf is questionable. all of the schemas must be matched but we just return the first match.
  #   there isn't really a better answer with the current implementation. merging the schemas together
  #   is a thought but is not practical.
  %w(oneOf allOf 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)
        return someof_schema.match_to_instance(instance)
      end
    end
  end
  return self
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



244
245
246
# File 'lib/jsi/schema.rb', line 244

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



256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jsi/schema.rb', line 256

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_classClass subclassing JSI::Base

Returns shortcut for JSI.class_for_schema(schema).

Returns:

  • (Class subclassing JSI::Base)

    shortcut for JSI.class_for_schema(schema)



104
105
106
# File 'lib/jsi/schema.rb', line 104

def schema_class
  JSI.class_for_schema(self)
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



48
49
50
51
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/jsi/schema.rb', line 48

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.path.empty? || 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.path.empty?
        done = true
      else
        path_from_id_node.unshift(node_for_id.path.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_node
      validator = ::JSON::Validator.new(node_for_id.content, 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 = ::JSON::Schema::Pointer.new(: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 = ::JSON::Schema::Pointer.new(:reference_tokens, path_from_id_node).fragment
    schema_id = parent_auri.to_s + fragment

    schema_id
  end
end

#schema_objectJSI::Base, JSI::JSON::Node

Returns either a JSI::Base subclass or a JSI::JSON::Node for the schema.

Returns:



37
38
39
# File 'lib/jsi/schema.rb', line 37

def schema_object
  @schema_jsi || @schema_node
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



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/jsi/schema.rb', line 162

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



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/jsi/schema.rb', line 136

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) ⇒ true, false

Returns whether the given instance validates against this schema.

Returns:

  • (true, false)

    whether the given instance validates against this schema



211
212
213
# File 'lib/jsi/schema.rb', line 211

def validate(instance)
  ::JSON::Validator.validate(JSI::Typelike.as_json(schema_node.document), JSI::Typelike.as_json(instance), fragment: schema_node.fragment)
end

#validate!(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



219
220
221
# File 'lib/jsi/schema.rb', line 219

def validate!(instance)
  ::JSON::Validator.validate!(JSI::Typelike.as_json(schema_node.document), JSI::Typelike.as_json(instance), fragment: schema_node.fragment)
end

#validate_schematrue, false

Returns whether this schema validates against its metaschema.

Returns:

  • (true, false)

    whether this schema validates against its metaschema



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

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



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

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