aspera-cli 4.27.2 → 4.27.4

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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +133 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/bin/ascli +3 -1
  6. data/docs/README.md +1006 -766
  7. data/lib/aspera/agent/base.rb +7 -2
  8. data/lib/aspera/agent/connect.rb +6 -8
  9. data/lib/aspera/agent/desktop.rb +2 -6
  10. data/lib/aspera/agent/direct.rb +52 -22
  11. data/lib/aspera/agent/node.rb +9 -8
  12. data/lib/aspera/agent/transferd.rb +2 -2
  13. data/lib/aspera/api/alee.rb +1 -1
  14. data/lib/aspera/api/aoc.rb +14 -12
  15. data/lib/aspera/api/ats.rb +1 -1
  16. data/lib/aspera/api/cos_node.rb +2 -2
  17. data/lib/aspera/api/faspex.rb +11 -7
  18. data/lib/aspera/api/httpgw.rb +38 -35
  19. data/lib/aspera/api/node.rb +39 -34
  20. data/lib/aspera/ascmd.rb +3 -1
  21. data/lib/aspera/ascp/installation.rb +63 -28
  22. data/lib/aspera/ascp/management.rb +1 -0
  23. data/lib/aspera/assert.rb +4 -0
  24. data/lib/aspera/cli/ascp_actions.rb +20 -41
  25. data/lib/aspera/cli/async_transfer_store.rb +12 -11
  26. data/lib/aspera/cli/bootstrapper.rb +14 -16
  27. data/lib/aspera/cli/command_line.rb +252 -0
  28. data/lib/aspera/cli/command_registry.rb +215 -37
  29. data/lib/aspera/cli/command_spec.rb +104 -15
  30. data/lib/aspera/cli/completion/ascli.bash +12 -0
  31. data/lib/aspera/cli/completion/ascli.fish +16 -0
  32. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  33. data/lib/aspera/cli/context.rb +3 -0
  34. data/lib/aspera/cli/deprecation.rb +37 -0
  35. data/lib/aspera/cli/extended_value.rb +6 -3
  36. data/lib/aspera/cli/formatter.rb +94 -80
  37. data/lib/aspera/cli/gem_checker.rb +1 -1
  38. data/lib/aspera/cli/hints.rb +7 -6
  39. data/lib/aspera/cli/http.rb +22 -34
  40. data/lib/aspera/cli/info.rb +3 -0
  41. data/lib/aspera/cli/mcp_tool.rb +47 -83
  42. data/lib/aspera/cli/option_declarator.rb +33 -42
  43. data/lib/aspera/cli/option_registry.rb +69 -0
  44. data/lib/aspera/cli/option_types.rb +105 -0
  45. data/lib/aspera/cli/option_value.rb +278 -0
  46. data/lib/aspera/cli/options.schema.yaml +124 -15
  47. data/lib/aspera/cli/parser.rb +333 -862
  48. data/lib/aspera/cli/plugins/alee.rb +7 -4
  49. data/lib/aspera/cli/plugins/aoc.rb +545 -518
  50. data/lib/aspera/cli/plugins/ats.rb +59 -80
  51. data/lib/aspera/cli/plugins/base.rb +221 -265
  52. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  53. data/lib/aspera/cli/plugins/config.rb +263 -184
  54. data/lib/aspera/cli/plugins/console.rb +103 -39
  55. data/lib/aspera/cli/plugins/cos.rb +6 -23
  56. data/lib/aspera/cli/plugins/factory.rb +3 -0
  57. data/lib/aspera/cli/plugins/faspex5.rb +204 -182
  58. data/lib/aspera/cli/plugins/faspio.rb +6 -11
  59. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  60. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  61. data/lib/aspera/cli/plugins/node.rb +300 -327
  62. data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
  63. data/lib/aspera/cli/plugins/preview.rb +96 -105
  64. data/lib/aspera/cli/plugins/server.rb +78 -53
  65. data/lib/aspera/cli/plugins/shares.rb +80 -131
  66. data/lib/aspera/cli/preset_actions.rb +44 -27
  67. data/lib/aspera/cli/preset_manager.rb +44 -19
  68. data/lib/aspera/cli/prompt.rb +36 -0
  69. data/lib/aspera/cli/result.rb +42 -36
  70. data/lib/aspera/cli/runner.rb +32 -59
  71. data/lib/aspera/cli/special_values.rb +5 -0
  72. data/lib/aspera/cli/sync_actions.rb +51 -46
  73. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  74. data/lib/aspera/cli/transfer_actions.rb +14 -9
  75. data/lib/aspera/cli/transfer_agent.rb +34 -38
  76. data/lib/aspera/cli/transfer_progress.rb +290 -55
  77. data/lib/aspera/cli/vault_manager.rb +0 -17
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +5 -3
  80. data/lib/aspera/coverage.rb +1 -1
  81. data/lib/aspera/environment.rb +35 -5
  82. data/lib/aspera/faspex_gw.rb +2 -1
  83. data/lib/aspera/faspex_postproc.rb +1 -0
  84. data/lib/aspera/graphql.rb +5 -5
  85. data/lib/aspera/json_rpc/client.rb +5 -5
  86. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  87. data/lib/aspera/keychain/factory.rb +2 -1
  88. data/lib/aspera/keychain/one_password_api.rb +1 -1
  89. data/lib/aspera/link_header.rb +2 -2
  90. data/lib/aspera/log.rb +47 -27
  91. data/lib/aspera/markdown.rb +2 -0
  92. data/lib/aspera/mime.rb +25 -0
  93. data/lib/aspera/node_emulator.rb +759 -0
  94. data/lib/aspera/oauth/base.rb +37 -26
  95. data/lib/aspera/oauth/factory.rb +7 -3
  96. data/lib/aspera/oauth/generic.rb +1 -1
  97. data/lib/aspera/oauth/json_credentials.rb +34 -0
  98. data/lib/aspera/oauth/jwt.rb +4 -5
  99. data/lib/aspera/oauth/web.rb +9 -8
  100. data/lib/aspera/oauth.rb +1 -0
  101. data/lib/aspera/persistency_folder.rb +1 -3
  102. data/lib/aspera/preview/file_types.rb +4 -4
  103. data/lib/aspera/preview/generator.rb +11 -1
  104. data/lib/aspera/preview/options.schema.yaml +119 -0
  105. data/lib/aspera/preview/terminal.rb +4 -3
  106. data/lib/aspera/preview/utils.rb +9 -6
  107. data/lib/aspera/products/connect.rb +1 -1
  108. data/lib/aspera/rainbow.rb +7 -0
  109. data/lib/aspera/rest/aspera_errors.rb +72 -0
  110. data/lib/aspera/rest/call_error.rb +27 -0
  111. data/lib/aspera/rest/client.rb +523 -0
  112. data/lib/aspera/rest/error_analyzer.rb +113 -0
  113. data/lib/aspera/rest/list.rb +149 -0
  114. data/lib/aspera/rest/parameters.rb +55 -0
  115. data/lib/aspera/rest/util.rb +176 -0
  116. data/lib/aspera/rest.rb +7 -621
  117. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  118. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
  119. data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
  120. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
  121. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  122. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
  123. data/lib/aspera/schema/documentation.rb +13 -3
  124. data/lib/aspera/schema/reader.rb +12 -18
  125. data/lib/aspera/schema/registry.rb +23 -1
  126. data/lib/aspera/schema/validator.rb +92 -0
  127. data/lib/aspera/secret_hider.rb +36 -25
  128. data/lib/aspera/string_ext.rb +15 -0
  129. data/lib/aspera/temp_file_manager.rb +6 -5
  130. data/lib/aspera/transfer/parameters.rb +2 -0
  131. data/lib/aspera/transfer/spec.rb +1 -0
  132. data/lib/aspera/uri_reader.rb +11 -11
  133. data/lib/aspera/web_auth/index.html +147 -0
  134. data/lib/aspera/web_auth/server.rb +81 -0
  135. data.tar.gz.sig +0 -0
  136. metadata +43 -9
  137. metadata.gz.sig +0 -0
  138. data/lib/aspera/colors.rb +0 -79
  139. data/lib/aspera/node_simulator.rb +0 -344
  140. data/lib/aspera/preview/options.rb +0 -45
  141. data/lib/aspera/rest_call_error.rb +0 -25
  142. data/lib/aspera/rest_error_analyzer.rb +0 -111
  143. data/lib/aspera/rest_errors_aspera.rb +0 -58
  144. data/lib/aspera/rest_list.rb +0 -136
  145. data/lib/aspera/web_auth.rb +0 -211
