Module: MCPClient::SchemaValidator::KeywordScan
- Included in:
- MCPClient::SchemaValidator
- Defined in:
- lib/mcp_client/schema_validator/keyword_scan.rb
Overview
The bounded scan for keywords the validator does not evaluate, so the client can say when its validation is only partial. Extended into SchemaValidator, so the methods are its own.
Instance Method Summary collapse
-
#queue_scan(scan, schema, depth, dialect, referenced, walker) ⇒ void
Queue the positions under a schema object (in document order, the stack being read from its end), then what its references reach.
-
#referenced_position(schema, root, depth, scan, dialect, keyword = '$ref') ⇒ Array?
The position what a local reference applies is scanned at (unresolvable or external references are the preflight's business).
-
#scan_position(schema, root, found, depth, scan, dialect = ) ⇒ void
Collect the unsupported keywords one schema position uses (keywords the dialect does not define are unknown, not unsupported) and queue what it leads to.
-
#scan_positions(root, found, scan) ⇒ void
Read every queued position.
-
#unsupported_keywords(schema) ⇒ Array<String>
List the unsupported JSON Schema keywords a schema uses (anywhere: at the top level or nested in subschemas).
Instance Method Details
#queue_scan(scan, schema, depth, dialect, referenced, walker) ⇒ void
This method returns an undefined value.
Queue the positions under a schema object (in document order, the stack being read from its end), then what its references reach.
85 86 87 88 89 90 |
# File 'lib/mcp_client/schema_validator/keyword_scan.rb', line 85 def queue_scan(scan, schema, depth, dialect, referenced, walker) positions = [] send(walker, schema, dialect) { |sub| positions << [sub, depth + 1, dialect] } scan[:pending].concat(positions.reverse) scan[:pending].concat(referenced.reverse) end |
#referenced_position(schema, root, depth, scan, dialect, keyword = '$ref') ⇒ Array?
The position what a local reference applies is scanned at (unresolvable or external references are the preflight's business).
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/mcp_client/schema_validator/keyword_scan.rb', line 96 def referenced_position(schema, root, depth, scan, dialect, keyword = '$ref') ref = schema[keyword] return nil unless ref.is_a?(String) return nil if external_ref?(ref, root, scan[:dialect], scan, from: schema) target = resolve_reference(root, ref, scan[:dialect], scan, from: schema) return nil if target.equal?(UNRESOLVED) # What a reference reaches is scanned under its own resource's dialect # and at its own lexical depth, so member order and reference fan-out # cannot hide its keywords. A target that is not a schema object has # no lexical depth at all — nil, never `false`, so the depth the # opaque-pointer branch compares stays a number. scan[:depths] ||= lexical_depths(root, scan[:dialect]) lexical = scan[:depths][target] if target.is_a?(Hash) position, opaque = pointer_position(ref, root, scan[:dialect], scan, schema) target_depth = if opaque [position, lexical].compact.max || (depth + 1) else lexical || position || (depth + 1) end [target, target_depth, (target.is_a?(Hash) && indexed_dialect(target, scan)) || dialect] end |
#scan_position(schema, root, found, depth, scan, dialect = ) ⇒ void
This method returns an undefined value.
Collect the unsupported keywords one schema position uses (keywords
the dialect does not define are unknown, not unsupported) and queue
what it leads to. What a local $ref applies is scanned too, wherever
it lives (a definition bag the dialect does not walk included); each
object is scanned once.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/mcp_client/schema_validator/keyword_scan.rb', line 57 def scan_position(schema, root, found, depth, scan, dialect = scan[:dialect]) return unless schema.is_a?(Hash) && depth <= MAX_SCHEMA_DEPTH return if scan[:walked].key?(schema) scan[:walked][schema] = true scan[:count] += 1 return if scan[:count] > MAX_SUBSCHEMAS # An embedded resource declaring its own $schema is scanned under it. dialect = (schema, dialect) || dialect if dialect == DRAFT_07 && schema.key?('$ref') # Nothing beside the $ref is applied; the definitions stay reachable. referenced = [referenced_position(schema, root, depth, scan, dialect)].compact return queue_scan(scan, schema, depth, dialect, referenced, :each_definition) end # A dynamic reference is applied like a `$ref` — where it binds is # decided during evaluation — so it is scanned like one. dynamic = DYNAMIC_REFERENCE_KEYWORDS.select { |k| schema.key?(k) && keyword_known?(k, dialect) } found.concat((schema.keys & UNSUPPORTED_KEYWORDS).select { |k| keyword_known?(k, dialect) }) referenced = (['$ref'] + dynamic).filter_map { |k| referenced_position(schema, root, depth, scan, dialect, k) } queue_scan(scan, schema, depth, dialect, referenced, :each_subschema) end |
#scan_positions(root, found, scan) ⇒ void
This method returns an undefined value.
Read every queued position. Like the preflight walk, the scan runs on
a stack of its own rather than on the interpreter's: a shallow
document whose $refs chain through hundreds of schemas is walked in
constant stack space, so scanning one cannot overflow the thread a
transport reads on.
38 39 40 41 42 43 |
# File 'lib/mcp_client/schema_validator/keyword_scan.rb', line 38 def scan_positions(root, found, scan) until scan[:pending].empty? schema, depth, dialect = scan[:pending].pop scan_position(schema, root, found, depth, scan, dialect) end end |
#unsupported_keywords(schema) ⇒ Array<String>
List the unsupported JSON Schema keywords a schema uses (anywhere: at the top level or nested in subschemas). Property names that merely look like keywords (e.g. a property called 'not') are not reported, and data-carrying keywords (enum/const/default/examples) are not scanned. The scan stops at the same bounds as MCPClient::SchemaValidator.check_schema (a schema beyond them is unusable anyway), so a huge server-supplied schema is never walked whole.
18 19 20 21 22 23 24 25 26 27 28 29 30 |
# File 'lib/mcp_client/schema_validator/keyword_scan.rb', line 18 def unsupported_keywords(schema) found = [] root = normalize_schema(schema) declared = dialect(root) canonical = declared && canonical_dialect(declared) scan = { count: 0, dialect: canonical, walked: {}.compare_by_identity, anchors: nil, pending: [[root, 0, canonical]] } scan_positions(root, found, scan) found.uniq rescue TooLarge # Unusable anyway: check_schema reports it. found.uniq end |