Module: MCPClient::SchemaValidator::Normalization

Included in:
MCPClient::SchemaValidator
Defined in:
lib/mcp_client/schema_validator/normalization.rb

Overview

The bounded, string-keyed copy of a peer-supplied schema document. Extended into SchemaValidator, so the methods are its own.

Instance Method Summary collapse

Instance Method Details

#charge_structure(budget, count) ⇒ Object

Account for structural elements about to be copied.

Raises:

  • (TooLarge) —

    when the budget is exceeded

  • (Aborted) —

    when the deadline passed



88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/mcp_client/schema_validator/normalization.rb', line 88

def charge_structure(budget, count)
  return unless budget

  budget[:objects] += count
  if budget[:objects] > MAX_STRUCTURAL_OBJECTS
    raise TooLarge,
          "schema has more than #{MAX_STRUCTURAL_OBJECTS} structural elements"
  end

  deadline = budget[:deadline]
  return unless deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline

  raise Aborted, 'time budget exhausted while reading the schema'
end

#deep_stringify(node, depth = 0, budget = nil, mode = :schema) ⇒ Object

A copy of the schema with every structural Hash key as a String, so keyword lookups and JSON pointers see one key form. The value of a data-carrying keyword (enum, const, default, examples) of a schema object is left exactly as given, the walk stops at the nesting bound, and — given a budget — the copy stops as soon as the document holds more structural elements (objects, their entries and array members, boolean subschemas included) than MAX_STRUCTURAL_OBJECTS, before the rest is ever read, or as soon as the validation deadline has passed.

Parameters:

  • node (Object)
  • depth (Integer) (defaults to: 0)
  • budget (Hash, nil) (defaults to: nil) —

    :objects copied so far, :deadline (optional)

  • mode (Symbol) (defaults to: :schema) —

    how this position is read (#member_mode)

Returns:

  • (Object)

Raises:

  • (TooLarge) —

    when the budget is exceeded, or the document is nested beyond what MAX_SCHEMA_DEPTH schema levels can hold (a subtree is never kept unread)

  • (Aborted) —

    when the deadline passed



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/mcp_client/schema_validator/normalization.rb', line 25

def deep_stringify(node, depth = 0, budget = nil, mode = :schema)
  # Each schema level takes at most two document levels (a keyword map
  # or array, then the subschema), so a deeper document holds more
  # schema levels than the preflight walk admits.
  raise TooLarge, "schema nesting depth exceeds #{MAX_SCHEMA_DEPTH}" if depth > (MAX_SCHEMA_DEPTH * 2) + 2

  case node
  when Hash
    charge_structure(budget, node.size + 1)
    node.to_h do |key, value|
      name = key.to_s
      [name, stringify_member(name, value, depth, budget, mode)]
    end
  when Array
    charge_structure(budget, node.length + 1)
    node.map.with_index do |value, idx|
      deep_stringify(value, depth + 1, budget, member_mode(idx.to_s, value, mode))
    end
  else node
  end
end

#list_member?(name, value) ⇒ Boolean

Returns whether a keyword holds a map or array of subschemas in the form it was written (draft-07 / 2019-09 read an items array positionally).

Returns:

  • (Boolean) —

    whether a keyword holds a map or array of subschemas in the form it was written (draft-07 / 2019-09 read an items array positionally)



80
81
82
83
# File 'lib/mcp_client/schema_validator/normalization.rb', line 80

def list_member?(name, value)
  (SUBSCHEMA_MAP_KEYWORDS.include?(name) && value.is_a?(Hash)) ||
    ((SUBSCHEMA_ARRAY_KEYWORDS.include?(name) || name == 'items') && value.is_a?(Array))
end

#member_mode(name, value, mode) ⇒ Symbol

How the member a name selects is read: :data (a data keyword of a schema object, kept as given), :list (a map or array of subschemas), :schema, or :plain (anything else — copied, never read as a schema). Only a schema object has keywords: a property, definition or pattern named enum, const, default or examples is a schema position like any other (JSON Schema 2020-12 Core Section 4.3.1), and so is everything under a keyword no dialect defines.

Returns:

  • (Symbol)


67
68
69
70
71
72
73
74
75
# File 'lib/mcp_client/schema_validator/normalization.rb', line 67

def member_mode(name, value, mode)
  return :data if mode == :data
  return :schema if mode == :list
  return :plain unless mode == :schema
  return :data if DATA_KEYWORDS.include?(name)
  return :list if list_member?(name, value)

  SUBSCHEMA_KEYWORDS.include?(name) ? :schema : :plain
end

#stringify_member(name, value, depth, budget, mode) ⇒ Object

The copy of one member: what a schema object holds under a data keyword is kept as given (its key form is the instance's, and it is compared for equality as written), everything else is copied in the mode its position implies.

Returns:

  • (Object)


52
53
54
55
56
57
# File 'lib/mcp_client/schema_validator/normalization.rb', line 52

def stringify_member(name, value, depth, budget, mode)
  member = member_mode(name, value, mode)
  return value if member == :data

  deep_stringify(value, depth + 1, budget, member)
end