@@ -6,6 +6,7 @@ require 'aspera/assert'
6
6
  require 'aspera/cli/result'
7
7
  require 'aspera/cli/command_registry'
8
8
  require 'aspera/cli/option_declarator'
9
+ require 'aspera/cli/special_values'
9
10
  require 'aspera/schema/registry'
10
11
 
11
12
  module Aspera
@@ -24,18 +25,24 @@ module Aspera
24
25
  ALL = (GLOBAL + INSTANCE).freeze
25
26
  end
26
27
  class << self
27
- # Per-class DSL registry (not inherited: each subclass gets its own instance).
28
- # @return [CommandRegistry]
28
+ include OptionDeclarator
29
+
30
+ # Option sources added with `use_options`, with their `on_set` target.
31
+ # @return [Hash{Class, Module => Proc, nil}]
29
32
  def used_option_sources
30
- @used_option_sources ||= []
33
+ @used_option_sources ||= {}
31
34
  end
32
35
 
33
36
  # Include options from another plugin or OptionDeclarator module.
34
37
  # @param source [Class, Module]
35
- def use_options(source)
36
- used_option_sources << source unless used_option_sources.include?(source)
38
+ # @param target [Proc, nil] Executed on the plugin instance, returns the object for Symbol and Proc `on_set` callbacks
39
+ # of `source` (e.g. `-> { http_config }`); nil: the plugin instance
40
+ def use_options(source, target: nil)
41
+ used_option_sources[source] = target unless used_option_sources.key?(source)
37
42
  end
