Module: MCPClient::SchemaValidator::Shapes
- Included in:
- MCPClient::SchemaValidator
- Defined in:
- lib/mcp_client/schema_validator/shapes.rb
Overview
The shape every keyword value must have before a schema can be used: an applicator must hold schemas, and an assertion this validator reads must hold what its keyword is defined to hold. A value of the wrong shape is neither ignored (that would turn an assertion into a pass) nor read as written (that would fail every instance): the schema is unusable, and the preflight says which keyword is at fault. Extended into SchemaValidator, so the methods are its own.
Constant Summary collapse
- ASSERTION_SHAPES =
Assertion keywords whose value shape this validator reads, with what each must be. A malformed value is not silently ignored (that would turn an assertion into a pass) nor read as data (that would fail every instance): the schema is unusable, and the caller is told why.
{ 'type' => :type_names, 'enum' => :array, 'required' => :property_names, 'pattern' => :string, 'minLength' => :non_negative_integer, 'maxLength' => :non_negative_integer, 'minItems' => :non_negative_integer, 'maxItems' => :non_negative_integer, 'minimum' => :number, 'maximum' => :number, 'multipleOf' => :positive_number, 'uniqueItems' => :boolean, 'minContains' => :non_negative_integer, 'maxContains' => :non_negative_integer, 'minProperties' => :non_negative_integer, 'maxProperties' => :non_negative_integer, 'dependentRequired' => :dependent_required }.freeze
- ANNOTATION_SHAPES =
The standard keywords that only annotate, with the type JSON Schema 2020-12 Validation Section 9 (and Section 8 for the content keywords) gives each. Annotating rather than asserting does not exempt a keyword from being written correctly: a schema that spells one wrong is a malformed document, and reading it as usable let :strict check results against a schema no validator could read.
{ 'title' => :string, 'description' => :string, '$comment' => :string, 'format' => :string, 'contentEncoding' => :string, 'contentMediaType' => :string, 'readOnly' => :boolean, 'writeOnly' => :boolean, 'deprecated' => :boolean, 'examples' => :array }.freeze
- KEYWORD_SHAPES =
Every keyword whose value shape the preflight reads.
ASSERTION_SHAPES.merge(ANNOTATION_SHAPES).freeze
- JSON_TYPE_NAMES =
The JSON Schema type names (2020-12 Validation Section 6.1.1).
%w[array boolean integer null number object string].freeze
- VALUE_SHAPES =
Each simple assertion shape: the predicate that admits a value, and how the requirement reads in a problem.
{ array: [:array_value?, 'an array'], property_names: [:property_names?, 'an array of distinct property names'], string: [:string_value?, 'a string'], non_negative_integer: [:non_negative_integer?, 'a non-negative integer'], number: [:number_value?, 'a number'], positive_number: [:positive_number?, 'a number greater than zero'], boolean: [:boolean_value?, 'a boolean'] }.freeze
Instance Method Summary collapse
-
#absolute_uri?(value) ⇒ Boolean
Whether the value is a URI with a scheme.
-
#all_schemas?(values) ⇒ Boolean
Whether values is an array of schemas.
-
#applicator_shape_problem(keyword, value) ⇒ String?
Why an applicator value is malformed.
- #array_value?(value) ⇒ Boolean
-
#assertion_shape_problem(keyword, value) ⇒ String?
Why an assertion or annotation value is malformed.
- #boolean_value?(value) ⇒ Boolean
-
#check_applicator_shapes(schema, dialect, problems) ⇒ void
Every applicator value must be a schema (object or boolean), an array of schemas or a map of schemas; anything else is not silently read as "true".
-
#check_assertion_shapes(schema, dialect, problems) ⇒ void
Check the assertion keyword values a schema object carries.
-
#check_core_keyword_shapes(schema, dialect, problems) ⇒ void
The core keywords with a fixed shape beyond the identifiers:
$vocabularyis an object mapping vocabulary URIs to booleans (Core Section 8.1.2) and 2019-09's$recursiveAnchoris a boolean (2019-09 Core Section 8.2.4.2.2). -
#check_exclusive_bounds(schema, _dialect, problems) ⇒ void
exclusiveMinimum / exclusiveMaximum are numbers in every supported dialect (draft-07 validation Sections 6.2.3 and 6.2.5, kept by 2019-09 and 2020-12); the boolean modifier form belongs to draft-04, which is not supported, and is not silently ignored (it would turn a bound into a pass).
-
#check_identifier_shapes(schema, dialect, problems) ⇒ void
An identifier must have the shape its dialect defines:
$idis a URI reference without a non-empty fragment (JSON Schema 2020-12 Core Section 8.2.1) — draft-07 additionally spells a plain-name identifier as a bare fragment (Core Section 8.2.3) — and$anchor/$dynamicAnchorhold a plain name (Core Section 8.2.2). -
#check_pattern_shapes(schema, dialect, problems, deadline = nil) ⇒ void
A
pattern, and everypatternPropertieskey, must be an ECMA-262 regular expression the validator can read (JSON Schema 2020-12 Core Section 4.3) within the length bound. -
#dependencies_shape_problem(value) ⇒ String?
draft-07: each dependencies entry is a schema or an array of property names.
-
#dependent_required_shape_problem(keyword, value) ⇒ String?
dependentRequiredmaps a property name to the names it requires (JSON Schema 2020-12 Validation Section 6.5.4). -
#id_shape_problem(id, dialect) ⇒ String?
Why an
$idis malformed. - #non_negative_integer?(value) ⇒ Boolean
- #number_value?(value) ⇒ Boolean
-
#pattern_shape_problem(keyword, pattern, deadline = nil) ⇒ String?
Why a pattern cannot be used.
- #positive_number?(value) ⇒ Boolean
-
#property_names?(value) ⇒ Boolean
JSON Schema 2020-12 Validation Sections 6.5.3 and 6.5.4: the elements of
required(and of adependentRequiredentry) are strings, and they MUST be unique — a name written twice is a malformed keyword, not the same assertion made again. -
#shape_requirement(shape, value) ⇒ String?
What a malformed value should have been.
- #string_value?(value) ⇒ Boolean
-
#type_shape_problem(value) ⇒ String?
Why a
typevalue is malformed. -
#uri_reference?(value) ⇒ Boolean
Whether a string parses as an RFC 3986 URI reference.
-
#vocabulary_map?(value) ⇒ Boolean
A vocabulary is identified by a URI (Core Section 8.1.2), never by a relative reference.
Instance Method Details
#absolute_uri?(value) ⇒ Boolean
Returns whether the value is a URI with a scheme.
254 255 256 257 258 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 254 def absolute_uri?(value) !URI::RFC3986_PARSER.parse(value).scheme.nil? rescue URI::InvalidURIError false end |
#all_schemas?(values) ⇒ Boolean
Returns whether values is an array of schemas.
314 315 316 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 314 def all_schemas?(values) values.is_a?(Array) && values.all? { |v| schema_value?(v) } end |
#applicator_shape_problem(keyword, value) ⇒ String?
Returns why an applicator value is malformed.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 27 def applicator_shape_problem(keyword, value) if SUBSCHEMA_KEYWORDS.include?(keyword) tuple = keyword == 'items' && all_schemas?(value) "#{keyword} must be a schema" unless schema_value?(value) || tuple elsif keyword == 'dependencies' dependencies_shape_problem(value) elsif SUBSCHEMA_MAP_KEYWORDS.include?(keyword) # A definition bag holds reusable schemas, whatever a reference ends # up pointing at (JSON Schema 2020-12 Core Section 8.2.4). "#{keyword} must be an object of schemas" unless value.is_a?(Hash) && all_schemas?(value.values) elsif SUBSCHEMA_ARRAY_KEYWORDS.include?(keyword) # allOf / anyOf / oneOf / prefixItems: "MUST be a non-empty array". "#{keyword} must be a non-empty array of schemas" unless all_schemas?(value) && !value.empty? end end |
#array_value?(value) ⇒ Boolean
119 120 121 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 119 def array_value?(value) value.is_a?(Array) end |
#assertion_shape_problem(keyword, value) ⇒ String?
Returns why an assertion or annotation value is malformed.
90 91 92 93 94 95 96 97 98 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 90 def assertion_shape_problem(keyword, value) shape = KEYWORD_SHAPES[keyword] return nil unless shape return type_shape_problem(value) if shape == :type_names return dependent_required_shape_problem(keyword, value) if shape == :dependent_required requirement = shape_requirement(shape, value) "#{keyword} must be #{requirement}" if requirement end |
#boolean_value?(value) ⇒ Boolean
144 145 146 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 144 def boolean_value?(value) [true, false].include?(value) end |
#check_applicator_shapes(schema, dialect, problems) ⇒ void
This method returns an undefined value.
Every applicator value must be a schema (object or boolean), an array of schemas or a map of schemas; anything else is not silently read as "true". Keywords the dialect does not define are ignored.
17 18 19 20 21 22 23 24 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 17 def check_applicator_shapes(schema, dialect, problems) schema.each do |keyword, value| next unless keyword_known?(keyword, dialect) problem = applicator_shape_problem(keyword, value) problems << problem if problem end end |
#check_assertion_shapes(schema, dialect, problems) ⇒ void
This method returns an undefined value.
Check the assertion keyword values a schema object carries. A keyword
the dialect does not define (minContains under draft-07) is an
unknown one there: ignored, never malformed.
80 81 82 83 84 85 86 87 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 80 def check_assertion_shapes(schema, dialect, problems) schema.each do |keyword, value| next unless keyword_known?(keyword, dialect) problem = assertion_shape_problem(keyword, value) problems << problem if problem end end |
#check_core_keyword_shapes(schema, dialect, problems) ⇒ void
This method returns an undefined value.
The core keywords with a fixed shape beyond the identifiers:
$vocabulary is an object mapping vocabulary URIs to booleans (Core
Section 8.1.2) and 2019-09's $recursiveAnchor is a boolean (2019-09
Core Section 8.2.4.2.2). A keyword the dialect does not define is
unknown there, never malformed.
236 237 238 239 240 241 242 243 244 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 236 def check_core_keyword_shapes(schema, dialect, problems) if schema.key?('$vocabulary') && keyword_known?('$vocabulary', dialect) && !vocabulary_map?(schema['$vocabulary']) problems << '$vocabulary must be an object of booleans keyed by URI' end return unless schema.key?('$recursiveAnchor') && keyword_known?('$recursiveAnchor', dialect) problems << '$recursiveAnchor must be a boolean' unless boolean_value?(schema['$recursiveAnchor']) end |
#check_exclusive_bounds(schema, _dialect, problems) ⇒ void
This method returns an undefined value.
exclusiveMinimum / exclusiveMaximum are numbers in every supported dialect (draft-07 validation Sections 6.2.3 and 6.2.5, kept by 2019-09 and 2020-12); the boolean modifier form belongs to draft-04, which is not supported, and is not silently ignored (it would turn a bound into a pass).
304 305 306 307 308 309 310 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 304 def check_exclusive_bounds(schema, _dialect, problems) %w[exclusiveMinimum exclusiveMaximum].each do |keyword| next if !schema.key?(keyword) || schema[keyword].is_a?(Numeric) problems << "#{keyword} must be a number (the draft-04 boolean form is not supported)" end end |
#check_identifier_shapes(schema, dialect, problems) ⇒ void
This method returns an undefined value.
An identifier must have the shape its dialect defines: $id is a URI
reference without a non-empty fragment (JSON Schema 2020-12 Core
Section 8.2.1) — draft-07 additionally spells a plain-name identifier
as a bare fragment (Core Section 8.2.3) — and $anchor /
$dynamicAnchor hold a plain name (Core Section 8.2.2). An
identifier the validator cannot read names nothing, so a reference
written to it would silently resolve elsewhere.
195 196 197 198 199 200 201 202 203 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 195 def check_identifier_shapes(schema, dialect, problems) id_problem = id_shape_problem(schema['$id'], dialect) if schema.key?('$id') problems << id_problem if id_problem %w[$anchor $dynamicAnchor].each do |keyword| next unless schema.key?(keyword) && keyword_known?(keyword, dialect) problems << "#{keyword} must be a plain name" unless anchor_name?(schema[keyword], dialect) end end |
#check_pattern_shapes(schema, dialect, problems, deadline = nil) ⇒ void
This method returns an undefined value.
A pattern, and every patternProperties key, must be an ECMA-262
regular expression the validator can read (JSON Schema 2020-12 Core
Section 4.3) within the length bound. One that is not is a malformed
keyword: the schema is unusable, not a schema without the pattern
(which admitted every string).
267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 267 def check_pattern_shapes(schema, dialect, problems, deadline = nil) pattern = schema['pattern'] problem = pattern_shape_problem('pattern', pattern, deadline) if pattern.is_a?(String) problems << problem if problem patterns = schema['patternProperties'] if keyword_known?('patternProperties', dialect) return unless patterns.is_a?(Hash) patterns.each_key do |key| break unless problems.empty? problem = pattern_shape_problem('patternProperties pattern', key.to_s, deadline) problems << problem if problem end end |
#dependencies_shape_problem(value) ⇒ String?
draft-07: each dependencies entry is a schema or an array of property names.
171 172 173 174 175 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 171 def dependencies_shape_problem(value) return if value.is_a?(Hash) && value.each_value.all? { |v| schema_value?(v) || property_names?(v) } 'dependencies entries must be schemas or arrays of property names' end |
#dependent_required_shape_problem(keyword, value) ⇒ String?
dependentRequired maps a property name to the names it requires
(JSON Schema 2020-12 Validation Section 6.5.4).
151 152 153 154 155 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 151 def dependent_required_shape_problem(keyword, value) return if value.is_a?(Hash) && value.each_value.all? { |v| property_names?(v) } "#{keyword} must be an object of arrays of distinct property names" end |
#id_shape_problem(id, dialect) ⇒ String?
Returns why an $id is malformed.
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 206 def id_shape_problem(id, dialect) return '$id must be a non-empty string' unless id.is_a?(String) && !id.empty? # JSON Schema 2020-12 Core Section 8.2.1: the value is a URI # reference. One that is not (a space in it, say) names no resource, # and a reference written to it would resolve elsewhere or nowhere. return "$id #{clip(id.inspect)} must be a URI reference" unless uri_reference?(id) # draft-07 Core Section 8.2.3: an $id that is exactly a fragment # declares a plain name rather than a base URI. return nil if dialect == DRAFT_07 && id.start_with?('#') fragment = id.split('#', 2)[1] return nil if fragment.nil? || fragment.empty? "$id #{clip(id.inspect)} must not contain a non-empty fragment" end |
#non_negative_integer?(value) ⇒ Boolean
139 140 141 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 139 def non_negative_integer?(value) integer?(value) && !value.negative? end |
#number_value?(value) ⇒ Boolean
129 130 131 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 129 def number_value?(value) value.is_a?(Numeric) end |
#pattern_shape_problem(keyword, pattern, deadline = nil) ⇒ String?
Returns why a pattern cannot be used.
283 284 285 286 287 288 289 290 291 292 293 294 295 296 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 283 def pattern_shape_problem(keyword, pattern, deadline = nil) return "#{keyword} is longer than #{MAX_PATTERN_LENGTH} characters" if pattern.length > MAX_PATTERN_LENGTH return 'validation aborted: validation time budget exhausted during the schema check' if budget_exhausted?(deadline) ecma_regexp(pattern, PATTERN_MATCH_TIMEOUT, deadline) nil rescue EcmaPatterns::Untranslatable => e "#{keyword} #{clip(pattern.inspect)} cannot be evaluated faithfully (#{clip(e.)})" rescue RegexpError => e "#{keyword} #{clip(pattern.inspect)} is not an ECMA-262 regular expression (#{clip(e.)})" rescue Aborted => e "validation aborted: #{e.}" end |
#positive_number?(value) ⇒ Boolean
134 135 136 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 134 def positive_number?(value) value.is_a?(Numeric) && value.positive? end |
#property_names?(value) ⇒ Boolean
JSON Schema 2020-12 Validation Sections 6.5.3 and 6.5.4: the elements
of required (and of a dependentRequired entry) are strings, and
they MUST be unique — a name written twice is a malformed keyword,
not the same assertion made again.
183 184 185 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 183 def property_names?(value) value.is_a?(Array) && value.all?(String) && value.uniq.size == value.size end |
#shape_requirement(shape, value) ⇒ String?
Returns what a malformed value should have been.
113 114 115 116 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 113 def shape_requirement(shape, value) predicate, requirement = VALUE_SHAPES[shape] requirement unless predicate.nil? || send(predicate, value) end |
#string_value?(value) ⇒ Boolean
124 125 126 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 124 def string_value?(value) value.is_a?(String) end |
#type_shape_problem(value) ⇒ String?
Returns why a type value is malformed.
158 159 160 161 162 163 164 165 166 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 158 def type_shape_problem(value) names = value.is_a?(Array) ? value : [value] known = !names.empty? && names.all? do |name| (name.is_a?(String) || name.is_a?(Symbol)) && JSON_TYPE_NAMES.include?(name.to_s) end return nil if known && (!value.is_a?(Array) || names.uniq.size == names.size) "type must be one of #{JSON_TYPE_NAMES.join(', ')}, or a non-empty array of distinct such names" end |
#uri_reference?(value) ⇒ Boolean
Returns whether a string parses as an RFC 3986 URI reference.
223 224 225 226 227 228 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 223 def uri_reference?(value) URI::RFC3986_PARSER.parse(value) true rescue URI::InvalidURIError false end |
#vocabulary_map?(value) ⇒ Boolean
A vocabulary is identified by a URI (Core Section 8.1.2), never by a relative reference.
249 250 251 |
# File 'lib/mcp_client/schema_validator/shapes.rb', line 249 def vocabulary_map?(value) value.is_a?(Hash) && value.all? { |uri, required| absolute_uri?(uri.to_s) && boolean_value?(required) } end |