Class: Aspera::Schema::Documentation

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

Overview

Generate documentation from Schema, for Transfer Spec, or async Conf spec

Constant Summary collapse

JSON_TYPE_TO_DOC =

Map JSON Schema type names to user-friendly display names

{
  'string'  => 'String',
  'integer' => 'Integer',
  'number'  => 'Number',
  'boolean' => 'Bool',
  'array'   => 'Array',
  'object'  => 'Hash'
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(formatter, schema, include_option: false, agent_columns: false, code_highlight: false) ⇒ Documentation

Returns a new instance of Documentation.

Parameters:

  • formatter (Cli::Formatter) —

    Formatter instance with methods: markdown_text, tick, check_row

  • schema (Reader)
  • include_option (Boolean) (defaults to: false) —

    true: include CLI options (switches, env vars) in descriptions

  • agent_columns (Boolean) (defaults to: false) —

    true: add separate columns for each transfer agent compatibility

  • code_highlight (Boolean) (defaults to: false) —

    true: format name and type as code



24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/aspera/schema/documentation.rb', line 24

def initialize(formatter, schema, include_option: false, agent_columns: false, code_highlight: false)
  @formatter = formatter
  @schema = schema
  @include_option = include_option
  @agent_columns = agent_columns
  @code_highlight = code_highlight
  @columns = %w[name type description]
  @columns.insert(-2, *Agent::Factory::ALL.values.map { |i| i[:short].to_s }.sort) if @agent_columns
  # Sections: each entry is {header: row_or_nil, rows: []}
  # A flat schema produces a single section with no header.
  @sections = [{header: nil, rows: []}]
end

Instance Attribute Details

#columns ⇒ Array<String> (readonly)

Returns:



45
46
47
# File 'lib/aspera/schema/documentation.rb', line 45

def columns
  @columns
end

Instance Method Details

#build(schema = nil) ⇒ Documentation

Generate a documentation table from a JSON schema for transfer specifications

Recursively processes a JSON schema to create a formatted table for manual documentation. Handles nested objects, arrays, and extracts metadata (descriptions, types, enums, deprecations).

Parameters:

  • schema (Reader) (defaults to: nil) —

    The JSON schema to process

Returns:



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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
133
134
135
136
137
138
139
140
141
# File 'lib/aspera/schema/documentation.rb', line 60

def build(schema = nil)
  code = @code_highlight ? ->(c) { "`#{c}`" } : ->(c) { c }
  schema ||= @schema
  render_title = ->(title) { title.gsub(Markdown::FORMATS) { @formatter.markdown_text(Regexp.last_match) } }
  on_variant = ->(variant_reader, discriminant_property, discriminant_value) do
    title = variant_reader.current['title'] || variant_reader.current['description']
    header =
      if discriminant_property && discriminant_value
        desc = render_title.call("`#{discriminant_value}`")
        desc += ": #{render_title.call(title)}" if title
        @formatter.check_row({
          'name'        => render_title.call("**#{discriminant_property}**"),
          'type'        => code.call(JSON_TYPE_TO_DOC['string']),
          'description' => desc
        })
      elsif title
        @formatter.check_row({'name' => "**#{render_title.call(title)}**", 'type' => '&nbsp;', 'description' => '&nbsp;'})
      end
    @sections.push({header: header, rows: []})
  end
  schema.each_property(on_variant: on_variant) do |property_schema, _name, property_full_name|
    node = property_schema.current
    # Manual table
    item_type =
      if node['type'].is_a?(Array)
        node['type'].map { |t| JSON_TYPE_TO_DOC.fetch(t, t) }.join(', ')
      elsif node['type'].eql?('array') && node.dig('items', 'type').is_a?(String)
        "#{JSON_TYPE_TO_DOC.fetch(node['type'], node['type'])}[#{JSON_TYPE_TO_DOC.fetch(node.dig('items', 'type'), node.dig('items', 'type'))}]"
      else
        JSON_TYPE_TO_DOC.fetch(node['type'], node['type'])
      end
    item = {
      'name'        => code.call(property_full_name),
      'type'        => code.call(item_type),
      'description' => []
    }
    # Render Markdown formatting and split lines
    item['description'] =
      node['description']
        .gsub(Markdown::FORMATS) { @formatter.markdown_text(Regexp.last_match) }
        .split("\n") if node.key?('description')
    item['description'].unshift("DEPRECATED: #{node['x-deprecation']}") if node.key?('x-deprecation')
    # Add flags for supported agents in doc
    agents = []
    Agent::Factory::ALL.each_key do |sym|
      agents.push(sym) if node['x-agents'].nil? || node['x-agents'].include?(sym.to_s)
    end
    Aspera.assert(agents.include?(:direct)) { "#{property_full_name}: x-cli-option requires agent direct (or nil)" } if node['x-cli-option']
    if @agent_columns
      Agent::Factory::ALL.each do |sym, names|
        item[names[:short].to_s] = @formatter.tick(agents.include?(sym))
      end
    else
      item['description'].push("(#{agents.map { |i| Agent::Factory::ALL[i][:short].to_s.upcase }.sort.join(', ')})") unless agents.length.eql?(Agent::Factory::ALL.length)
    end
    # Only keep lines that are usable in supported agents
    next if agents.empty?
    item['description'].push("Allowed values: #{node['enum'].map { |v| value_text(v) }.join(', ')}.") if node.key?('enum')
    item['description'].push("Default: #{value_text(node['default'])}.") if node.key?('default')
    item['description'].push("Example: #{value_text(node['example'])}.") if node.key?('example')
    if @include_option
      envvar_prefix = ''
      cli_option =
        if node.key?('x-cli-envvar')
          envvar_prefix = 'env:'
          node['x-cli-envvar']
        elsif node['x-cli-switch']
          false_part = node.key?('x-cli-false') ? " / #{node['x-cli-false']}" : ''
          "#{node['x-cli-option']}#{false_part}"
        elsif node['x-cli-option']
          arg_type = node.key?('enum') ? '{enum}' : "{#{[node['type']].flatten.join('|')}}"
          conversion_tag = node.key?('x-cli-convert') ? 'conversion' : nil
          sep = node['x-cli-option'].start_with?('--') ? '=' : ' '
          "#{node['x-cli-option']}#{sep}#{"(#{conversion_tag})" if conversion_tag}#{arg_type}"
        end
      short = node.key?('x-cli-short') ? "(#{node['x-cli-short']})" : nil
      item['description'].push("(#{'special:' if node['x-cli-special']}#{envvar_prefix}#{@formatter.markdown_text("`#{cli_option}`")})#{short}") if cli_option
    end
    @sections.last[:rows].push(@formatter.check_row(item))
  end
  self
end

#rows ⇒ Object



37
38
39
40
41
42
# File 'lib/aspera/schema/documentation.rb', line 37

def rows
  @sections.flat_map do |section|
    sorted = section[:rows].sort_by { |i| i['name'] }
    section[:header] ? [section[:header]] + sorted : sorted
  end
end

#table ⇒ Array<Array<String>>

First row is the titles (for Markdown table generation)

Returns:



49
50
51
# File 'lib/aspera/schema/documentation.rb', line 49

def table
  [@columns] + rows.map { |row| @columns.map { |field| row[field] } }
end