38
43
 
44
+ # Per-class DSL registry (not inherited: each subclass gets its own instance).
45
+ # @return [CommandRegistry]
39
46
  def command_registry
40
47
  @command_registry ||= CommandRegistry.new
41
48
  end
@@ -43,25 +50,50 @@ module Aspera
43
50
  # DSL class method: register a command in this plugin's registry.
44
51
  # Inherits parent from the enclosing commands_under block when parent: is omitted.
45
52
  # @param id [Symbol]
46
- # @param kwargs [Hash] forwarded to CommandSpec
53
+ # @param kwargs [Hash] forwarded to [CommandSpec]
47
54
  def command(id, **kwargs)
48
55
  kwargs[:parent] = @current_parent if kwargs[:parent].nil? && @current_parent
49
56
  command_registry.register(CommandSpec.new(id: id, **kwargs))
50
57
  end
51
58
 
52
- # Derive a display name from an entity path:
53
- # last segment after '/', underscores replaced by spaces, first letter capitalized.
54
- # e.g. 'data/smtp_server' -> 'Smtp server', 'data/transfer_settings' -> 'Transfer settings'
55
- def entity_display_name(entity)
56
- entity.to_s.split('/').last.tr('_', ' ').capitalize
59
+ # Words displayed with specific case in descriptions
60
+ NOUN_WORDS = {'smtp' => 'SMTP', 'ldap' => 'LDAP', 'saml' => 'SAML', 'oauth' => 'OAuth', 'kms' => 'KMS', 'api' => 'API'}.freeze
61
+ private_constant :NOUN_WORDS
62
+
63
+ # Derive a lowercase noun from an entity path, singular unless told otherwise.
64
+ # e.g. 'access_keys' -> 'access key', 'data/smtp_server' -> 'SMTP server'
65
+ # @param entity [String, Symbol] REST path or entity name
66
+ # @param singular [Boolean] Singularize the last word
67
+ # @return [String]
68
+ def entity_noun(entity, singular: true)
69
+ words = entity.to_s.split('/').last.split('_').map { |w| NOUN_WORDS.fetch(w, w) }
70
+ words[-1] = words[-1].sub(/ies\z/, 'y').sub(/(ss|x|sh|ch)es\z/, '\1').sub(/(?<!s)s\z/, '') if singular
71
+ words.join(' ')
72
+ end
73
+
74
+ # Standard description of a CRUD operation on an entity.
75
+ # e.g. (:list, 'access key') -> 'List access keys', (:show, 'access key') -> 'Show access key'
76
+ # @param verb [Symbol] Operation
77
+ # @param noun [String] Singular noun of entity
78
+ # @return [String]
79
+ def operation_description(verb, noun)
80
+ return "#{verb.capitalize} #{noun}" unless verb.eql?(:list)
81
+ plural =
82
+ case noun
83
+ when /[^aeiou]y\z/ then noun.sub(/y\z/, 'ies')
84
+ when /(s|x|sh|ch)\z/ then "#{noun}es"
85
+ else "#{noun}s"
86
+ end
87
+ "List #{plural}"
57
88
  end
58
89
 
59
90
  # DSL class method: declare CRUD commands for a REST entity.
60
91
  #
61
92
  # For each verb in operations:, registers one CommandSpec with:
62
- # - description: "#{verb.capitalize} #{name}"
63
- # - arguments: [{name: :id, type: :identifier, lookup: lookup}] for instance verbs
93
+ # - description: operation_description(verb, name)
94
+ # - arguments: [{name: id_name, type: :identifier, lookup: lookup}] for instance verbs
64
95
  # (:show, :modify, :delete) when not a singleton; none for global verbs
96
+ # body of :create and :modify is named after the entity (e.g. <access_key>), passed as data:
65
97
  # - action: calls entity_<verb>(api:, entity:, **shared_kwargs, **ctx)
66
98
  #
67
99
  # api: is resolved at runtime: :@ivar -> instance_variable_get, else -> send.
@@ -71,14 +103,23 @@ module Aspera
71
103
  # @param api [Symbol, String] Runtime API ref (:@ivar or method name) or literal string
72
104
  # @param entity [String, Symbol] REST sub-path, or ctx key Symbol resolved at runtime
73
105
  # @param operations [Array<Symbol>] Verbs to expose; defaults to Operations::ALL
74
- # @param name [String, nil] Display name; defaults to last segment of entity (static only)
106
+ # @param name [String, nil] Singular display name; defaults to last segment of entity (static only)
75
107
  # @param lookup [Symbol, nil] Instance method for percent-selector resolution
