aspera-cli 4.27.1 → 4.27.3
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 +67 -1
- data/bin/ascli +2 -1
- data/docs/README.md +805 -747
- data/lib/aspera/agent/connect.rb +6 -4
- data/lib/aspera/agent/desktop.rb +2 -2
- data/lib/aspera/agent/direct.rb +3 -1
- data/lib/aspera/agent/node.rb +3 -3
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +14 -12
- data/lib/aspera/api/ats.rb +1 -1
- data/lib/aspera/api/cos_node.rb +2 -2
- data/lib/aspera/api/faspex.rb +9 -7
- data/lib/aspera/api/httpgw.rb +37 -33
- data/lib/aspera/api/node.rb +38 -33
- data/lib/aspera/ascmd.rb +3 -1
- data/lib/aspera/ascp/installation.rb +62 -27
- data/lib/aspera/ascp/management.rb +1 -0
- data/lib/aspera/assert.rb +4 -0
- data/lib/aspera/cli/ascp_actions.rb +20 -41
- data/lib/aspera/cli/async_transfer_store.rb +2 -2
- data/lib/aspera/cli/bootstrapper.rb +11 -15
- data/lib/aspera/cli/command_line.rb +252 -0
- data/lib/aspera/cli/command_registry.rb +149 -33
- data/lib/aspera/cli/command_spec.rb +103 -14
- data/lib/aspera/cli/completion/ascli.bash +12 -0
- data/lib/aspera/cli/completion/ascli.fish +16 -0
- data/lib/aspera/cli/completion/ascli.zsh +19 -0
- data/lib/aspera/cli/context.rb +3 -0
- data/lib/aspera/cli/deprecation.rb +37 -0
- data/lib/aspera/cli/extended_value.rb +2 -0
- data/lib/aspera/cli/formatter.rb +87 -75
- data/lib/aspera/cli/gem_checker.rb +1 -1
- data/lib/aspera/cli/hints.rb +7 -6
- data/lib/aspera/cli/http.rb +21 -21
- data/lib/aspera/cli/info.rb +3 -0
- data/lib/aspera/cli/mcp_tool.rb +47 -83
- data/lib/aspera/cli/option_declarator.rb +33 -42
- data/lib/aspera/cli/option_registry.rb +69 -0
- data/lib/aspera/cli/option_types.rb +103 -0
- data/lib/aspera/cli/option_value.rb +281 -0
- data/lib/aspera/cli/options.schema.yaml +38 -5
- data/lib/aspera/cli/parser.rb +307 -848
- data/lib/aspera/cli/plugins/alee.rb +7 -4
- data/lib/aspera/cli/plugins/aoc.rb +435 -380
- data/lib/aspera/cli/plugins/ats.rb +58 -73
- data/lib/aspera/cli/plugins/base.rb +190 -240
- data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
- data/lib/aspera/cli/plugins/config.rb +244 -178
- data/lib/aspera/cli/plugins/console.rb +102 -38
- data/lib/aspera/cli/plugins/cos.rb +6 -23
- data/lib/aspera/cli/plugins/factory.rb +3 -0
- data/lib/aspera/cli/plugins/faspex5.rb +176 -173
- data/lib/aspera/cli/plugins/faspio.rb +5 -10
- data/lib/aspera/cli/plugins/httpgw.rb +8 -11
- data/lib/aspera/cli/plugins/mcp.rb +20 -55
- data/lib/aspera/cli/plugins/node.rb +277 -311
- data/lib/aspera/cli/plugins/orchestrator.rb +90 -77
- data/lib/aspera/cli/plugins/preview.rb +79 -90
- data/lib/aspera/cli/plugins/server.rb +76 -50
- data/lib/aspera/cli/plugins/shares.rb +68 -116
- data/lib/aspera/cli/preset_actions.rb +17 -10
- data/lib/aspera/cli/preset_manager.rb +12 -2
- data/lib/aspera/cli/prompt.rb +35 -0
- data/lib/aspera/cli/result.rb +13 -18
- data/lib/aspera/cli/runner.rb +31 -54
- data/lib/aspera/cli/special_values.rb +5 -0
- data/lib/aspera/cli/sync_actions.rb +41 -37
- data/lib/aspera/cli/terminal_formatter.rb +9 -3
- data/lib/aspera/cli/transfer_actions.rb +0 -6
- data/lib/aspera/cli/transfer_agent.rb +29 -35
- data/lib/aspera/cli/vault_manager.rb +0 -17
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +4 -2
- data/lib/aspera/command_line_builder.rb +1 -0
- data/lib/aspera/coverage.rb +1 -0
- data/lib/aspera/environment.rb +7 -1
- data/lib/aspera/faspex_gw.rb +2 -1
- data/lib/aspera/faspex_postproc.rb +1 -0
- data/lib/aspera/graphql.rb +5 -5
- data/lib/aspera/json_rpc/client.rb +5 -5
- data/lib/aspera/keychain/encrypted_hash.rb +1 -1
- data/lib/aspera/keychain/one_password_api.rb +1 -1
- data/lib/aspera/link_header.rb +2 -2
- data/lib/aspera/log.rb +22 -25
- data/lib/aspera/markdown.rb +2 -0
- data/lib/aspera/mime.rb +25 -0
- data/lib/aspera/node_simulator.rb +1 -0
- data/lib/aspera/oauth/base.rb +35 -25
- data/lib/aspera/oauth/factory.rb +1 -0
- data/lib/aspera/oauth/generic.rb +1 -1
- data/lib/aspera/oauth/jwt.rb +1 -1
- data/lib/aspera/oauth/web.rb +9 -8
- data/lib/aspera/preview/file_types.rb +4 -4
- data/lib/aspera/preview/generator.rb +7 -0
- data/lib/aspera/preview/options.rb +4 -4
- data/lib/aspera/preview/terminal.rb +4 -3
- data/lib/aspera/preview/utils.rb +9 -6
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/rainbow.rb +7 -0
- data/lib/aspera/rest/aspera_errors.rb +60 -0
- data/lib/aspera/rest/call_error.rb +27 -0
- data/lib/aspera/rest/client.rb +514 -0
- data/lib/aspera/rest/error_analyzer.rb +113 -0
- data/lib/aspera/rest/list.rb +143 -0
- data/lib/aspera/rest/parameters.rb +55 -0
- data/lib/aspera/rest/util.rb +176 -0
- data/lib/aspera/rest.rb +7 -621
- data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +39 -0
- data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
- data/lib/aspera/schema/documentation.rb +13 -3
- data/lib/aspera/schema/registry.rb +18 -1
- data/lib/aspera/schema/validator.rb +92 -0
- data/lib/aspera/secret_hider.rb +36 -25
- data/lib/aspera/string_ext.rb +15 -0
- data/lib/aspera/temp_file_manager.rb +6 -5
- data/lib/aspera/transfer/parameters.rb +2 -0
- data/lib/aspera/transfer/spec.rb +1 -0
- data/lib/aspera/transfer/spec.schema.yaml +1 -0
- data/lib/aspera/uri_reader.rb +11 -11
- data/lib/aspera/web_auth/index.html +147 -0
- data/lib/aspera/web_auth/server.rb +81 -0
- data.tar.gz.sig +0 -0
- metadata +39 -7
- metadata.gz.sig +0 -0
- data/lib/aspera/colors.rb +0 -79
- data/lib/aspera/rest_call_error.rb +0 -25
- data/lib/aspera/rest_error_analyzer.rb +0 -111
- data/lib/aspera/rest_errors_aspera.rb +0 -58
- data/lib/aspera/rest_list.rb +0 -136
- data/lib/aspera/web_auth.rb +0 -211
|
@@ -24,8 +24,10 @@ module Aspera
|
|
|
24
24
|
ALL = (GLOBAL + INSTANCE).freeze
|
|
25
25
|
end
|
|
26
26
|
class << self
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
include OptionDeclarator
|
|
28
|
+
|
|
29
|
+
# Option sources added with `use_options`.
|
|
30
|
+
# @return [Array<Class, Module>]
|
|
29
31
|
def used_option_sources
|
|
30
32
|
@used_option_sources ||= []
|
|
31
33
|
end
|
|
@@ -36,6 +38,8 @@ module Aspera
|
|
|
36
38
|
used_option_sources << source unless used_option_sources.include?(source)
|
|
37
39
|
end
|
|
38
40
|
|
|
41
|
+
# Per-class DSL registry (not inherited: each subclass gets its own instance).
|
|
42
|
+
# @return [CommandRegistry]
|
|
39
43
|
def command_registry
|
|
40
44
|
@command_registry ||= CommandRegistry.new
|
|
41
45
|
end
|
|
@@ -43,25 +47,50 @@ module Aspera
|
|
|
43
47
|
# DSL class method: register a command in this plugin's registry.
|
|
44
48
|
# Inherits parent from the enclosing commands_under block when parent: is omitted.
|
|
45
49
|
# @param id [Symbol]
|
|
46
|
-
# @param kwargs [Hash] forwarded to CommandSpec
|
|
50
|
+
# @param kwargs [Hash] forwarded to [CommandSpec]
|
|
47
51
|
def command(id, **kwargs)
|
|
48
52
|
kwargs[:parent] = @current_parent if kwargs[:parent].nil? && @current_parent
|
|
49
53
|
command_registry.register(CommandSpec.new(id: id, **kwargs))
|
|
50
54
|
end
|
|
51
55
|
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
# Words displayed with specific case in descriptions
|
|
57
|
+
NOUN_WORDS = {'smtp' => 'SMTP', 'ldap' => 'LDAP', 'saml' => 'SAML', 'oauth' => 'OAuth', 'kms' => 'KMS', 'api' => 'API'}.freeze
|
|
58
|
+
private_constant :NOUN_WORDS
|
|
59
|
+
|
|
60
|
+
# Derive a lowercase noun from an entity path, singular unless told otherwise.
|
|
61
|
+
# e.g. 'access_keys' -> 'access key', 'data/smtp_server' -> 'SMTP server'
|
|
62
|
+
# @param entity [String, Symbol] REST path or entity name
|
|
63
|
+
# @param singular [Boolean] Singularize the last word
|
|
64
|
+
# @return [String]
|
|
65
|
+
def entity_noun(entity, singular: true)
|
|
66
|
+
words = entity.to_s.split('/').last.split('_').map { |w| NOUN_WORDS.fetch(w, w) }
|
|
67
|
+
words[-1] = words[-1].sub(/ies\z/, 'y').sub(/(ss|x|sh|ch)es\z/, '\1').sub(/(?<!s)s\z/, '') if singular
|
|
68
|
+
words.join(' ')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Standard description of a CRUD operation on an entity.
|
|
72
|
+
# e.g. (:list, 'access key') -> 'List access keys', (:show, 'access key') -> 'Show access key'
|
|
73
|
+
# @param verb [Symbol] Operation
|
|
74
|
+
# @param noun [String] Singular noun of entity
|
|
75
|
+
# @return [String]
|
|
76
|
+
def operation_description(verb, noun)
|
|
77
|
+
return "#{verb.capitalize} #{noun}" unless verb.eql?(:list)
|
|
78
|
+
plural =
|
|
79
|
+
case noun
|
|
80
|
+
when /[^aeiou]y\z/ then noun.sub(/y\z/, 'ies')
|
|
81
|
+
when /(s|x|sh|ch)\z/ then "#{noun}es"
|
|
82
|
+
else "#{noun}s"
|
|
83
|
+
end
|
|
84
|
+
"List #{plural}"
|
|
57
85
|
end
|
|
58
86
|
|
|
59
87
|
# DSL class method: declare CRUD commands for a REST entity.
|
|
60
88
|
#
|
|
61
89
|
# For each verb in operations:, registers one CommandSpec with:
|
|
62
|
-
# - description:
|
|
63
|
-
# - arguments: [{name:
|
|
90
|
+
# - description: operation_description(verb, name)
|
|
91
|
+
# - arguments: [{name: id_name, type: :identifier, lookup: lookup}] for instance verbs
|
|
64
92
|
# (:show, :modify, :delete) when not a singleton; none for global verbs
|
|
93
|
+
# body of :create and :modify is named after the entity (e.g. <access_key>), passed as data:
|
|
65
94
|
# - action: calls entity_<verb>(api:, entity:, **shared_kwargs, **ctx)
|
|
66
95
|
#
|
|
67
96
|
# api: is resolved at runtime: :@ivar -> instance_variable_get, else -> send.
|
|
@@ -71,14 +100,23 @@ module Aspera
|
|
|
71
100
|
# @param api [Symbol, String] Runtime API ref (:@ivar or method name) or literal string
|
|
72
101
|
# @param entity [String, Symbol] REST sub-path, or ctx key Symbol resolved at runtime
|
|
73
102
|
# @param operations [Array<Symbol>] Verbs to expose; defaults to Operations::ALL
|
|
74
|
-
# @param name [String, nil]
|
|
103
|
+
# @param name [String, nil] Singular display name; defaults to last segment of entity (static only)
|
|
75
104
|
# @param lookup [Symbol, nil] Instance method for percent-selector resolution
|
|
105
|
+
# @param id_name [Symbol, nil] Name of identifier argument; defaults to <name>_id, or id_as_arg field, or :id
|
|
76
106
|
# @param kwargs [Hash] Shared params forwarded to every per-verb method
|
|
77
|
-
def crud_commands(api:, entity:, operations: nil, name: nil, lookup: nil, **kwargs)
|
|
78
|
-
name ||=
|
|
107
|
+
def crud_commands(api:, entity:, operations: nil, name: nil, lookup: nil, id_name: nil, **kwargs)
|
|
108
|
+
name ||= entity_noun(entity, singular: !kwargs[:is_singleton]) unless entity.is_a?(Symbol)
|
|
79
109
|
operations ||= Operations::ALL
|
|
110
|
+
# Body argument is named after the entity, e.g. <access_key>, and passed as data: to entity_<verb>
|
|
111
|
+
data_name = name ? name.downcase.tr(' ', '_').to_sym : :data
|
|
112
|
+
# Identifier argument is named after the entity, e.g. <access_key_id>, and passed as id: to entity_<verb>
|
|
113
|
+
id_name ||=
|
|
114
|
+
if kwargs[:id_as_arg].is_a?(String) then kwargs[:id_as_arg].to_sym
|
|
115
|
+
elsif name then :"#{data_name}_id"
|
|
116
|
+
else :id
|
|
117
|
+
end
|
|
80
118
|
operations.each do |verb|
|
|
81
|
-
id_arg = ({name:
|
|
119
|
+
id_arg = ({name: id_name, type: :identifier, lookup: lookup} if Operations::INSTANCE.include?(verb) && !kwargs[:is_singleton])
|
|
82
120
|
schema_val =
|
|
83
121
|
if kwargs[:body_component] && entity.is_a?(String)
|
|
84
122
|
case verb
|
|
@@ -89,9 +127,9 @@ module Aspera
|
|
|
89
127
|
args =
|
|
90
128
|
case verb
|
|
91
129
|
when :create
|
|
92
|
-
[{name:
|
|
130
|
+
[{name: data_name, type: Hash, bulk: true, schema: schema_val}]
|
|
93
131
|
when :modify
|
|
94
|
-
[id_arg, {name:
|
|
132
|
+
[id_arg, {name: data_name, type: Hash, schema: schema_val}].compact
|
|
95
133
|
when :delete
|
|
96
134
|
id_arg ? [id_arg.merge(bulk: true)] : nil
|
|
97
135
|
else
|
|
@@ -107,10 +145,13 @@ module Aspera
|
|
|
107
145
|
api
|
|
108
146
|
end
|
|
109
147
|
resolved_entity = entity.is_a?(Symbol) ? ctx.fetch(entity) : entity
|
|
148
|
+
ctx = ctx.merge(data: ctx[data_name]) if ctx.key?(data_name)
|
|
149
|
+
ctx = ctx.merge(id: ctx[id_name]) if ctx.key?(id_name)
|
|
110
150
|
send(:"entity_#{verb}", api: resolved_api, entity: resolved_entity, **kwargs, **ctx)
|
|
111
151
|
end
|
|
112
|
-
cmd_attrs = {description:
|
|
152
|
+
cmd_attrs = {description: operation_description(verb, name || entity.inspect), action: action_proc}
|
|
113
153
|
cmd_attrs[:arguments] = args if args
|
|
154
|
+
cmd_attrs[:query_schema] = Schema::Registry.query_params(kwargs[:query_component], entity) if verb.eql?(:list) && kwargs[:query_component] && entity.is_a?(String)
|
|
114
155
|
command(verb, **cmd_attrs)
|
|
115
156
|
end
|
|
116
157
|
end
|
|
@@ -139,7 +180,7 @@ module Aspera
|
|
|
139
180
|
path = Array(@current_parent) + Array(parent)
|
|
140
181
|
unless command_registry[path]
|
|
141
182
|
id = path.last
|
|
142
|
-
desc = description || "Manage #{
|
|
183
|
+
desc = description || "Manage #{entity_noun(id, singular: false)}"
|
|
143
184
|
parent_path = path[0..-2]
|
|
144
185
|
saved = @current_parent
|
|
145
186
|
@current_parent = parent_path.empty? ? nil : parent_path
|
|
@@ -153,101 +194,55 @@ module Aspera
|
|
|
153
194
|
@current_parent = previous
|
|
154
195
|
end
|
|
155
196
|
|
|
156
|
-
#
|
|
197
|
+
# Options of this plugin class (`option` DSL, see OptionDeclarator).
|
|
157
198
|
# Metadata is stored as an OptionSpec at class-load time; the actual
|
|
158
199
|
# options.declare call happens in Base#initialize once the instance exists.
|
|
159
|
-
#
|
|
200
|
+
# @return [Hash{Symbol => OptionSpec}]
|
|
201
|
+
def option_specs
|
|
202
|
+
command_registry.option_specs
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Store an OptionSpec in the command registry.
|
|
160
206
|
# Raises ArgumentError at class-load time if the same option name is already
|
|
161
207
|
# declared by any ancestor class, preventing silent shadowing.
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
# @param description [String, nil] User-facing description; if nil, derived from schema: title/description
|
|
165
|
-
# @param short [String, nil] Single-character short form (without leading '-')
|
|
166
|
-
# @param allowed [Object, nil] Allowed values (see OptionValue)
|
|
167
|
-
# @param default [Object, nil] Default value
|
|
168
|
-
# @param handler [Symbol, Hash, nil]
|
|
169
|
-
# - Symbol: resolved to {o: <plugin instance>, m: <symbol>} at runtime (Category B)
|
|
170
|
-
# - Hash: {o: <object>, m: <method>} used as-is (Category A: singletons / constants)
|
|
171
|
-
# - nil: option stores its value locally (no delegation)
|
|
172
|
-
# @param deprecation [String, nil] Deprecation message forwarded to options.declare
|
|
173
|
-
# @param schema [String, nil] Schema reference (e.g. "opts:components.schemas.Foo");
|
|
174
|
-
# when description: is nil, the schema title or first description line is used
|
|
175
|
-
def option(name, description: nil,
|
|
176
|
-
short: nil, allowed: nil, default: nil,
|
|
177
|
-
handler: nil, deprecation: nil, schema: nil)
|
|
208
|
+
# @param spec [OptionSpec]
|
|
209
|
+
def register_option_spec(spec)
|
|
178
210
|
ancestor_owner = ancestors.drop(1).find do |klass|
|
|
179
211
|
klass.is_a?(Class) && klass <= Base &&
|
|
180
212
|
klass.instance_variable_defined?(:@command_registry) &&
|
|
181
|
-
klass.command_registry.option_specs.key?(name)
|
|
213
|
+
klass.command_registry.option_specs.key?(spec.name)
|
|
182
214
|
end
|
|
183
|
-
raise ArgumentError, "#{self}: option :#{name} already declared in ancestor #{ancestor_owner}" if ancestor_owner
|
|
184
|
-
command_registry.register_option(
|
|
185
|
-
OptionSpec.new(
|
|
186
|
-
name: name,
|
|
187
|
-
description: description,
|
|
188
|
-
short: short,
|
|
189
|
-
allowed: allowed,
|
|
190
|
-
default: default,
|
|
191
|
-
handler: handler,
|
|
192
|
-
deprecation: deprecation,
|
|
193
|
-
schema: schema
|
|
194
|
-
)
|
|
195
|
-
)
|
|
215
|
+
raise ArgumentError, "#{self}: option :#{spec.name} already declared in ancestor #{ancestor_owner}" if ancestor_owner
|
|
216
|
+
command_registry.register_option(spec)
|
|
196
217
|
end
|
|
197
218
|
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
# @
|
|
201
|
-
|
|
202
|
-
def declare_options(options, parse: false)
|
|
219
|
+
# Classes and modules whose options apply to this plugin:
|
|
220
|
+
# this class, its plugin ancestors and sources added via `use_options`.
|
|
221
|
+
# @return [Array<Class, Module>] each responds to `option_specs`
|
|
222
|
+
def option_sources
|
|
203
223
|
sources = []
|
|
204
224
|
ancestors.each do |klass|
|
|
205
225
|
next unless klass.is_a?(Class) && klass <= Base
|
|
206
226
|
sources << klass if klass.instance_variable_defined?(:@command_registry)
|
|
207
|
-
sources.concat(klass.used_option_sources)
|
|
227
|
+
sources.concat(klass.used_option_sources)
|
|
208
228
|
end
|
|
209
|
-
sources.uniq
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
case spec.handler
|
|
222
|
-
when Hash then spec.handler
|
|
223
|
-
end
|
|
224
|
-
options.declare(
|
|
225
|
-
spec.name,
|
|
226
|
-
description: spec.description,
|
|
227
|
-
short: spec.short,
|
|
228
|
-
allowed: spec.allowed,
|
|
229
|
-
default: spec.default,
|
|
230
|
-
handler: resolved_handler,
|
|
231
|
-
deprecation: spec.deprecation,
|
|
232
|
-
schema: spec.schema
|
|
233
|
-
)
|
|
229
|
+
sources.uniq
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Declare all options of `option_sources` onto a Parser instance.
|
|
233
|
+
# Skips options already declared on the parser: it is shared across all plugins in a run.
|
|
234
|
+
# @param options [Aspera::Cli::Parser]
|
|
235
|
+
# @param target [Base, nil] plugin instance for Symbol and Proc `on_set` callbacks; nil: such callbacks are not bound
|
|
236
|
+
# @param parse [Boolean] whether to call parse_options! after declaring
|
|
237
|
+
def declare_options(options, target: nil, parse: false)
|
|
238
|
+
option_sources.each do |src|
|
|
239
|
+
src.option_specs.each_value do |spec|
|
|
240
|
+
spec.declare_on(options, target: target) unless options.option_declared?(spec.name)
|
|
234
241
|
end
|
|
235
242
|
end
|
|
236
243
|
options.parse_options! if parse
|
|
237
244
|
end
|
|
238
245
|
|
|
239
|
-
# DSL class method: declare a setup method to run once before root dispatch.
|
|
240
|
-
# The method is called before any command is consumed, and its return value
|
|
241
|
-
# (a Hash) is merged into the initial ctx. This is useful when conditions
|
|
242
|
-
# on root commands depend on state built during setup (e.g. @connection_type).
|
|
243
|
-
# @param method_name [Symbol]
|
|
244
|
-
def root_setup(method_name)
|
|
245
|
-
@root_setup_method = method_name
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# @return [Symbol, nil]
|
|
249
|
-
attr_reader :root_setup_method
|
|
250
|
-
|
|
251
246
|
# DSL class method: declare the human-readable application name shown in wizards.
|
|
252
247
|
# When called with an argument, sets the name. When called with no argument, returns it.
|
|
253
248
|
# Falls back to the last component of the class name if never set.
|
|
@@ -289,49 +284,9 @@ module Aspera
|
|
|
289
284
|
# below (DSL-registered and imperative) appear under the plugin section in
|
|
290
285
|
# --help output, separate from the global options.
|
|
291
286
|
options.group(self.class.name.split('::').last.downcase) if @context.man_header
|
|
292
|
-
# Auto-declare all options registered via the DSL `option` class method
|
|
293
|
-
#
|
|
294
|
-
|
|
295
|
-
# The options object is shared across all plugins in a run; skip options already
|
|
296
|
-
# declared by an earlier plugin (Base.option prevents duplicates within one hierarchy).
|
|
297
|
-
# Each OptionSpec is translated to an options.declare call, resolving the
|
|
298
|
-
# handler: shorthand:
|
|
299
|
-
# Symbol handler: {o: self, m: <symbol>} (Category B - plugin instance methods)
|
|
300
|
-
# Hash handler: used as-is (Category A - singletons / class constants)
|
|
301
|
-
sources = []
|
|
302
|
-
self.class.ancestors.each do |klass|
|
|
303
|
-
next unless klass.is_a?(Class) && klass <= Base
|
|
304
|
-
sources << klass if klass.instance_variable_defined?(:@command_registry)
|
|
305
|
-
sources.concat(klass.used_option_sources) if klass.respond_to?(:used_option_sources)
|
|
306
|
-
end
|
|
307
|
-
sources.uniq.each do |src|
|
|
308
|
-
specs =
|
|
309
|
-
if src.respond_to?(:command_registry)
|
|
310
|
-
src.command_registry.option_specs
|
|
311
|
-
elsif src.respond_to?(:option_specs)
|
|
312
|
-
src.option_specs
|
|
313
|
-
else
|
|
314
|
-
{}
|
|
315
|
-
end
|
|
316
|
-
specs.each_value do |spec|
|
|
317
|
-
next if options.option_declared?(spec.name)
|
|
318
|
-
resolved_handler =
|
|
319
|
-
case spec.handler
|
|
320
|
-
when Symbol then {o: self, m: spec.handler}
|
|
321
|
-
when Hash then spec.handler
|
|
322
|
-
end
|
|
323
|
-
options.declare(
|
|
324
|
-
spec.name,
|
|
325
|
-
description: spec.description,
|
|
326
|
-
short: spec.short,
|
|
327
|
-
allowed: spec.allowed,
|
|
328
|
-
default: spec.default,
|
|
329
|
-
handler: resolved_handler,
|
|
330
|
-
deprecation: spec.deprecation,
|
|
331
|
-
schema: spec.schema
|
|
332
|
-
)
|
|
333
|
-
end
|
|
334
|
-
end
|
|
287
|
+
# Auto-declare all options registered via the DSL `option` class method,
|
|
288
|
+
# including those of parent plugin classes (e.g. Oauth, BasicAuth) and `use_options` sources.
|
|
289
|
+
self.class.declare_options(options, target: self)
|
|
335
290
|
end
|
|
336
291
|
|
|
337
292
|
# Global objects
|
|
@@ -365,38 +320,25 @@ module Aspera
|
|
|
365
320
|
# Entry point for all DSL-based plugins.
|
|
366
321
|
def execute_action
|
|
367
322
|
@help_path = nil
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
unless self.class.instance_variable_defined?(:@registry_validated)
|
|
371
|
-
self.class.command_registry.validate!(plugin_class: self.class)
|
|
372
|
-
self.class.instance_variable_set(:@registry_validated, true)
|
|
373
|
-
end
|
|
374
|
-
# Run the root setup (if declared) before consuming any argument.
|
|
375
|
-
# This ensures condition methods on root commands can read instance variables
|
|
376
|
-
# populated by the setup (e.g. @connection_type in server.rb).
|
|
377
|
-
init_ctx = {}
|
|
378
|
-
if (rsm = self.class.root_setup_method)
|
|
379
|
-
init_ctx = send(rsm) || {}
|
|
380
|
-
end
|
|
381
|
-
dispatch_from_registry([], init_ctx)
|
|
323
|
+
validate_registry
|
|
324
|
+
dispatch_from_registry([])
|
|
382
325
|
end
|
|
383
326
|
|
|
384
327
|
# Two-phase dispatcher: run setup on the current node (Phase A), then either
|
|
385
328
|
# execute a leaf directly or consume the next argument and recurse (Phase B).
|
|
386
329
|
# @param current_path [Array<Symbol>] path of the node currently being dispatched
|
|
387
330
|
# @param ctx [Hash] accumulated context passed down from parent nodes
|
|
388
|
-
# @param skip_setup [Boolean] when true, skip Phase A (setup already done by caller)
|
|
389
331
|
# @return [Object] result suitable for CLI output
|
|
390
|
-
def dispatch_from_registry(current_path, ctx = {}
|
|
332
|
+
def dispatch_from_registry(current_path, ctx = {})
|
|
391
333
|
registry = self.class.command_registry
|
|
392
334
|
spec = registry[current_path]
|
|
393
335
|
is_leaf = spec && registry.children_of(current_path).empty?
|
|
394
336
|
|
|
395
|
-
if
|
|
337
|
+
if @context.help_requested
|
|
396
338
|
# help_requested on an intermediate node: drain positional args without validation
|
|
397
339
|
# so that dispatch_child can still consume the correct sub-command token
|
|
398
|
-
if !is_leaf
|
|
399
|
-
|
|
340
|
+
if !is_leaf
|
|
341
|
+
registry.arguments_at(current_path).each do |arg_spec|
|
|
400
342
|
next if ctx.key?(arg_spec.name)
|
|
401
343
|
options.get_next_argument(arg_spec.name.to_s, mandatory: false)
|
|
402
344
|
end
|
|
@@ -404,24 +346,7 @@ module Aspera
|
|
|
404
346
|
else
|
|
405
347
|
# Phase A - for intermediate nodes only: resolve all ArgumentSpec declared on this node
|
|
406
348
|
# before dispatching to children (leaf nodes resolve their arguments inside execute_leaf).
|
|
407
|
-
if !is_leaf
|
|
408
|
-
(spec&.arguments || []).each do |arg_spec|
|
|
409
|
-
next if ctx.key?(arg_spec.name)
|
|
410
|
-
if arg_spec.type.eql?(:identifier)
|
|
411
|
-
lookup_cb = arg_spec.lookup
|
|
412
|
-
res_id = if lookup_cb.nil?
|
|
413
|
-
options.instance_identifier(description: arg_spec.name.to_s)
|
|
414
|
-
elsif lookup_cb.is_a?(Symbol)
|
|
415
|
-
options.instance_identifier(description: arg_spec.name.to_s) { |f, v| send(lookup_cb, f, v, **ctx) }
|
|
416
|
-
else
|
|
417
|
-
options.instance_identifier(description: arg_spec.name.to_s) { |f, v| instance_exec(f, v, **ctx, &lookup_cb) }
|
|
418
|
-
end
|
|
419
|
-
ctx = ctx.merge(arg_spec.name => res_id)
|
|
420
|
-
else
|
|
421
|
-
ctx = ctx.merge(arg_spec.name => resolve_argument(arg_spec))
|
|
422
|
-
end
|
|
423
|
-
end
|
|
424
|
-
end
|
|
349
|
+
ctx = resolve_arguments(spec.arguments, ctx) if !is_leaf && spec&.arguments
|
|
425
350
|
ctx = ctx.merge(send(spec.setup, **ctx)) if spec&.setup
|
|
426
351
|
end
|
|
427
352
|
|
|
@@ -440,15 +365,15 @@ module Aspera
|
|
|
440
365
|
# @param ctx [Hash]
|
|
441
366
|
# @return [Object]
|
|
442
367
|
def dispatch_leaf(current_path, spec, ctx)
|
|
443
|
-
if
|
|
368
|
+
if @context.help_requested
|
|
444
369
|
@help_path = current_path
|
|
445
370
|
raise Cli::HelpRequest, self
|
|
446
371
|
end
|
|
447
372
|
execute_leaf(spec, ctx)
|
|
448
373
|
end
|
|
449
374
|
|
|
450
|
-
# Phase B, child branch: consume the next command argument,
|
|
451
|
-
#
|
|
375
|
+
# Phase B, child branch: consume the next command argument, then either continue
|
|
376
|
+
# on a mounted plugin instance, or recurse into the child.
|
|
452
377
|
# --help is intercepted at two points:
|
|
453
378
|
# 1. Before get_next_command when no positional arg is pending: raises HelpRequest
|
|
454
379
|
# immediately so the subcommand list with descriptions is shown rather than a
|
|
@@ -461,37 +386,63 @@ module Aspera
|
|
|
461
386
|
# @return [Object]
|
|
462
387
|
def dispatch_child(current_path, registry, ctx)
|
|
463
388
|
children = registry.children_of(current_path)
|
|
464
|
-
|
|
389
|
+
# condition: methods belong to the class declaring the spec: only evaluate local ones
|
|
390
|
+
# (mounted children are only walked here for --help, see below).
|
|
391
|
+
# With --help, conditions are not evaluated: they may need the API, which is not built for help.
|
|
392
|
+
available = children.reject { |id, c| c.condition && !@context.help_requested && registry.local?(current_path + [id]) && !send(c.condition) }
|
|
465
393
|
aliases = children.values.each_with_object({}) do |c, h|
|
|
466
394
|
Array(c.aliases).each { |a| h[a] = c.id } if c.aliases
|
|
467
395
|
end
|
|
468
396
|
|
|
469
397
|
# Intercept --help before consuming the command token when no arg is pending.
|
|
470
398
|
# This avoids MissingArgument being raised by get_next_command before HelpRequest.
|
|
471
|
-
if
|
|
399
|
+
if @context.help_requested && options.command_or_arg_empty?
|
|
472
400
|
@help_path = current_path
|
|
473
401
|
raise Cli::HelpRequest, self
|
|
474
402
|
end
|
|
475
403
|
|
|
476
404
|
command = options.get_next_command(available.keys, aliases: aliases.empty? ? nil : aliases)
|
|
477
|
-
child = available[command]
|
|
478
405
|
|
|
479
406
|
# Intercept --help after a command was consumed but no further args remain.
|
|
480
407
|
# (e.g. `aoc files find -h`). When further args remain, keep recursing.
|
|
481
|
-
if
|
|
408
|
+
if @context.help_requested && options.command_or_arg_empty?
|
|
482
409
|
@help_path = current_path + [command]
|
|
483
410
|
raise Cli::HelpRequest, self
|
|
484
411
|
end
|
|
485
412
|
|
|
486
|
-
#
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return dispatch_from_registry(Array(child.delegates_to), ctx) if child.delegates_to
|
|
413
|
+
# Mounted child: continue on the target plugin instance, in its own namespace.
|
|
414
|
+
# For --help, keep walking the (mount-aware) registry of this class instead, so that
|
|
415
|
+
# no target instance (and thus no API connection) is needed.
|
|
416
|
+
child_path = current_path + [command]
|
|
417
|
+
return dispatch_mount(registry.mount_of(current_path), command, ctx) unless @context.help_requested || registry.local?(child_path)
|
|
492
418
|
|
|
493
|
-
# Both intermediate and leaf:
|
|
494
|
-
dispatch_from_registry(
|
|
419
|
+
# Both intermediate and leaf: arguments + setup are handled by Phase A of the next call
|
|
420
|
+
dispatch_from_registry(child_path, ctx)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Hand over dispatch of a mounted child to the target plugin instance.
|
|
424
|
+
# Setups of the mount point `at` and of its ancestors in the target are not executed:
|
|
425
|
+
# the seed ctx returned by the host's `instance` method replaces them.
|
|
426
|
+
# The mount's own arguments (if any) are read first, and passed to `instance` in ctx.
|
|
427
|
+
# @param mount [MountSpec]
|
|
428
|
+
# @param command [Symbol] mounted child id, already consumed
|
|
429
|
+
# @param ctx [Hash] host context, passed to the `instance` method
|
|
430
|
+
# @return [Object]
|
|
431
|
+
def dispatch_mount(mount, command, ctx)
|
|
432
|
+
ctx = resolve_arguments(mount.arguments, ctx)
|
|
433
|
+
target = send(mount.instance, **ctx)
|
|
434
|
+
target, seed = target if target.is_a?(Array)
|
|
435
|
+
Aspera.assert_type(target, mount.plugin)
|
|
436
|
+
target.validate_registry
|
|
437
|
+
target.dispatch_from_registry(mount.at + [command], seed || {})
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Validate the registry once per class (memoised by the ivar check).
|
|
441
|
+
# Passes the plugin class so implicit action methods can be verified.
|
|
442
|
+
def validate_registry
|
|
443
|
+
return if self.class.instance_variable_defined?(:@registry_validated)
|
|
444
|
+
self.class.command_registry.validate!(plugin_class: self.class)
|
|
445
|
+
self.class.instance_variable_set(:@registry_validated, true)
|
|
495
446
|
end
|
|
496
447
|
|
|
497
448
|
# Resolve the action for a leaf CommandSpec.
|
|
@@ -520,10 +471,8 @@ module Aspera
|
|
|
520
471
|
end
|
|
521
472
|
|
|
522
473
|
# Execute a leaf CommandSpec: resolve arguments and call action.
|
|
523
|
-
# Arguments already present in
|
|
524
|
-
#
|
|
525
|
-
# instance_arg (if any) is resolved here as an ArgumentSpec(type: :identifier) and merged
|
|
526
|
-
# into ctx, exactly like any other keyword argument received by the action.
|
|
474
|
+
# Arguments already present in `ctx` (e.g. provided by a caller or a mount seed) are skipped:
|
|
475
|
+
# they are not read again from the command line.
|
|
527
476
|
# @param spec [CommandSpec] a leaf node (no children)
|
|
528
477
|
# @param ctx [Hash] accumulated context (pre-resolved keys are not re-consumed)
|
|
529
478
|
# @return [Object]
|
|
@@ -531,21 +480,30 @@ module Aspera
|
|
|
531
480
|
a = action_for(spec)
|
|
532
481
|
# Always resolve declared arguments (even when transfer_paths is set — those arguments
|
|
533
482
|
# are consumed first; ts_source_paths then reads whatever remains in the queue).
|
|
534
|
-
(spec.arguments
|
|
483
|
+
ctx = resolve_arguments(spec.arguments, ctx) if spec.arguments
|
|
484
|
+
invoke_action(a, [], ctx)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Resolve positional arguments from the CLI argument stream, in order.
|
|
488
|
+
# Arguments already present in `ctx` are not read again.
|
|
489
|
+
# For type: :identifier, the percent-selector lookup receives the ctx accumulated so far.
|
|
490
|
+
# @param arg_specs [Array<ArgumentSpec>]
|
|
491
|
+
# @param ctx [Hash] accumulated context
|
|
492
|
+
# @return [Hash] ctx merged with the resolved arguments
|
|
493
|
+
def resolve_arguments(arg_specs, ctx)
|
|
494
|
+
arg_specs.each do |arg_spec|
|
|
535
495
|
next if ctx.key?(arg_spec.name)
|
|
536
|
-
if arg_spec.type.eql?(:identifier)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
ctx = ctx.merge(arg_spec.name => resolve_argument(arg_spec))
|
|
546
|
-
end
|
|
496
|
+
lookup_cb = arg_spec.lookup if arg_spec.type.eql?(:identifier)
|
|
497
|
+
current = ctx
|
|
498
|
+
block =
|
|
499
|
+
case lookup_cb
|
|
500
|
+
when nil then nil
|
|
501
|
+
when Symbol then ->(f, v) { send(lookup_cb, f, v, **current) }
|
|
502
|
+
else ->(f, v) { instance_exec(f, v, **current, &lookup_cb) }
|
|
503
|
+
end
|
|
504
|
+
ctx = ctx.merge(arg_spec.name => resolve_argument(arg_spec, &block))
|
|
547
505
|
end
|
|
548
|
-
|
|
506
|
+
ctx
|
|
549
507
|
end
|
|
550
508
|
|
|
551
509
|
# Resolve a single positional argument from the CLI argument stream.
|
|
@@ -603,17 +561,18 @@ module Aspera
|
|
|
603
561
|
# @param path [Array<Symbol>] starting path ([] for the full tree)
|
|
604
562
|
# @return [Hash] { command_id => { description:, condition:, children: } }
|
|
605
563
|
def generate_help(path = [])
|
|
606
|
-
self.class.command_registry.children_of(path).
|
|
564
|
+
self.class.command_registry.children_of(path).to_h do |id, child_spec|
|
|
607
565
|
annotation = child_spec.condition ? " [#{child_spec.condition}]" : ''
|
|
608
|
-
|
|
566
|
+
# path + [id], not child_spec.full_path: a mounted spec's full_path is in the target namespace
|
|
567
|
+
[id, {
|
|
609
568
|
description: "#{child_spec.description}#{annotation}",
|
|
610
569
|
condition: child_spec.condition,
|
|
611
|
-
children: generate_help(
|
|
612
|
-
}
|
|
570
|
+
children: generate_help(path + [id])
|
|
571
|
+
}]
|
|
613
572
|
end
|
|
614
573
|
end
|
|
615
574
|
|
|
616
|
-
# Convenience wrapper: reads :bulk and :bfail from options,
|
|
575
|
+
# Convenience wrapper: reads :bulk and :bfail from options, normalizes `items`
|
|
617
576
|
# to an Array, then delegates to Result.bulk.
|
|
618
577
|
# Use this in action methods instead of the three-line boilerplate:
|
|
619
578
|
# is_bulk = options.get_option(:bulk)
|
|
@@ -623,7 +582,7 @@ module Aspera
|
|
|
623
582
|
# @param command [Symbol] Operation name (:create, :delete, ...)
|
|
624
583
|
# @param id_result [String] Key used as item identifier in the result row
|
|
625
584
|
# @param fields [Object] Fields hint passed to Result constructor (non-bulk only)
|
|
626
|
-
# @yieldparam item [Object] Each item in
|
|
585
|
+
# @yieldparam item [Object] Each item in `items`
|
|
627
586
|
# @return [Result::ObjectList, Result::SingleObject]
|
|
628
587
|
def bulk_result(items, command:, id_result: 'id', fields: :default, &block)
|
|
629
588
|
items = items.is_a?(Array) ? items : [items]
|
|
@@ -645,7 +604,7 @@ module Aspera
|
|
|
645
604
|
# NOT read from the CLI queue inside the method.
|
|
646
605
|
|
|
647
606
|
# List all instances of an entity.
|
|
648
|
-
# @param api [Aspera::Rest] REST API object
|
|
607
|
+
# @param api [Aspera::Rest::Client] REST API object
|
|
649
608
|
# @param entity [String] API sub-path
|
|
650
609
|
# @param display_fields [Array, nil] Fields to display
|
|
651
610
|
# @param items_key [String, nil] Sub-key in response containing the array
|
|
@@ -655,12 +614,14 @@ module Aspera
|
|
|
655
614
|
qs_path = query_component ? Schema::Registry.query_params(query_component, entity) : nil
|
|
656
615
|
data, http = api.read(entity, query_read_delete(default: list_query, schema: qs_path), ret: :both)
|
|
657
616
|
return Result::Empty.new if http.code == '204'
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
617
|
+
if !data.is_a?(Hash)
|
|
618
|
+
# already the list
|
|
619
|
+
elsif items_key
|
|
620
|
+
data = data[items_key]
|
|
621
|
+
elsif http['Content-Type'].start_with?(Mime::JSON_API)
|
|
622
|
+
# JSON:API: list is under the entity name
|
|
661
623
|
data = data[entity]
|
|
662
624
|
end
|
|
663
|
-
data = data[items_key] if items_key
|
|
664
625
|
case data
|
|
665
626
|
when Hash then Result::SingleObject.new(data, fields: display_fields)
|
|
666
627
|
when Array
|
|
@@ -671,7 +632,7 @@ module Aspera
|
|
|
671
632
|
end
|
|
672
633
|
|
|
673
634
|
# Show one instance of an entity.
|
|
674
|
-
# @param api [Aspera::Rest] REST API object
|
|
635
|
+
# @param api [Aspera::Rest::Client] REST API object
|
|
675
636
|
# @param entity [String] API sub-path
|
|
676
637
|
# @param id [String, nil] Resource identifier; nil when is_singleton: true
|
|
677
638
|
# @param display_fields [Array, nil] Fields to display
|
|
@@ -683,43 +644,32 @@ module Aspera
|
|
|
683
644
|
end
|
|
684
645
|
|
|
685
646
|
# Create one or more instances of an entity (supports bulk).
|
|
686
|
-
# @param api [Aspera::Rest]
|
|
687
|
-
# @param entity [String]
|
|
688
|
-
# @param
|
|
689
|
-
# @param
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
input_data ||= data
|
|
694
|
-
unless input_data
|
|
695
|
-
is_bulk = options.get_option(:bulk)
|
|
696
|
-
raw = options.get_next_argument('data', validation: is_bulk ? Array : Hash, schema: schema)
|
|
697
|
-
input_data = is_bulk ? raw : [raw]
|
|
698
|
-
end
|
|
699
|
-
input_data = [input_data] unless input_data.is_a?(Array)
|
|
700
|
-
bulk_result(input_data, command: :create, fields: display_fields) do |params|
|
|
647
|
+
# @param api [Aspera::Rest::Client] REST API object
|
|
648
|
+
# @param entity [String] API sub-path
|
|
649
|
+
# @param data [Hash, Array<Hash>] Entity data (Array with bulk), from the command's declared `data` argument
|
|
650
|
+
# @param display_fields [Array, nil] Fields to display
|
|
651
|
+
def entity_create(api:, entity:, data:, display_fields: nil, **)
|
|
652
|
+
data = [data] unless data.is_a?(Array)
|
|
653
|
+
bulk_result(data, command: :create, fields: display_fields) do |params|
|
|
701
654
|
api.create(entity, params)
|
|
702
655
|
end
|
|
703
656
|
end
|
|
704
657
|
|
|
705
658
|
# Modify an existing instance of an entity.
|
|
706
|
-
# @param api [Aspera::Rest] REST API object
|
|
659
|
+
# @param api [Aspera::Rest::Client] REST API object
|
|
707
660
|
# @param entity [String] API sub-path
|
|
708
661
|
# @param id [String, nil] Resource identifier; nil when is_singleton: true
|
|
709
662
|
# @param is_singleton [Boolean] When true, entity is the full path (no id appended)
|
|
710
663
|
# @param id_as_arg [Boolean, String] When set, id is appended as ?<id_as_arg>=<id>
|
|
711
|
-
# @param
|
|
712
|
-
|
|
713
|
-
def entity_modify(api:, entity:, id: nil, is_singleton: false, id_as_arg: false, body_component: nil, input_data: nil, data: nil, **)
|
|
714
|
-
schema = body_component ? Schema::Registry.req_body(body_component, "#{entity}/{id}.put") : nil
|
|
664
|
+
# @param data [Hash] Modified fields, from the command's declared `data` argument
|
|
665
|
+
def entity_modify(api:, entity:, data:, id: nil, is_singleton: false, id_as_arg: false, **)
|
|
715
666
|
path = entity_res_path(entity, id, is_singleton: is_singleton, id_as_arg: id_as_arg)
|
|
716
|
-
|
|
717
|
-
api.update(path, parameters)
|
|
667
|
+
api.update(path, data)
|
|
718
668
|
Result::Status.new('modified')
|
|
719
669
|
end
|
|
720
670
|
|
|
721
671
|
# Delete one or more instances of an entity (supports bulk).
|
|
722
|
-
# @param api [Aspera::Rest] REST API object
|
|
672
|
+
# @param api [Aspera::Rest::Client] REST API object
|
|
723
673
|
# @param entity [String] API sub-path
|
|
724
674
|
# @param id [String, Array, nil] Resource identifier(s)
|
|
725
675
|
# @param id_as_arg [Boolean, String] When set, id is appended as ?<id_as_arg>=<id>
|