Class: Aspera::Schema::Reader

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/schema/reader.rb

Overview

JSON schema reader

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root, current = nil) ⇒ Hash?

Read schema from file or from cache

Parameters:

  • root (Hash) —

    root schema

  • current (Hash, nil) (defaults to: nil) —

    current position in



67
68
69
70
# File 'lib/aspera/schema/reader.rb', line 67

def initialize(root, current = nil)
  @root = root
  @current = current || root
end

Instance Attribute Details

#current ⇒ Object (readonly)

Returns the value of attribute current.



8
9
10
# File 'lib/aspera/schema/reader.rb', line 8

def current
  @current
end

Class Method Details

.from_query_params(params) ⇒ Reader

Build a synthetic Reader from an OAS parameters array (entries with in: query). Produces a JSON Schema object whose properties map each query param name to its schema, with the OAS-level description and required merged in.

Parameters:

  • params (Array<Hash>) —

    raw OAS parameter objects (may contain path/header params too)

Returns:



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/aspera/schema/reader.rb', line 16

def from_query_params(params)
  properties = {}
  required_names = []
  params.each do |param|
    next unless param['in'] == 'query'
    name = param['name']
    prop = (param['schema'] || {}).dup
    prop['description'] = param['description'] if param['description'] && !prop.key?('description')
    properties[name] = prop
    required_names << name if param['required']
  end
  synthetic = {'type' => 'object', 'properties' => properties}
  synthetic['required'] = required_names unless required_names.empty?
  new(synthetic)
end

Instance Method Details

#[](key) ⇒ Hash, ...

Shortcut to access current value at path

Parameters:

  • key (String) —

    path element

Returns:

  • (Hash, Array, String, Integer) —

    current value at path



36
37
38
# File 'lib/aspera/schema/reader.rb', line 36

def [](key)
  @current[key]
end

#dig(*path) ⇒ Object

Find sub path relative to current Honors $ref



42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/aspera/schema/reader.rb', line 42

def dig(*path)
  current = @current
  path.each do |p|
    Aspera.assert(current.key?(p)) { "schema: #{p} in #{path}" }
    current = current[p]
    Aspera.assert_type(current, Hash) { 'schema' }
    if current.key?('$ref')
      ref = current['$ref']
      Aspera.assert(ref.start_with?('#/')) { "schema $ref must start with '#/': #{ref}" }
      current = @root.dig(*ref[2..].split('/'))
    end
  end
  Reader.new(@root, current)
end

#each_property(prefix = '', on_variant: nil) {|property_schema, name, full_name| ... } ⇒ nil

Recursively traverse schema properties with a block. If the current node has oneOf, each variant is traversed in turn and on_variant is called (if given) before each variant's properties.

Parameters:

  • prefix (String) (defaults to: '') —

    Prefix for property names (e.g., 'parent.child.')

  • on_variant (Proc, nil) (defaults to: nil) —

    Called with the variant Reader before its properties

Yields:

  • (property_schema, name, full_name) —

    Yields property info to block

Yield Parameters:

  • property_schema (Reader) —

    Schema reader for this property

  • name (String) —

    Property name

  • full_name (String) —

    Full property name with prefix

Returns:

  • (nil)


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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/aspera/schema/reader.rb', line 82

def each_property(prefix = '', on_variant: nil, &block)
  if @current.key?('oneOf')
    # Build reverse map: $ref -> discriminant value, from discriminator.mapping if present
    discriminant_by_ref = {}
    if @current.dig('discriminator', 'mapping').is_a?(Hash)
      @current['discriminator']['mapping'].each do |value, ref|
        discriminant_by_ref[ref] = value
      end
    end
    discriminant_property = @current.dig('discriminator', 'propertyName')
    @current['oneOf'].each do |variant_node|
      ref = variant_node['$ref']
      variant_reader = ref ? resolve_ref(ref) : Reader.new(@root, variant_node)
      discriminant_value = ref ? discriminant_by_ref[ref] : nil
      on_variant&.call(variant_reader, discriminant_property, discriminant_value)
      variant_reader.each_property(prefix, on_variant: on_variant, &block)
    end
    return
  end
  if @current.key?('allOf')
    # Merge all branches: each branch contributes its properties (no variants)
    @current['allOf'].each do |branch_node|
      ref = branch_node['$ref']
      branch_reader = ref ? resolve_ref(ref) : Reader.new(@root, branch_node)
      branch_reader.each_property(prefix, on_variant: on_variant, &block)
    end
    return
  end
  properties = dig('properties')
  properties.current.each_key do |name|
    property_full_name = "#{prefix}#{name}"
    property_schema = properties.dig(name)
    node = property_schema.current

    # Yield current property to block
    yield(property_schema, name, property_full_name)

    # Recursively process nested structures
    case node['type']
    when 'object'
      property_schema.each_property("#{property_full_name}.", on_variant: on_variant, &block) if node['properties']
    when 'array'
      if node['items']
        array_item_schema = property_schema.dig('items')
        array_item_schema.each_property("#{property_full_name}[].", on_variant: on_variant, &block) if array_item_schema.current['properties']
      end
    end
    # allOf without explicit type: object — recurse to merge all branches
    property_schema.each_property("#{property_full_name}.", on_variant: on_variant, &block) if node['allOf']
  end
end

#resolve_ref(ref) ⇒ Object

Resolve a $ref string to a Reader



58
59
60
61
# File 'lib/aspera/schema/reader.rb', line 58

def resolve_ref(ref)
  Aspera.assert(ref.start_with?('#/')) { "schema $ref must start with '#/': #{ref}" }
  Reader.new(@root, @root.dig(*ref[2..].split('/')))
end

#to_rows ⇒ Array<Hash>

Convert this schema to a flat array of field descriptors. Returns raw semantic fields with no ANSI or formatting, suitable for JSON/YAML output or MCP consumption. An AI or script can use the result directly to build a valid payload.

Each Hash entry contains:

name        [String]  dot/bracket-path of the field (e.g. "recipients[].name")
type        [String]  JSON type string (e.g. "string", "boolean", "Array[object]")
required    [Boolean] true when the field is in its immediate parent's required list
description [String]  human description (may contain Markdown **bold** / `code`)
default     [Object]  (optional) default value as native Ruby type
enum        [Array]   (optional) list of allowed string values

Returns:



148
149
150
151
152
# File 'lib/aspera/schema/reader.rb', line 148

def to_rows
  rows = []
  collect_rows(rows, self, '')
  rows
end