108
+ # @param id_name [Symbol, nil] Name of identifier argument; defaults to <name>_id, or id_as_arg field, or :id
76
109
  # @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 ||= entity_display_name(entity) unless entity.is_a?(Symbol)
110
+ def crud_commands(api:, entity:, operations: nil, name: nil, lookup: nil, id_name: nil, **kwargs)
111
+ name ||= entity_noun(entity, singular: !kwargs[:is_singleton]) unless entity.is_a?(Symbol)
79
112
  operations ||= Operations::ALL
113
+ # Body argument is named after the entity, e.g. <access_key>, and passed as data: to entity_<verb>
114
+ data_name = name ? name.downcase.tr(' ', '_').to_sym : :data
115
+ # Identifier argument is named after the entity, e.g. <access_key_id>, and passed as id: to entity_<verb>
116
+ id_name ||=
117
+ if kwargs[:id_as_arg].is_a?(String) then kwargs[:id_as_arg].to_sym
118
+ elsif name then :"#{data_name}_id"
119
+ else :id
120
+ end
80
121
  operations.each do |verb|
81
- id_arg = ({name: :id, type: :identifier, lookup: lookup} if Operations::INSTANCE.include?(verb) && !kwargs[:is_singleton])
122
+ id_arg = ({name: id_name, type: :identifier, lookup: lookup} if Operations::INSTANCE.include?(verb) && !kwargs[:is_singleton])
82
123
  schema_val =
83
124
  if kwargs[:body_component] && entity.is_a?(String)
84
125
  case verb
@@ -89,9 +130,9 @@ module Aspera
89
130
  args =
90
131
  case verb
91
132
  when :create
92
- [{name: :data, type: Hash, bulk: true, schema: schema_val}]
133
+ [{name: data_name, type: Hash, bulk: true, schema: schema_val}]
93
134
  when :modify
94
- [id_arg, {name: :data, type: Hash, schema: schema_val}].compact
135
+ [id_arg, {name: data_name, type: Hash, schema: schema_val}].compact
95
136
  when :delete
96
137
  id_arg ? [id_arg.merge(bulk: true)] : nil
97
138
  else
@@ -107,10 +148,13 @@ module Aspera
107
148
  api
108
149
  end
109
150
  resolved_entity = entity.is_a?(Symbol) ? ctx.fetch(entity) : entity
151
+ ctx = ctx.merge(data: ctx[data_name]) if ctx.key?(data_name)
152
+ ctx = ctx.merge(id: ctx[id_name]) if ctx.key?(id_name)
110
153
  send(:"entity_#{verb}", api: resolved_api, entity: resolved_entity, **kwargs, **ctx)
111
154
  end
112
- cmd_attrs = {description: "#{verb.capitalize} #{name || entity.inspect}", action: action_proc}
155
+ cmd_attrs = {description: operation_description(verb, name || entity.inspect), action: action_proc}
113
156
  cmd_attrs[:arguments] = args if args
157
+ cmd_attrs[:query_schema] = Schema::Registry.query_params(kwargs[:query_component], entity) if verb.eql?(:list) && kwargs[:query_component] && entity.is_a?(String)
114
158
  command(verb, **cmd_attrs)
115
159
  end
116
160
  end
@@ -139,7 +183,7 @@ module Aspera
139
183
  path = Array(@current_parent) + Array(parent)
140
184
  unless command_registry[path]
141
185
  id = path.last
142
- desc = description || "Manage #{entity_display_name(id)}"
186
+ desc = description || "Manage #{entity_noun(id, singular: false)}"
143
187
  parent_path = path[0..-2]
144
188
  saved = @current_parent
145
189
  @current_parent = parent_path.empty? ? nil : parent_path
@@ -153,101 +197,56 @@ module Aspera
153
197
  @current_parent = previous
154
198
  end
155
199
 
156
- # DSL class method: declare an option in this plugin's registry.
200
+ # Options of this plugin class (`option` DSL, see OptionDeclarator).
157
201
  # Metadata is stored as an OptionSpec at class-load time; the actual
158
202
  # options.declare call happens in Base#initialize once the instance exists.
159
- #
203
+ # @return [Hash{Symbol => OptionSpec}]
204
+ def option_specs
205
+ command_registry.option_specs
206
+ end
207
+
208
+ # Store an OptionSpec in the command registry.
160
209
  # Raises ArgumentError at class-load time if the same option name is already
161
210
  # declared by any ancestor class, preventing silent shadowing.
162
- #
163
- # @param name [Symbol] Option name
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)
211
+ # @param spec [OptionSpec]
212
+ def register_option_spec(spec)
178
213
  ancestor_owner = ancestors.drop(1).find do |klass|
179
214
  klass.is_a?(Class) && klass <= Base &&
180
215
  klass.instance_variable_defined?(:@command_registry) &&
181
- klass.command_registry.option_specs.key?(name)
216
+ klass.command_registry.option_specs.key?(spec.name)
182
217
  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
- )
218
+ raise ArgumentError, "#{self}: option :#{spec.name} already declared in ancestor #{ancestor_owner}" if ancestor_owner
219
+ command_registry.register_option(spec)
196
220
  end
