aspera-cli 4.26.2 → 4.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +310 -21
- data/CONTRIBUTING.md +99 -14
- data/TODO.md +50 -0
- data/bin/ascli +3 -3
- data/docs/README.md +10977 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +29 -7
- data/lib/aspera/agent/connect.rb +58 -21
- data/lib/aspera/agent/desktop.rb +53 -18
- data/lib/aspera/agent/direct.rb +71 -40
- data/lib/aspera/agent/factory.rb +3 -3
- data/lib/aspera/agent/httpgw.rb +55 -14
- data/lib/aspera/agent/node.rb +34 -2
- data/lib/aspera/agent/transferd.rb +48 -12
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +42 -44
- data/lib/aspera/api/cos_node.rb +10 -9
- data/lib/aspera/api/faspex.rb +16 -15
- data/lib/aspera/api/httpgw.rb +23 -23
- data/lib/aspera/api/node.rb +51 -70
- data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
- data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
- data/lib/aspera/ascmd.rb +32 -30
- data/lib/aspera/ascp/installation.rb +75 -69
- data/lib/aspera/ascp/management.rb +7 -8
- data/lib/aspera/assert.rb +12 -13
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +125 -0
- data/lib/aspera/cli/bootstrapper.rb +197 -0
- data/lib/aspera/cli/command_registry.rb +145 -0
- data/lib/aspera/cli/command_spec.rb +141 -0
- data/lib/aspera/cli/context.rb +19 -6
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +27 -26
- data/lib/aspera/cli/formatter.rb +95 -59
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +3 -3
- data/lib/aspera/cli/http.rb +74 -30
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +225 -0
- data/lib/aspera/cli/option_declarator.rb +77 -0
- data/lib/aspera/cli/options.schema.yaml +634 -7
- data/lib/aspera/cli/parser.rb +1217 -0
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1178 -881
- data/lib/aspera/cli/plugins/ats.rb +205 -162
- data/lib/aspera/cli/plugins/base.rb +703 -175
- data/lib/aspera/cli/plugins/basic_auth.rb +7 -9
- data/lib/aspera/cli/plugins/config.rb +434 -757
- data/lib/aspera/cli/plugins/console.rb +107 -65
- data/lib/aspera/cli/plugins/cos.rb +46 -33
- data/lib/aspera/cli/plugins/factory.rb +6 -6
- data/lib/aspera/cli/plugins/faspex5.rb +627 -388
- data/lib/aspera/cli/plugins/faspio.rb +54 -51
- data/lib/aspera/cli/plugins/httpgw.rb +18 -25
- data/lib/aspera/cli/plugins/mcp.rb +279 -0
- data/lib/aspera/cli/plugins/node.rb +988 -840
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +112 -135
- data/lib/aspera/cli/plugins/preview.rb +195 -157
- data/lib/aspera/cli/plugins/server.rb +141 -91
- data/lib/aspera/cli/plugins/shares.rb +343 -110
- data/lib/aspera/cli/preset_actions.rb +159 -0
- data/lib/aspera/cli/preset_manager.rb +82 -39
- data/lib/aspera/cli/result.rb +78 -26
- data/lib/aspera/cli/runner.rb +323 -153
- data/lib/aspera/cli/secret_finder.rb +40 -0
- data/lib/aspera/cli/special_values.rb +1 -0
- data/lib/aspera/cli/sync_actions.rb +89 -64
- data/lib/aspera/cli/terminal_formatter.rb +3 -3
- data/lib/aspera/cli/transfer_actions.rb +90 -0
- data/lib/aspera/cli/transfer_agent.rb +129 -64
- data/lib/aspera/cli/transfer_progress.rb +9 -9
- data/lib/aspera/cli/vault_manager.rb +79 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +28 -30
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +34 -34
- data/lib/aspera/command_line_converter.rb +1 -1
- data/lib/aspera/coverage.rb +1 -2
- data/lib/aspera/data_repository.rb +2 -1
- data/lib/aspera/dot_container.rb +12 -12
- data/lib/aspera/environment.rb +30 -23
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +5 -5
- data/lib/aspera/faspex_postproc.rb +16 -10
- data/lib/aspera/graphql.rb +37 -0
- data/lib/aspera/hash_ext.rb +8 -2
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/base.rb +10 -3
- data/lib/aspera/keychain/encrypted_hash.rb +92 -17
- data/lib/aspera/keychain/factory.rb +20 -8
- data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +23 -25
- data/lib/aspera/keychain/one_password_api.rb +86 -0
- data/lib/aspera/keychain/one_password_base.rb +34 -0
- data/lib/aspera/keychain/one_password_cli.rb +98 -0
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +29 -15
- data/lib/aspera/markdown.rb +90 -7
- data/lib/aspera/nagios.rb +8 -8
- data/lib/aspera/node_simulator.rb +35 -27
- data/lib/aspera/oauth/base.rb +9 -10
- data/lib/aspera/oauth/boot.rb +5 -5
- data/lib/aspera/oauth/factory.rb +12 -10
- data/lib/aspera/oauth/jwt.rb +9 -11
- data/lib/aspera/oauth/web.rb +6 -6
- data/lib/aspera/persistency_action_once.rb +12 -12
- data/lib/aspera/persistency_folder.rb +18 -11
- data/lib/aspera/preview/file_types.rb +5 -5
- data/lib/aspera/preview/generator.rb +52 -49
- data/lib/aspera/preview/options.rb +3 -2
- data/lib/aspera/preview/terminal.rb +5 -5
- data/lib/aspera/preview/utils.rb +66 -19
- data/lib/aspera/products/connect.rb +2 -2
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +3 -3
- data/lib/aspera/products/transferd.rb +4 -4
- data/lib/aspera/proxy_auto_config.rb +15 -14
- data/lib/aspera/rest.rb +159 -151
- data/lib/aspera/rest_error_analyzer.rb +6 -6
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +11 -10
- data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
- data/lib/aspera/schema/documentation.rb +65 -28
- data/lib/aspera/schema/reader.rb +136 -13
- data/lib/aspera/schema/registry.rb +60 -8
- data/lib/aspera/secret_hider.rb +12 -7
- data/lib/aspera/ssh.rb +64 -31
- data/lib/aspera/ssl.rb +5 -5
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/database.rb +2 -2
- data/lib/aspera/sync/operations.rb +22 -25
- data/lib/aspera/temp_file_manager.rb +25 -5
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +36 -34
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +10 -10
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +12 -3
- data/lib/aspera/uri_reader.rb +52 -15
- data/lib/aspera/web_auth.rb +8 -7
- data/lib/aspera/web_server_simple.rb +15 -12
- data/lib/aspera/yaml.rb +5 -4
- data.tar.gz.sig +0 -0
- metadata +34 -6
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/manager.rb +0 -766
- data/lib/aspera/cli/plugins/faspex.rb +0 -539
- data/lib/aspera/json_rpc.rb +0 -52
- data/lib/aspera/transfer/uri.rb +0 -56
|
@@ -7,6 +7,15 @@ module Aspera
|
|
|
7
7
|
module Schema
|
|
8
8
|
# Generate documentation from Schema, for Transfer Spec, or async Conf spec
|
|
9
9
|
class Documentation
|
|
10
|
+
# Map JSON Schema type names to user-friendly display names
|
|
11
|
+
JSON_TYPE_TO_DOC = {
|
|
12
|
+
'string' => 'String',
|
|
13
|
+
'integer' => 'Integer',
|
|
14
|
+
'number' => 'Number',
|
|
15
|
+
'boolean' => 'Bool',
|
|
16
|
+
'array' => 'Array',
|
|
17
|
+
'object' => 'Hash'
|
|
18
|
+
}.freeze
|
|
10
19
|
# @param formatter [Cli::Formatter] Formatter instance with methods: markdown_text, tick, check_row
|
|
11
20
|
# @param schema [Reader]
|
|
12
21
|
# @param include_option [Boolean] `true`: include CLI options (switches, env vars) in descriptions
|
|
@@ -18,25 +27,27 @@ module Aspera
|
|
|
18
27
|
@include_option = include_option
|
|
19
28
|
@agent_columns = agent_columns
|
|
20
29
|
@code_highlight = code_highlight
|
|
21
|
-
@columns = %
|
|
22
|
-
@columns.insert(-2, *Agent::Factory::ALL.values.map{ |i| i[:short]}.sort) if @agent_columns
|
|
23
|
-
#
|
|
24
|
-
|
|
30
|
+
@columns = %w[name type description]
|
|
31
|
+
@columns.insert(-2, *Agent::Factory::ALL.values.map { |i| i[:short].to_s }.sort) if @agent_columns
|
|
32
|
+
# Sections: each entry is {header: row_or_nil, rows: []}
|
|
33
|
+
# A flat schema produces a single section with no header.
|
|
34
|
+
@sections = [{header: nil, rows: []}]
|
|
25
35
|
end
|
|
26
36
|
|
|
27
37
|
def rows
|
|
28
|
-
@
|
|
38
|
+
@sections.flat_map do |section|
|
|
39
|
+
sorted = section[:rows].sort_by { |i| i['name'] }
|
|
40
|
+
section[:header] ? [section[:header]] + sorted : sorted
|
|
41
|
+
end
|
|
29
42
|
end
|
|
30
43
|
|
|
31
44
|
# @return [Array<String>]
|
|
32
|
-
|
|
33
|
-
@columns.map(&:to_s)
|
|
34
|
-
end
|
|
45
|
+
attr_reader :columns
|
|
35
46
|
|
|
36
|
-
# First row is the titles
|
|
47
|
+
# First row is the titles (for Markdown table generation)
|
|
37
48
|
# @return [Array<Array<String>>]
|
|
38
49
|
def table
|
|
39
|
-
[@columns
|
|
50
|
+
[@columns] + rows.map { |row| @columns.map { |field| row[field] } }
|
|
40
51
|
end
|
|
41
52
|
|
|
42
53
|
# Generate a documentation table from a JSON schema for transfer specifications
|
|
@@ -47,39 +58,65 @@ module Aspera
|
|
|
47
58
|
# @param schema [Reader] The JSON schema to process
|
|
48
59
|
# @return [Documentation]
|
|
49
60
|
def build(schema = nil)
|
|
50
|
-
code = @code_highlight ? ->(c){"`#{c}`"} : ->(c){c}
|
|
61
|
+
code = @code_highlight ? ->(c) { "`#{c}`" } : ->(c) { c }
|
|
51
62
|
schema ||= @schema
|
|
52
|
-
|
|
63
|
+
render_title = ->(title) { title.gsub(Markdown::FORMATS) { @formatter.markdown_text(Regexp.last_match) } }
|
|
64
|
+
on_variant = ->(variant_reader, discriminant_property, discriminant_value) do
|
|
65
|
+
title = variant_reader.current['title'] || variant_reader.current['description']
|
|
66
|
+
header =
|
|
67
|
+
if discriminant_property && discriminant_value
|
|
68
|
+
desc = render_title.call("`#{discriminant_value}`")
|
|
69
|
+
desc += ": #{render_title.call(title)}" if title
|
|
70
|
+
@formatter.check_row({
|
|
71
|
+
'name' => render_title.call("**#{discriminant_property}**"),
|
|
72
|
+
'type' => code.call(JSON_TYPE_TO_DOC['string']),
|
|
73
|
+
'description' => desc
|
|
74
|
+
})
|
|
75
|
+
elsif title
|
|
76
|
+
@formatter.check_row({'name' => "**#{render_title.call(title)}**", 'type' => ' ', 'description' => ' '})
|
|
77
|
+
end
|
|
78
|
+
@sections.push({header: header, rows: []})
|
|
79
|
+
end
|
|
80
|
+
schema.each_property(on_variant: on_variant) do |property_schema, _name, property_full_name|
|
|
53
81
|
node = property_schema.current
|
|
54
82
|
# Manual table
|
|
83
|
+
item_type =
|
|
84
|
+
if node['type'].is_a?(Array)
|
|
85
|
+
node['type'].map { |t| JSON_TYPE_TO_DOC.fetch(t, t) }.join(', ')
|
|
86
|
+
elsif node['type'].eql?('array') && node.dig('items', 'type').is_a?(String)
|
|
87
|
+
"#{JSON_TYPE_TO_DOC.fetch(node['type'], node['type'])}[#{JSON_TYPE_TO_DOC.fetch(node.dig('items', 'type'), node.dig('items', 'type'))}]"
|
|
88
|
+
else
|
|
89
|
+
JSON_TYPE_TO_DOC.fetch(node['type'], node['type'])
|
|
90
|
+
end
|
|
55
91
|
item = {
|
|
56
|
-
name
|
|
57
|
-
type
|
|
58
|
-
description
|
|
92
|
+
'name' => code.call(property_full_name),
|
|
93
|
+
'type' => code.call(item_type),
|
|
94
|
+
'description' => []
|
|
59
95
|
}
|
|
60
96
|
# Render Markdown formatting and split lines
|
|
61
|
-
item[
|
|
97
|
+
item['description'] =
|
|
62
98
|
node['description']
|
|
63
|
-
.gsub(Markdown::FORMATS){@formatter.markdown_text(Regexp.last_match)}
|
|
99
|
+
.gsub(Markdown::FORMATS) { @formatter.markdown_text(Regexp.last_match) }
|
|
64
100
|
.split("\n") if node.key?('description')
|
|
65
|
-
item[
|
|
101
|
+
item['description'].unshift("DEPRECATED: #{node['x-deprecation']}") if node.key?('x-deprecation')
|
|
66
102
|
# Add flags for supported agents in doc
|
|
67
103
|
agents = []
|
|
68
104
|
Agent::Factory::ALL.each_key do |sym|
|
|
69
105
|
agents.push(sym) if node['x-agents'].nil? || node['x-agents'].include?(sym.to_s)
|
|
70
106
|
end
|
|
71
|
-
Aspera.assert(agents.include?(:direct)){"#{
|
|
107
|
+
Aspera.assert(agents.include?(:direct)) { "#{property_full_name}: x-cli-option requires agent direct (or nil)" } if node['x-cli-option']
|
|
72
108
|
if @agent_columns
|
|
73
109
|
Agent::Factory::ALL.each do |sym, names|
|
|
74
|
-
item[names[:short]] = @formatter.tick(agents.include?(sym))
|
|
110
|
+
item[names[:short].to_s] = @formatter.tick(agents.include?(sym))
|
|
75
111
|
end
|
|
76
112
|
else
|
|
77
|
-
item[
|
|
113
|
+
item['description'].push("(#{agents.map { |i| Agent::Factory::ALL[i][:short].to_s.upcase }.sort.join(', ')})") unless agents.length.eql?(Agent::Factory::ALL.length)
|
|
78
114
|
end
|
|
79
115
|
# Only keep lines that are usable in supported agents
|
|
80
|
-
next
|
|
81
|
-
item[
|
|
82
|
-
item[
|
|
116
|
+
next if agents.empty?
|
|
117
|
+
item['description'].push("Allowed values: #{node['enum'].map { |v| @formatter.markdown_text("`#{v}`") }.join(', ')}.") if node.key?('enum')
|
|
118
|
+
item['description'].push("Default: #{@formatter.markdown_text("`#{node['default']}`")}.") if node.key?('default')
|
|
119
|
+
item['description'].push("Example: #{@formatter.markdown_text("`#{node['example']}`")}.") if node.key?('example')
|
|
83
120
|
if @include_option
|
|
84
121
|
envvar_prefix = ''
|
|
85
122
|
cli_option =
|
|
@@ -87,18 +124,18 @@ module Aspera
|
|
|
87
124
|
envvar_prefix = 'env:'
|
|
88
125
|
node['x-cli-envvar']
|
|
89
126
|
elsif node['x-cli-switch']
|
|
90
|
-
node['x-cli-
|
|
127
|
+
false_part = node.key?('x-cli-false') ? " / #{node['x-cli-false']}" : ''
|
|
128
|
+
"#{node['x-cli-option']}#{false_part}"
|
|
91
129
|
elsif node['x-cli-option']
|
|
92
130
|
arg_type = node.key?('enum') ? '{enum}' : "{#{[node['type']].flatten.join('|')}}"
|
|
93
|
-
# conversion_tag = node['x-cli-convert']
|
|
94
131
|
conversion_tag = node.key?('x-cli-convert') ? 'conversion' : nil
|
|
95
132
|
sep = node['x-cli-option'].start_with?('--') ? '=' : ' '
|
|
96
133
|
"#{node['x-cli-option']}#{sep}#{"(#{conversion_tag})" if conversion_tag}#{arg_type}"
|
|
97
134
|
end
|
|
98
135
|
short = node.key?('x-cli-short') ? "(#{node['x-cli-short']})" : nil
|
|
99
|
-
item[
|
|
136
|
+
item['description'].push("(#{'special:' if node['x-cli-special']}#{envvar_prefix}#{@formatter.markdown_text("`#{cli_option}`")})#{short}") if cli_option
|
|
100
137
|
end
|
|
101
|
-
@rows.push(@formatter.check_row(item))
|
|
138
|
+
@sections.last[:rows].push(@formatter.check_row(item))
|
|
102
139
|
end
|
|
103
140
|
self
|
|
104
141
|
end
|
data/lib/aspera/schema/reader.rb
CHANGED
|
@@ -7,11 +7,34 @@ module Aspera
|
|
|
7
7
|
class Reader
|
|
8
8
|
attr_reader :current
|
|
9
9
|
|
|
10
|
+
class << self
|
|
11
|
+
# Build a synthetic Reader from an OAS `parameters` array (entries with `in: query`).
|
|
12
|
+
# Produces a JSON Schema object whose `properties` map each query param name to its schema,
|
|
13
|
+
# with the OAS-level `description` and `required` merged in.
|
|
14
|
+
# @param params [Array<Hash>] raw OAS parameter objects (may contain path/header params too)
|
|
15
|
+
# @return [Reader]
|
|
16
|
+
def from_query_params(params)
|
|
17
|
+
properties = {}
|
|
18
|
+
required_names = []
|
|
19
|
+
params.each do |param|
|
|
20
|
+
next unless param['in'] == 'query'
|
|
21
|
+
name = param['name']
|
|
22
|
+
prop = (param['schema'] || {}).dup
|
|
23
|
+
prop['description'] = param['description'] if param['description'] && !prop.key?('description')
|
|
24
|
+
properties[name] = prop
|
|
25
|
+
required_names << name if param['required']
|
|
26
|
+
end
|
|
27
|
+
synthetic = {'type' => 'object', 'properties' => properties}
|
|
28
|
+
synthetic['required'] = required_names unless required_names.empty?
|
|
29
|
+
new(synthetic)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
10
33
|
# Shortcut to access current value at path
|
|
11
|
-
# @param
|
|
34
|
+
# @param key [String] path element
|
|
12
35
|
# @return [Hash, Array, String, Integer] current value at path
|
|
13
|
-
def [](
|
|
14
|
-
@current[
|
|
36
|
+
def [](key)
|
|
37
|
+
@current[key]
|
|
15
38
|
end
|
|
16
39
|
|
|
17
40
|
# Find sub path relative to current
|
|
@@ -19,18 +42,24 @@ module Aspera
|
|
|
19
42
|
def dig(*path)
|
|
20
43
|
current = @current
|
|
21
44
|
path.each do |p|
|
|
22
|
-
Aspera.assert(current.key?(p)){"schema: #{p} in #{path}"}
|
|
45
|
+
Aspera.assert(current.key?(p)) { "schema: #{p} in #{path}" }
|
|
23
46
|
current = current[p]
|
|
24
|
-
Aspera.assert_type(current, Hash){'schema'}
|
|
47
|
+
Aspera.assert_type(current, Hash) { 'schema' }
|
|
25
48
|
if current.key?('$ref')
|
|
26
49
|
ref = current['$ref']
|
|
27
|
-
Aspera.assert(ref.start_with?('#/')){"schema $ref must start with '#/': #{ref}"}
|
|
50
|
+
Aspera.assert(ref.start_with?('#/')) { "schema $ref must start with '#/': #{ref}" }
|
|
28
51
|
current = @root.dig(*ref[2..].split('/'))
|
|
29
52
|
end
|
|
30
53
|
end
|
|
31
54
|
Reader.new(@root, current)
|
|
32
55
|
end
|
|
33
56
|
|
|
57
|
+
# Resolve a $ref string to a Reader
|
|
58
|
+
def resolve_ref(ref)
|
|
59
|
+
Aspera.assert(ref.start_with?('#/')) { "schema $ref must start with '#/': #{ref}" }
|
|
60
|
+
Reader.new(@root, @root.dig(*ref[2..].split('/')))
|
|
61
|
+
end
|
|
62
|
+
|
|
34
63
|
# Read schema from file or from cache
|
|
35
64
|
# @param root [Hash] root schema
|
|
36
65
|
# @param current [Hash, nil] current position in
|
|
@@ -40,15 +69,44 @@ module Aspera
|
|
|
40
69
|
@current = current || root
|
|
41
70
|
end
|
|
42
71
|
|
|
43
|
-
# Recursively traverse schema properties with a block
|
|
44
|
-
#
|
|
45
|
-
#
|
|
72
|
+
# Recursively traverse schema properties with a block.
|
|
73
|
+
# If the current node has `oneOf`, each variant is traversed in turn and
|
|
74
|
+
# `on_variant` is called (if given) before each variant's properties.
|
|
75
|
+
# @param prefix [String] Prefix for property names (e.g., 'parent.child.')
|
|
76
|
+
# @param on_variant [Proc, nil] Called with the variant Reader before its properties
|
|
46
77
|
# @yield [property_schema, name, full_name] Yields property info to block
|
|
47
|
-
# @yieldparam property_schema [Reader] Schema reader for this property
|
|
78
|
+
# @yieldparam property_schema [Reader] Schema reader for this property
|
|
48
79
|
# @yieldparam name [String] Property name
|
|
49
80
|
# @yieldparam full_name [String] Full property name with prefix
|
|
50
81
|
# @return [nil]
|
|
51
|
-
def each_property(prefix = '', &block)
|
|
82
|
+
def each_property(prefix = '', on_variant: nil, &block)
|
|
83
|
+
if @current.key?('oneOf')
|
|
84
|
+
# Build reverse map: $ref -> discriminant value, from discriminator.mapping if present
|
|
85
|
+
discriminant_by_ref = {}
|
|
86
|
+
if @current.dig('discriminator', 'mapping').is_a?(Hash)
|
|
87
|
+
@current['discriminator']['mapping'].each do |value, ref|
|
|
88
|
+
discriminant_by_ref[ref] = value
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
discriminant_property = @current.dig('discriminator', 'propertyName')
|
|
92
|
+
@current['oneOf'].each do |variant_node|
|
|
93
|
+
ref = variant_node['$ref']
|
|
94
|
+
variant_reader = ref ? resolve_ref(ref) : Reader.new(@root, variant_node)
|
|
95
|
+
discriminant_value = ref ? discriminant_by_ref[ref] : nil
|
|
96
|
+
on_variant&.call(variant_reader, discriminant_property, discriminant_value)
|
|
97
|
+
variant_reader.each_property(prefix, on_variant: on_variant, &block)
|
|
98
|
+
end
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
if @current.key?('allOf')
|
|
102
|
+
# Merge all branches: each branch contributes its properties (no variants)
|
|
103
|
+
@current['allOf'].each do |branch_node|
|
|
104
|
+
ref = branch_node['$ref']
|
|
105
|
+
branch_reader = ref ? resolve_ref(ref) : Reader.new(@root, branch_node)
|
|
106
|
+
branch_reader.each_property(prefix, on_variant: on_variant, &block)
|
|
107
|
+
end
|
|
108
|
+
return
|
|
109
|
+
end
|
|
52
110
|
properties = dig('properties')
|
|
53
111
|
properties.current.each_key do |name|
|
|
54
112
|
property_full_name = "#{prefix}#{name}"
|
|
@@ -61,13 +119,78 @@ module Aspera
|
|
|
61
119
|
# Recursively process nested structures
|
|
62
120
|
case node['type']
|
|
63
121
|
when 'object'
|
|
64
|
-
property_schema.each_property("#{property_full_name}.", &block) if node['properties']
|
|
122
|
+
property_schema.each_property("#{property_full_name}.", on_variant: on_variant, &block) if node['properties']
|
|
65
123
|
when 'array'
|
|
66
124
|
if node['items']
|
|
67
125
|
array_item_schema = property_schema.dig('items')
|
|
68
|
-
array_item_schema.each_property("#{property_full_name}[].", &block) if array_item_schema.current['properties']
|
|
126
|
+
array_item_schema.each_property("#{property_full_name}[].", on_variant: on_variant, &block) if array_item_schema.current['properties']
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
# allOf without explicit type: object — recurse to merge all branches
|
|
130
|
+
property_schema.each_property("#{property_full_name}.", on_variant: on_variant, &block) if node['allOf']
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Convert this schema to a flat array of field descriptors.
|
|
135
|
+
# Returns raw semantic fields with no ANSI or formatting, suitable for
|
|
136
|
+
# JSON/YAML output or MCP consumption. An AI or script can use the result
|
|
137
|
+
# directly to build a valid payload.
|
|
138
|
+
#
|
|
139
|
+
# Each Hash entry contains:
|
|
140
|
+
# name [String] dot/bracket-path of the field (e.g. "recipients[].name")
|
|
141
|
+
# type [String] JSON type string (e.g. "string", "boolean", "Array[object]")
|
|
142
|
+
# required [Boolean] true when the field is in its immediate parent's required list
|
|
143
|
+
# description [String] human description (may contain Markdown **bold** / `code`)
|
|
144
|
+
# default [Object] (optional) default value as native Ruby type
|
|
145
|
+
# enum [Array] (optional) list of allowed string values
|
|
146
|
+
#
|
|
147
|
+
# @return [Array<Hash>]
|
|
148
|
+
def to_rows
|
|
149
|
+
rows = []
|
|
150
|
+
collect_rows(rows, self, '')
|
|
151
|
+
rows
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
# Recursively collect rows from a schema node, passing each property's own
|
|
157
|
+
# parent `required` array so that only direct-parent membership is checked.
|
|
158
|
+
def collect_rows(rows, reader, prefix)
|
|
159
|
+
return unless reader.current.key?('properties')
|
|
160
|
+
parent_required = Set.new(Array(reader.current['required']))
|
|
161
|
+
props_reader = reader.dig('properties')
|
|
162
|
+
props_reader.current.each_key do |name|
|
|
163
|
+
prop_reader = props_reader.dig(name)
|
|
164
|
+
node = prop_reader.current
|
|
165
|
+
full_name = "#{prefix}#{name}"
|
|
166
|
+
type_val =
|
|
167
|
+
if node['type'].is_a?(Array)
|
|
168
|
+
node['type'].join(', ')
|
|
169
|
+
elsif node['type'].eql?('array') && node.dig('items', 'type').is_a?(String)
|
|
170
|
+
"Array[#{node.dig('items', 'type')}]"
|
|
171
|
+
else
|
|
172
|
+
node['type'].to_s
|
|
173
|
+
end
|
|
174
|
+
row = {
|
|
175
|
+
'name' => full_name,
|
|
176
|
+
'type' => type_val,
|
|
177
|
+
'required' => parent_required.include?(name),
|
|
178
|
+
'description' => node['description'].to_s
|
|
179
|
+
}
|
|
180
|
+
row['default'] = node['default'] if node.key?('default')
|
|
181
|
+
row['enum'] = node['enum'] if node.key?('enum')
|
|
182
|
+
rows << row
|
|
183
|
+
# Recurse into nested object or array-of-objects
|
|
184
|
+
case node['type']
|
|
185
|
+
when 'object'
|
|
186
|
+
collect_rows(rows, prop_reader, "#{full_name}.") if node['properties']
|
|
187
|
+
when 'array'
|
|
188
|
+
if node['items']
|
|
189
|
+
item_reader = prop_reader.dig('items')
|
|
190
|
+
collect_rows(rows, item_reader, "#{full_name}[].") if item_reader.current['properties']
|
|
69
191
|
end
|
|
70
192
|
end
|
|
193
|
+
collect_rows(rows, prop_reader, "#{full_name}.") if node['allOf']
|
|
71
194
|
end
|
|
72
195
|
end
|
|
73
196
|
end
|
|
@@ -18,10 +18,32 @@ module Aspera
|
|
|
18
18
|
LOCATIONS.key?(sym)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
#
|
|
21
|
+
# Split a component string into registry key and optional path prefix.
|
|
22
|
+
# Syntax: 'key' or 'key+/prefix' (e.g. 'shares+/api/v1')
|
|
23
|
+
# @param component [String] registry key with optional '+/prefix'
|
|
24
|
+
# @return [Array(String, String)] [key, prefix] where prefix may be ''
|
|
25
|
+
def split_component(component)
|
|
26
|
+
key, prefix = component.split('+', 2)
|
|
27
|
+
[key, prefix || '']
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get path to request body schema, no check if it exists
|
|
31
|
+
# @param component [String] registry key, optionally with path prefix (e.g. 'shares+/api/v1')
|
|
32
|
+
# @param endpoint [String] endpoint path without leading slash (e.g. 'data/shares.post')
|
|
33
|
+
# @return [String] schema path usable in schema: keyword
|
|
23
34
|
def req_body(component, endpoint)
|
|
24
|
-
|
|
35
|
+
key, prefix = split_component(component)
|
|
36
|
+
"#{key}:paths.#{prefix}/#{endpoint}.requestBody.content.application/json.schema"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get path to query parameters for a GET endpoint
|
|
40
|
+
# @param component [String] registry key, optionally with path prefix (e.g. 'shares+/api/v1')
|
|
41
|
+
# @param endpoint [String] resource path without leading slash (e.g. 'data/shares')
|
|
42
|
+
# @param method [String] HTTP method (default: 'get')
|
|
43
|
+
# @return [String] schema path usable in query_schema: keyword
|
|
44
|
+
def query_params(component, endpoint, method: 'get')
|
|
45
|
+
key, prefix = split_component(component)
|
|
46
|
+
"#{key}:paths.#{prefix}/#{endpoint}.#{method}#{QUERY_PARAMS_SUFFIX}"
|
|
25
47
|
end
|
|
26
48
|
end
|
|
27
49
|
|
|
@@ -32,6 +54,9 @@ module Aspera
|
|
|
32
54
|
opts: 'aspera/cli/options.schema.yaml',
|
|
33
55
|
aoc: 'aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml',
|
|
34
56
|
faspex: 'aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml',
|
|
57
|
+
faspio: 'aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml',
|
|
58
|
+
node: 'aspera/schema/IBM Aspera Node API-4.4.6.yaml',
|
|
59
|
+
shares: 'aspera/schema/IBM_Aspera_Shares.yaml',
|
|
35
60
|
async_tables: 'aspera/schema/async_tables.yaml'
|
|
36
61
|
}
|
|
37
62
|
|
|
@@ -41,26 +66,53 @@ module Aspera
|
|
|
41
66
|
SYNC_ARGS = 'args'
|
|
42
67
|
AOC = 'aoc'
|
|
43
68
|
FASPEX = 'faspex'
|
|
69
|
+
FASPIO = 'faspio'
|
|
70
|
+
NODE = 'node'
|
|
71
|
+
SHARES = 'shares+/api/v1'
|
|
44
72
|
ASYNC_TABLES = 'async_tables'
|
|
45
|
-
|
|
73
|
+
LOG_OPTIONS = "#{OPTIONS}:components.schemas.LogOptions"
|
|
74
|
+
DIRECT_AGENT_OPTIONS = "#{OPTIONS}:components.schemas.DirectAgentOptions"
|
|
75
|
+
NODE_AGENT_OPTIONS = "#{OPTIONS}:components.schemas.NodeAgentOptions"
|
|
76
|
+
HTTPGW_AGENT_OPTIONS = "#{OPTIONS}:components.schemas.HttpgwAgentOptions"
|
|
77
|
+
TRANSFERD_AGENT_OPTIONS = "#{OPTIONS}:components.schemas.TransferdAgentOptions"
|
|
78
|
+
TRANSFER_AGENT_OPTIONS = "#{OPTIONS}:components.schemas.TransferAgentOptions"
|
|
79
|
+
SMTP_OPTIONS = "#{OPTIONS}:components.schemas.SmtpOptions"
|
|
80
|
+
HTTP_OPTIONS = "#{OPTIONS}:components.schemas.HttpOptions"
|
|
81
|
+
VAULT_OPTIONS = "#{OPTIONS}:components.schemas.VaultOptions"
|
|
82
|
+
VAULT_SECRET = "#{OPTIONS}:components.schemas.VaultSecret"
|
|
83
|
+
IMAGE_OPTIONS = "#{OPTIONS}:components.schemas.ImageOptions"
|
|
84
|
+
OUT_OPTIONS = "#{OPTIONS}:components.schemas.OutOptions"
|
|
85
|
+
PACKAGE_FOLDER_OPTIONS = "#{OPTIONS}:components.schemas.PackageFolderOptions"
|
|
46
86
|
|
|
47
87
|
REQ_BODY = '.requestBody.content.application/json.schema'
|
|
88
|
+
# Suffix appended to a dotted path to signal query-param extraction in reader()
|
|
89
|
+
QUERY_PARAMS_SUFFIX = '.parameters'
|
|
48
90
|
|
|
49
91
|
def initialize
|
|
50
92
|
@cache = {}
|
|
51
93
|
@main_folder = File.expand_path('../..', __dir__)
|
|
52
94
|
end
|
|
53
95
|
|
|
54
|
-
# Read schema from file or from cache
|
|
55
|
-
#
|
|
56
|
-
#
|
|
96
|
+
# Read schema from file or from cache.
|
|
97
|
+
# When name_path ends with QUERY_PARAMS_SUFFIX, the OAS `parameters` array at that path
|
|
98
|
+
# is synthesised into an object schema via Reader.from_query_params instead of navigating
|
|
99
|
+
# into the tree.
|
|
100
|
+
# @param name_path [String] registry key with optional colon-separated dotted path suffix,
|
|
101
|
+
# e.g. "faspex:paths./packages.get.parameters" or "faspex:paths./packages.post.requestBody..."
|
|
102
|
+
# @return [Reader] schema reader
|
|
57
103
|
def reader(name_path)
|
|
58
104
|
name, path = name_path.split(':', 2)
|
|
59
105
|
sym = name.to_sym
|
|
60
|
-
Aspera.assert(Registry.known?(sym)){"schema: #{sym}"}
|
|
106
|
+
Aspera.assert(Registry.known?(sym)) { "schema: #{sym}" }
|
|
61
107
|
spec_file = File.join(@main_folder, LOCATIONS[sym])
|
|
62
108
|
@cache[sym] = Yaml.safe_load(File.read(spec_file)) if spec_file.end_with?('.yaml') && !@cache.key?(sym)
|
|
63
109
|
@cache[sym] = JSON.parse(File.read(spec_file)) if spec_file.end_with?('.json') && !@cache.key?(sym)
|
|
110
|
+
# Query-params path: strip the suffix, navigate to the operation node, extract parameters
|
|
111
|
+
if path&.end_with?(QUERY_PARAMS_SUFFIX)
|
|
112
|
+
parent_path = path.delete_suffix(QUERY_PARAMS_SUFFIX)
|
|
113
|
+
node = @cache[sym].dig(*parent_path.split('.'))
|
|
114
|
+
return Reader.from_query_params(node&.fetch('parameters', []) || [])
|
|
115
|
+
end
|
|
64
116
|
reader = Reader.new(@cache[sym])
|
|
65
117
|
return reader unless path
|
|
66
118
|
reader.dig(*path.split('.'))
|
data/lib/aspera/secret_hider.rb
CHANGED
|
@@ -45,41 +45,46 @@ module Aspera
|
|
|
45
45
|
private_constant :HIDDEN_PASSWORD, :ASCP_ENV_SECRETS, :KEY_SECRETS, :HTTP_SECRETS, :ALL_SECRETS, :KEY_FALSE_POSITIVES, :REGEX_LOG_REPLACES
|
|
46
46
|
attr_accessor :log_secrets
|
|
47
47
|
|
|
48
|
-
# @return new log formatter that hides secrets
|
|
48
|
+
# @return [Proc] new log formatter that hides secrets
|
|
49
49
|
def log_formatter(original_formatter)
|
|
50
50
|
original_formatter ||= Logger::Formatter.new
|
|
51
51
|
# NOTE: that @log_secrets may be set AFTER this init is done, so it's done at runtime
|
|
52
52
|
return lambda do |severity, date_time, program_name, msg|
|
|
53
53
|
if msg.is_a?(String) && !@log_secrets
|
|
54
54
|
REGEX_LOG_REPLACES.each do |reg_ex|
|
|
55
|
-
msg = msg.gsub(reg_ex){"#{Regexp.last_match(:begin)}#{HIDDEN_PASSWORD}#{Regexp.last_match(:end)}"}
|
|
55
|
+
msg = msg.gsub(reg_ex) { "#{Regexp.last_match(:begin)}#{HIDDEN_PASSWORD}#{Regexp.last_match(:end)}" }
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
original_formatter.call(severity, date_time, program_name, msg)
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Replace private key content in a string with the hidden password placeholder
|
|
63
|
+
# @param value [String] Input string possibly containing a private key
|
|
64
|
+
# @return [String] String with private key replaced by placeholder
|
|
62
65
|
def hide_secrets_in_string(value)
|
|
63
|
-
return value.gsub(REGEX_LOG_REPLACES.first){"#{Regexp.last_match(:begin)}#{HIDDEN_PASSWORD}#{Regexp.last_match(:end)}"}
|
|
66
|
+
return value.gsub(REGEX_LOG_REPLACES.first) { "#{Regexp.last_match(:begin)}#{HIDDEN_PASSWORD}#{Regexp.last_match(:end)}" }
|
|
64
67
|
end
|
|
65
68
|
|
|
66
|
-
# @
|
|
69
|
+
# @param keyword [String, Symbol] Key name to check
|
|
70
|
+
# @param value [String] Associated value (must be a String to be a secret)
|
|
71
|
+
# @return [Boolean] true if the key denotes a secret
|
|
67
72
|
def secret?(keyword, value)
|
|
68
73
|
keyword = keyword.to_s if keyword.is_a?(Symbol)
|
|
69
74
|
# only Strings can be secrets, not booleans, or hash, arrays
|
|
70
75
|
return false unless keyword.is_a?(String) && value.is_a?(String)
|
|
71
76
|
# those are not secrets
|
|
72
|
-
return false if KEY_FALSE_POSITIVES.any?{ |f| f.match?(keyword)}
|
|
77
|
+
return false if KEY_FALSE_POSITIVES.any? { |f| f.match?(keyword) }
|
|
73
78
|
return true if ADDITIONAL_KEYS_TO_HIDE.include?(keyword)
|
|
74
79
|
# check if keyword (name) contains an element that designate it as a secret
|
|
75
|
-
ALL_SECRETS.any?{ |kw| keyword.include?(kw)}
|
|
80
|
+
ALL_SECRETS.any? { |kw| keyword.include?(kw) }
|
|
76
81
|
end
|
|
77
82
|
|
|
78
83
|
# Hides recursively secrets in Hash or Array of Hash
|
|
79
84
|
def deep_remove_secret(obj)
|
|
80
85
|
case obj
|
|
81
86
|
when Array
|
|
82
|
-
obj.each{ |i| deep_remove_secret(i)}
|
|
87
|
+
obj.each { |i| deep_remove_secret(i) }
|
|
83
88
|
when Hash
|
|
84
89
|
obj.each do |k, v|
|
|
85
90
|
if secret?(k, v)
|