Module: MCPClient::SchemaValidator::Scalars
- Included in:
- MCPClient::SchemaValidator
- Defined in:
- lib/mcp_client/schema_validator/scalars.rb
Overview
The string and number keywords, and the budget a peer-supplied pattern is matched under. Both a pattern and the values it runs against come from the remote peer, so matching is bounded by the validation-wide deadline. Extended into SchemaValidator, so the methods are its own.
Instance Method Summary collapse
-
#multiple_of?(data, factor) ⇒ Boolean
Whether dividing the value by the factor gives an integer (JSON Schema 2020-12 Validation Section 6.2.1).
-
#pattern_budget_remaining(deadline) ⇒ Float
Time left in the validation-wide budget.
-
#validate_number(data, schema, path, _dialect = nil) ⇒ Array<String>
Validate a number against its bounds.
-
#validate_pattern(data, pattern, path, deadline = nil) ⇒ Array<String>
Validate a string against a regular-expression pattern.
-
#validate_string(data, schema, path, deadline = nil) ⇒ Array<String>
Validate a string against minLength/maxLength/pattern.
Instance Method Details
#multiple_of?(data, factor) ⇒ Boolean
Whether dividing the value by the factor gives an integer (JSON Schema 2020-12 Validation Section 6.2.1). The division is exact: 0.0075 is a multiple of 0.0001, which binary floating point says it is not, so the decimal each number was written as decides.
115 116 117 118 119 120 121 122 123 |
# File 'lib/mcp_client/schema_validator/scalars.rb', line 115 def multiple_of?(data, factor) return (data % factor).zero? if data.is_a?(Integer) && factor.is_a?(Integer) (Rational(data.to_s) / Rational(factor.to_s)).denominator == 1 rescue ArgumentError, ZeroDivisionError, FloatDomainError, TypeError # A value no decimal describes (an infinity a Ruby caller passed in; # JSON carries none) falls back to the floating-point remainder. (data % factor).zero? end |
#pattern_budget_remaining(deadline) ⇒ Float
Time left in the validation-wide budget.
65 66 67 68 69 70 71 72 |
# File 'lib/mcp_client/schema_validator/scalars.rb', line 65 def pattern_budget_remaining(deadline) return PATTERN_MATCH_TIMEOUT unless deadline remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) return 0.0 if remaining <= 0 [remaining, MIN_PATTERN_MATCH_TIMEOUT].max end |
#validate_number(data, schema, path, _dialect = nil) ⇒ Array<String>
Validate a number against its bounds. minimum / maximum and
exclusiveMinimum / exclusiveMaximum are four independent numeric
assertions in every supported dialect (draft-07 validation Sections
6.2.2-6.2.5); each present one is applied.
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 |
# File 'lib/mcp_client/schema_validator/scalars.rb', line 82 def validate_number(data, schema, path, _dialect = nil) errors = [] minimum = schema['minimum'] maximum = schema['maximum'] exclusive_min = schema['exclusiveMinimum'] exclusive_max = schema['exclusiveMaximum'] shown = clip_value(data) if minimum.is_a?(Numeric) && data < minimum errors << "#{path}: value #{shown} is less than minimum #{clip_value(minimum)}" end if maximum.is_a?(Numeric) && data > maximum errors << "#{path}: value #{shown} is greater than maximum #{clip_value(maximum)}" end if exclusive_min.is_a?(Numeric) && data <= exclusive_min errors << "#{path}: value #{shown} must be greater than exclusiveMinimum #{clip_value(exclusive_min)}" end if exclusive_max.is_a?(Numeric) && data >= exclusive_max errors << "#{path}: value #{shown} must be less than exclusiveMaximum #{clip_value(exclusive_max)}" end factor = schema['multipleOf'] if factor.is_a?(Numeric) && factor.positive? && !multiple_of?(data, factor) errors << "#{path}: value #{shown} is not a multiple of #{clip_value(factor)}" end errors end |
#validate_pattern(data, pattern, path, deadline = nil) ⇒ Array<String>
Validate a string against a regular-expression pattern. A pattern that is no ECMA-262 expression is a malformed keyword the preflight refuses; should one reach here it is an error, never a pass.
The pattern comes from the tool's outputSchema, i.e. from the remote server, so matching runs against the validation-wide deadline: neither a single expensive expression nor many cheap-looking ones can pin the calling thread. A match that exceeds the budget aborts the validation rather than silently accepting the value — the value was never shown to satisfy the schema.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/mcp_client/schema_validator/scalars.rb', line 45 def validate_pattern(data, pattern, path, deadline = nil) return [] unless pattern.is_a?(String) remaining = pattern_budget_remaining(deadline) raise Aborted, "validation time budget exhausted before pattern #{clip(pattern.inspect)}" if remaining.zero? return [] if data.match?(ecma_regexp(pattern, remaining, deadline)) ["#{path}: string does not match pattern #{clip(pattern.inspect)}"] rescue Regexp::TimeoutError raise Aborted, "pattern #{clip(pattern.inspect)} exceeded the #{PATTERN_MATCH_TIMEOUT}s matching budget" rescue EcmaPatterns::Untranslatable => e ["#{path}: pattern #{clip(pattern.inspect)} cannot be evaluated faithfully (#{clip(e.message)})"] rescue RegexpError => e ["#{path}: pattern #{clip(pattern.inspect)} is not an ECMA-262 regular expression (#{clip(e.message)})"] end |
#validate_string(data, schema, path, deadline = nil) ⇒ Array<String>
Validate a string against minLength/maxLength/pattern.
15 16 17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/mcp_client/schema_validator/scalars.rb', line 15 def validate_string(data, schema, path, deadline = nil) errors = [] min_length = schema['minLength'] max_length = schema['maxLength'] if min_length.is_a?(Numeric) && data.length < min_length errors << "#{path}: string is shorter than minLength #{min_length}" end if max_length.is_a?(Numeric) && data.length > max_length errors << "#{path}: string is longer than maxLength #{max_length}" end errors.concat(validate_pattern(data, schema['pattern'], path, deadline)) errors end |