197
221
 
198
- # Declare all options registered on this plugin class onto a Parser instance.
199
- # Walks inherited options and any sources added via `use_options`.
200
- # @param options [Aspera::Cli::Parser]
201
- # @param parse [Boolean] whether to call parse_options! after declaring
202
- def declare_options(options, parse: false)
203
- sources = []
222
+ # Classes and modules whose options apply to this plugin:
223
+ # this class, its plugin ancestors and sources added via `use_options`.
224
+ # @return [Hash{Class, Module => Proc, nil}] key responds to `option_specs`, value: `on_set` target (see `use_options`)
225
+ def option_sources
226
+ sources = {}
204
227
  ancestors.each do |klass|
205
228
  next unless klass.is_a?(Class) && klass <= Base
206
- sources << klass if klass.instance_variable_defined?(:@command_registry)
207
- sources.concat(klass.used_option_sources) if klass.respond_to?(:used_option_sources)
229
+ sources[klass] = nil if klass.instance_variable_defined?(:@command_registry) && !sources.key?(klass)
230
+ klass.used_option_sources.each { |src, src_target| sources[src] = src_target unless sources.key?(src) }
208
231
  end
209
- sources.uniq.each do |src|
210
- specs =
211
- if src.respond_to?(:command_registry)
212
- src.command_registry.option_specs
213
- elsif src.respond_to?(:option_specs)
214
- src.option_specs
215
- else
216
- {}
217
- end
218
- specs.each_value do |spec|
219
- next if options.option_declared?(spec.name)
220
- resolved_handler =
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
- )
232
+ sources
233
+ end
234
+
235
+ # Declare all options of `option_sources` onto a Parser instance.
236
+ # Skips options already declared on the parser: it is shared across all plugins in a run.
237
+ # @param options [Aspera::Cli::Parser]
238
+ # @param target [Base, nil] plugin instance for Symbol and Proc `on_set` callbacks; nil: such callbacks are not bound
239
+ # @param parse [Boolean] whether to call parse_options! after declaring
240
+ def declare_options(options, target: nil, parse: false)
241
+ option_sources.each do |src, src_target|
242
+ src_target = src_target.nil? ? target : target&.instance_exec(&src_target)
243
+ src.option_specs.each_value do |spec|
244
+ spec.declare_on(options, target: src_target) unless options.option_declared?(spec.name)
234
245
  end
235
246
  end
236
247
  options.parse_options! if parse
237
248
  end
238
249
 
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
250
  # DSL class method: declare the human-readable application name shown in wizards.
252
251
  # When called with an argument, sets the name. When called with no argument, returns it.
253
252
  # Falls back to the last component of the class name if never set.
@@ -289,49 +288,9 @@ module Aspera
289
288
  # below (DSL-registered and imperative) appear under the plugin section in
290
289
  # --help output, separate from the global options.
291
290
  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
- # Walk the ancestor chain so that options declared on parent plugin classes
294
- # (e.g. Oauth, BasicAuth) are also registered for sub-classes (e.g. Aoc).
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
291
+ # Auto-declare all options registered via the DSL `option` class method,
292
+ # including those of parent plugin classes (e.g. Oauth, BasicAuth) and `use_options` sources.
293
+ self.class.declare_options(options, target: self)
335
294
  end
336
295
 
337
296
  # Global objects
@@ -357,46 +316,28 @@ module Aspera
357
316
  # @return [Aspera::Cli::TransferProgress, nil]
358
317
  def progress_bar; @context.progress_bar; end
359
318
 
360
- def add_manual_header(_has_options = true)
361
- # No-op: the group is set at the start of initialize.
362
- # Kept for compatibility with Config, which calls add_manual_header(false) from Runner.
363
- end
364
-
365
319
  # Entry point for all DSL-based plugins.
366
320
  def execute_action
367
321
  @help_path = nil
368
- # Validate the registry once per class (memoised by the ivar check).
369
- # Passes the plugin class so implicit handler methods can be verified.
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)
322
+ validate_registry
323
+ dispatch_from_registry([])
382
324
  end
383
325
 
384
326
  # Two-phase dispatcher: run setup on the current node (Phase A), then either
385
327
  # execute a leaf directly or consume the next argument and recurse (Phase B).
386
328
  # @param current_path [Array<Symbol>] path of the node currently being dispatched
387
329
  # @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
330
  # @return [Object] result suitable for CLI output
390
- def dispatch_from_registry(current_path, ctx = {}, skip_setup: false)
331
+ def dispatch_from_registry(current_path, ctx = {})
391
332
  registry = self.class.command_registry
392
333
  spec = registry[current_path]
393
334
  is_leaf = spec && registry.children_of(current_path).empty?
394
335
 
395
- if options.help_requested || skip_setup
336
+ if @context.help_requested
396
337
  # help_requested on an intermediate node: drain positional args without validation
