Class: JSI::Schema
- Inherits:
-
Object
- Object
- JSI::Schema
- 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
-
#schema_jsi ⇒ JSI::Base?
readonly
A JSI for this schema, if a metaschema is known; otherwise nil.
-
#schema_node ⇒ JSI::JSON::Node
readonly
A JSI::JSON::Node for the schema.
Instance Method Summary collapse
-
#[](property_name) ⇒ JSI::Base, ...
Property value from the schema_object.
-
#as_json(*opt) ⇒ Object
Returns a jsonifiable representation of this schema.
-
#described_object_property_names ⇒ Set
Any object property names this schema indicates may be present on its instances.
-
#fingerprint ⇒ Object
An opaque fingerprint of this Schema for FingerprintHash.
-
#fully_validate(instance) ⇒ Array<String>
Array of schema validation error messages for the given instance against this schema.
-
#fully_validate_schema ⇒ Array<String>
Array of schema validation error messages for this schema, validated against its metaschema.
-
#initialize(schema_object) ⇒ Schema
constructor
initializes a schema from a given JSI::Base, JSI::JSON::Node, or hash.
-
#inspect ⇒ String
(also: #to_s)
A string representing this Schema.
-
#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.
-
#object_group_text ⇒ String
A string for #instance and #pretty_print including the schema_id.
-
#pretty_print(q) ⇒ void
pretty-prints a representation this Schema to the given printer.
-
#schema_class ⇒ Class subclassing JSI::Base
Shortcut for JSI.class_for_schema(schema).
-
#schema_id ⇒ String
An absolute id for the schema, with a json pointer fragment.
-
#schema_object ⇒ JSI::Base, JSI::JSON::Node
Either a JSI::Base subclass or a JSI::JSON::Node for the schema.
-
#subschema_for_index(index_) ⇒ JSI::Schema?
A subschema from
itemsoradditionalItemsfor the given index. -
#subschema_for_property(property_name_) ⇒ JSI::Schema?
A subschema from
properties,patternProperties, oradditionalPropertiesfor the given property_name. -
#validate(instance) ⇒ true, false
Whether the given instance validates against this schema.
-
#validate!(instance) ⇒ true
If this method does not raise, it returns true to indicate the instance is valid against this schema.
-
#validate_schema ⇒ true, false
Whether this schema validates against its metaschema.
-
#validate_schema! ⇒ true
If this method does not raise, it returns true to indicate this schema is valid against its metaschema.
Methods included from FingerprintHash
Methods included from Memoize
Constructor Details
#initialize(schema_object) ⇒ Schema
initializes a schema from a given JSI::Base, JSI::JSON::Node, or hash.
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_jsi ⇒ JSI::Base? (readonly)
Returns 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_node ⇒ JSI::JSON::Node (readonly)
Returns a JSI::JSON::Node for the schema.
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.
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
271 272 273 |
# File 'lib/jsi/schema.rb', line 271 def as_json(*opt) Typelike.as_json(schema_object, *opt) end |
#described_object_property_names ⇒ Set
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.
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 |
#fingerprint ⇒ Object
Returns 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.
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_schema ⇒ Array<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.
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 |
#inspect ⇒ String Also known as: to_s
Returns 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.
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_text ⇒ String
Returns 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_class ⇒ Class subclassing JSI::Base
Returns 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_id ⇒ String
Returns 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_object ⇒ JSI::Base, JSI::JSON::Node
Returns either a JSI::Base subclass or a JSI::JSON::Node for the schema.
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.
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.
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.
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.
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_schema ⇒ true, false
Returns 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.
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 |