397
338
  # so that dispatch_child can still consume the correct sub-command token
398
- if !is_leaf && !skip_setup && spec&.arguments
399
- spec.arguments.each do |arg_spec|
339
+ if !is_leaf
340
+ registry.arguments_at(current_path).each do |arg_spec|
400
341
  next if ctx.key?(arg_spec.name)
401
342
  options.get_next_argument(arg_spec.name.to_s, mandatory: false)
402
343
  end
@@ -404,24 +345,7 @@ module Aspera
404
345
  else
405
346
  # Phase A - for intermediate nodes only: resolve all ArgumentSpec declared on this node
406
347
  # 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
348
+ ctx = resolve_arguments(spec.arguments, ctx) if !is_leaf && spec&.arguments
425
349
  ctx = ctx.merge(send(spec.setup, **ctx)) if spec&.setup
426
350
  end
427
351
 
@@ -440,15 +364,15 @@ module Aspera
440
364
  # @param ctx [Hash]
441
365
  # @return [Object]
442
366
  def dispatch_leaf(current_path, spec, ctx)
443
- if options.help_requested
367
+ if @context.help_requested
444
368
  @help_path = current_path
445
369
  raise Cli::HelpRequest, self
446
370
  end
447
371
  execute_leaf(spec, ctx)
448
372
  end
449
373
 
450
- # Phase B, child branch: consume the next command argument, resolve the matching
451
- # child spec, handle delegation, and recurse or execute.
374
+ # Phase B, child branch: consume the next command argument, then either continue
375
+ # on a mounted plugin instance, or recurse into the child.
452
376
  # --help is intercepted at two points:
453
377
  # 1. Before get_next_command when no positional arg is pending: raises HelpRequest
454
378
  # immediately so the subcommand list with descriptions is shown rather than a
@@ -461,37 +385,64 @@ module Aspera
461
385
  # @return [Object]
462
386
  def dispatch_child(current_path, registry, ctx)
463
387
  children = registry.children_of(current_path)
464
- available = children.reject { |_, c| c.condition && !send(c.condition) }
465
- aliases = children.values.each_with_object({}) do |c, h|
388
+ # condition: methods belong to the class declaring the spec: only evaluate local ones
389
+ # (mounted children are only walked here for --help, see below).
390
+ # With --help, conditions are not evaluated: they may need the API, which is not built for help.
391
+ available = children.reject { |id, c| c.condition && !@context.help_requested && registry.local?(current_path + [id]) && !send(c.condition) }
392
+ # Aliases of available commands only: an alias must not bypass the condition of its command
393
+ aliases = available.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 options.help_requested && options.command_or_arg_empty?
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 options.help_requested && options.command_or_arg_empty?
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
- # Instance delegation: hand off to a different plugin object
487
- if child.delegate_instance
488
- target = send(child.delegate_instance)
489
- return target.dispatch_from_registry(Array(child.delegates_to), {})
490
- end
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)
418
+
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
492
422
 
493
- # Both intermediate and leaf: instance_arg + setup are handled by Phase A of the next call
494
- dispatch_from_registry(current_path + [command], ctx)
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 +ctx+ (pre-resolved by a parent plugin, e.g. aoc.rb forwarding
524
- # path: into execute_nodegen4_command) are skipped — the token has already been consumed.
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,25 +480,40 @@ 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 || []).each do |arg_spec|
535
- next if ctx.key?(arg_spec.name)
536
- if arg_spec.type.eql?(:identifier)
537
- lookup_cb = arg_spec.lookup
538
- block =
539
- if lookup_cb.nil? then nil
540
- elsif lookup_cb.is_a?(Symbol) then ->(f, v) { send(lookup_cb, f, v, **ctx) }
541
- else ->(f, v) { instance_exec(f, v, **ctx, &lookup_cb) }
542
- end
543
- ctx = ctx.merge(arg_spec.name => resolve_argument(arg_spec, &block))
544
- else
545
- ctx = ctx.merge(arg_spec.name => resolve_argument(arg_spec))
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` (e.g. injected by a setup method) are not read from the command line.
489
+ # If the next CLI argument is that same value, or `ALL`, it is skipped, so that it is not mistaken for a later argument
490
+ # (e.g. `faspex5 packages receive ALL` with a public link to a package).
491
+ # For type: :identifier, the percent-selector lookup receives the ctx accumulated so far.
492
+ # @param arg_specs [Array<ArgumentSpec>]
493
+ # @param ctx [Hash] accumulated context
494
+ # @return [Hash] ctx merged with the resolved arguments
495
+ def resolve_arguments(arg_specs, ctx)
496
+ arg_specs.each do |arg_spec|
497
+ if ctx.key?(arg_spec.name)
498
+ options.get_next_argument(arg_spec.name.to_s, mandatory: false) if options.next_argument_in?([ctx[arg_spec.name].to_s, SpecialValues::ALL])
499
+ next
546
500
  end
501
+ lookup_cb = arg_spec.lookup if arg_spec.type.eql?(:identifier)
502
+ current = ctx
503
+ block =
504
+ case lookup_cb
505
+ when nil then nil
506
+ when Symbol then ->(f, v) { send(lookup_cb, f, v, **current) }
507
+ else ->(f, v) { instance_exec(f, v, **current, &lookup_cb) }
508
+ end
509
+ ctx = ctx.merge(arg_spec.name => resolve_argument(arg_spec, &block))
547
510
  end
548
- invoke_action(a, [], ctx)
511
+ ctx
549
512
  end
550
513
 
551
514
  # Resolve a single positional argument from the CLI argument stream.
552
515
  # When arg_spec.bulk is true, always returns an Array (normalized to [value] when non-bulk).
516
+ # Only such arguments are read as a list with `--bulk=yes`: others (e.g. the id of a parent node) are single values.
553
517
  # For type: :identifier, an optional block provides the percent-selector lookup.
554
518
  # @param arg_spec [ArgumentSpec]
555
519
  # @yieldparam field [String] field name from a percent-selector (%field:value)
@@ -560,7 +524,7 @@ module Aspera
560
524
  if arg_spec.bulk
561
525
  is_bulk = options.get_option(:bulk)
562
526
  if arg_spec.type.eql?(:identifier)
563
- val = options.instance_identifier(description: arg_spec.name.to_s, &block)
527
+ val = options.instance_identifier(description: arg_spec.name.to_s, multiple: is_bulk, &block)
564
528
  else
565
529
  val = options.get_next_argument(
566
530
  arg_spec.name.to_s,
@@ -581,19 +545,19 @@ module Aspera
581
545
  options.instance_identifier(description: arg_spec.name.to_s, &block)
582
546
  else
583
547
  # Class or Array<Class> -> pass as validation type
584
- # When interactive: true, set ask_missing_mandatory so that get_interactive is triggered
585
- # when no CLI arguments are provided (mandatory is forced to true for the same reason:
586
- # a non-nil default would short-circuit get_interactive before it is ever called).
587
- options.ask_missing_mandatory = true if arg_spec.interactive
588
- options.get_next_argument(
589
- arg_spec.name.to_s,
590
- mandatory: arg_spec.interactive ? true : arg_spec.mandatory,
591
- multiple: arg_spec.multiple || false,
592
- validation: arg_spec.type,
593
- accept_list: arg_spec.allowed,
594
- default: arg_spec.interactive ? nil : arg_spec.default,
595
- schema: arg_spec.schema
596
- )
548
+ # When interactive: true, prompt for this argument only when no CLI argument is provided
549
+ # (mandatory is forced to true and default ignored: a non-nil default would short-circuit the prompt).
550
+ options.with_interactive(enabled: arg_spec.interactive) do
551
+ options.get_next_argument(
552
+ arg_spec.name.to_s,
553
+ mandatory: arg_spec.interactive ? true : arg_spec.mandatory,
554
+ multiple: arg_spec.multiple || false,
555
+ validation: arg_spec.type,
556
+ accept_list: arg_spec.allowed,
557
+ default: arg_spec.interactive ? nil : arg_spec.default,
558
+ schema: arg_spec.schema
559
+ )
560
+ end
597
561
  end
598
562
  end
599
563
  end
@@ -603,17 +567,18 @@ module Aspera
603
567
  # @param path [Array<Symbol>] starting path ([] for the full tree)
604
568
  # @return [Hash] { command_id => { description:, condition:, children: } }
605
569
  def generate_help(path = [])
606
- self.class.command_registry.children_of(path).transform_values do |child_spec|
570
+ self.class.command_registry.children_of(path).to_h do |id, child_spec|
607
571
  annotation = child_spec.condition ? " [#{child_spec.condition}]" : ''
608
- {
572
+ # path + [id], not child_spec.full_path: a mounted spec's full_path is in the target namespace
573
+ [id, {
609
574
  description: "#{child_spec.description}#{annotation}",
610
575
  condition: child_spec.condition,
611
- children: generate_help(child_spec.full_path)
612
- }
576
+ children: generate_help(path + [id])
577
+ }]
613
578
  end
614
579
  end
615
580
 
616
- # Convenience wrapper: reads :bulk and :bfail from options, normalises +items+
581
+ # Convenience wrapper: reads :bulk and :bfail from options, normalizes `items`
617
582
  # to an Array, then delegates to Result.bulk.
618
583
  # Use this in action methods instead of the three-line boilerplate:
619
584
  # is_bulk = options.get_option(:bulk)
@@ -623,7 +588,7 @@ module Aspera
623
588
  # @param command [Symbol] Operation name (:create, :delete, ...)
624
589
  # @param id_result [String] Key used as item identifier in the result row
625
590
  # @param fields [Object] Fields hint passed to Result constructor (non-bulk only)
626
- # @yieldparam item [Object] Each item in +items+
591
+ # @yieldparam item [Object] Each item in `items`
627
592
  # @return [Result::ObjectList, Result::SingleObject]
628
593
  def bulk_result(items, command:, id_result: 'id', fields: :default, &block)
629
594
  items = items.is_a?(Array) ? items : [items]
@@ -645,7 +610,7 @@ module Aspera
645
610
  # NOT read from the CLI queue inside the method.
646
611
 
647
612
  # List all instances of an entity.
648
- # @param api [Aspera::Rest] REST API object
613
+ # @param api [Aspera::Rest::Client] REST API object
649
614
  # @param entity [String] API sub-path
650
615
  # @param display_fields [Array, nil] Fields to display
651
616
  # @param items_key [String, nil] Sub-key in response containing the array
@@ -655,12 +620,14 @@ module Aspera
655
620
  qs_path = query_component ? Schema::Registry.query_params(query_component, entity) : nil
656
621
  data, http = api.read(entity, query_read_delete(default: list_query, schema: qs_path), ret: :both)
657
622
  return Result::Empty.new if http.code == '204'
658
- # TODO: not generic : which application is this for ?
659
- if http['Content-Type'].start_with?('application/vnd.api+json')
660
- Log.log.debug('is vnd.api')
623
+ if !data.is_a?(Hash)
624
+ # already the list
625
+ elsif items_key
626
+ data = data[items_key]
627
+ elsif http['Content-Type'].start_with?(Mime::JSON_API)
628
+ # JSON:API: list is under the entity name
661
629
  data = data[entity]
662
630
  end
663
- data = data[items_key] if items_key
664
631
  case data
665
632
  when Hash then Result::SingleObject.new(data, fields: display_fields)
666
633
  when Array
@@ -671,7 +638,7 @@ module Aspera
671
638
  end
672
639
 
673
640
  # Show one instance of an entity.
674
- # @param api [Aspera::Rest] REST API object
641
+ # @param api [Aspera::Rest::Client] REST API object
675
642
  # @param entity [String] API sub-path
676
643
  # @param id [String, nil] Resource identifier; nil when is_singleton: true
677
644
  # @param display_fields [Array, nil] Fields to display
@@ -683,43 +650,32 @@ module Aspera
683
650
  end
684
651
 
685
652
  # Create one or more instances of an entity (supports bulk).
686
- # @param api [Aspera::Rest] REST API object
687
- # @param entity [String] API sub-path
688
- # @param display_fields [Array, nil] Fields to display
689
- # @param body_component [String, nil] Registry key for request body schema
690
- # @param input_data [Array, nil] Pre-resolved data; when nil, read from CLI
691
- def entity_create(api:, entity:, display_fields: nil, body_component: nil, input_data: nil, data: nil, **)
692
- schema = body_component ? Schema::Registry.req_body(body_component, "#{entity}.post") : nil
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|
653
+ # @param api [Aspera::Rest::Client] REST API object
654
+ # @param entity [String] API sub-path
655
+ # @param data [Hash, Array<Hash>] Entity data (Array with bulk), from the command's declared `data` argument
656
+ # @param display_fields [Array, nil] Fields to display
657
+ def entity_create(api:, entity:, data:, display_fields: nil, **)
658
+ data = [data] unless data.is_a?(Array)
659
+ bulk_result(data, command: :create, fields: display_fields) do |params|
701
660
  api.create(entity, params)
702
661
  end
703
662
  end
704
663
 
705
664
  # Modify an existing instance of an entity.
706
- # @param api [Aspera::Rest] REST API object
665
+ # @param api [Aspera::Rest::Client] REST API object
707
666
  # @param entity [String] API sub-path
708
667
  # @param id [String, nil] Resource identifier; nil when is_singleton: true
709
668
  # @param is_singleton [Boolean] When true, entity is the full path (no id appended)
710
669
  # @param id_as_arg [Boolean, String] When set, id is appended as ?<id_as_arg>=<id>
711
- # @param body_component [String, nil] Registry key for request body schema
712
- # @param input_data [Hash, nil] Pre-resolved data; when nil, read from CLI
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
670
+ # @param data [Hash] Modified fields, from the command's declared `data` argument
671
+ def entity_modify(api:, entity:, data:, id: nil, is_singleton: false, id_as_arg: false, **)
715
672
  path = entity_res_path(entity, id, is_singleton: is_singleton, id_as_arg: id_as_arg)
716
- parameters = input_data || data || options.get_next_argument('data', validation: Hash, schema: schema)
717
- api.update(path, parameters)
673
+ api.update(path, data)
718
674
  Result::Status.new('modified')
719
675
  end
720
676
 
721
677
  # Delete one or more instances of an entity (supports bulk).
722
- # @param api [Aspera::Rest] REST API object
678
+ # @param api [Aspera::Rest::Client] REST API object
723
679
  # @param entity [String] API sub-path
724
680
  # @param id [String, Array, nil] Resource identifier(s)
725
681
  # @param id_as_arg [Boolean, String] When set, id is appended as ?<id_as_arg>=<id>