Class: Aspera::Cli::Plugins::Base

Inherits:
Object
  • Object
show all
Extended by:
OptionDeclarator
Defined in:
lib/aspera/cli/plugins/base.rb

Overview

Base class for command plugins

Direct Known Subclasses

Ats, BasicAuth, Config, Cos, Httpgw, Mcp

Defined Under Namespace

Modules: Operations

Constant Summary collapse

FILTER_ARGS =

Shared positional argument for commands that accept an optional file name filter. Accepted types: String (shell glob matched against entry name), Regexp, or Proc. Used by node files find, and preview scan/events/trevents.

[{name: :filter, type: [String, Regexp, Proc], description: 'File name filter: String (glob), Regexp, or Proc', mandatory: false, default: nil}].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from OptionDeclarator

declare_options, option, option_specs, register_option_spec

Constructor Details

#initialize(context:) ⇒ Base

Returns a new instance of Base.



279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/aspera/cli/plugins/base.rb', line 279

def initialize(context:)
  Aspera.assert_type(context, Context) { 'context' }
  Aspera.assert_type(context.man_header, TrueClass, FalseClass) { 'context.man_header' }
  @context = context
  # Switch to the plugin-specific options group so that all options declared
  # below (DSL-registered and imperative) appear under the plugin section in
  # --help output, separate from the global options.
  options.group(self.class.name.split('::').last.downcase) if @context.man_header
  # Auto-declare all options registered via the DSL `option` class method,
  # including those of parent plugin classes (e.g. Oauth, BasicAuth) and `use_options` sources.
  self.class.declare_options(options, target: self)
end

Instance Attribute Details

#context ⇒ Object (readonly)

Global objects



293
294
295
# File 'lib/aspera/cli/plugins/base.rb', line 293

def context
  @context
end

#help_path ⇒ Object (readonly)

Path reached in the command tree at the moment --help was intercepted. Nil until set by dispatch_from_registry.



296
297
298
# File 'lib/aspera/cli/plugins/base.rb', line 296

def help_path
  @help_path
end

Class Method Details

.application_name(name = nil) ⇒ String

DSL class method: declare the human-readable application name shown in wizards. When called with an argument, sets the name. When called with no argument, returns it. Falls back to the last component of the class name if never set.

Parameters:

  • name (String, nil) (defaults to: nil)

Returns:



251
252
253
254
# File 'lib/aspera/cli/plugins/base.rb', line 251

def application_name(name = nil)
  @application_name = name unless name.nil?
  @application_name || self.name.split('::').last
end

.command(id, **kwargs) ⇒ Object

DSL class method: register a command in this plugin's registry. Inherits parent from the enclosing commands_under block when parent: is omitted.

Parameters:

  • id (Symbol)
  • kwargs (Hash) —

    forwarded to [CommandSpec]



51
52
53
54
# File 'lib/aspera/cli/plugins/base.rb', line 51

def command(id, **kwargs)
  kwargs[:parent] = @current_parent if kwargs[:parent].nil? && @current_parent
  command_registry.register(CommandSpec.new(id: id, **kwargs))
end

.command_registry ⇒ CommandRegistry

Per-class DSL registry (not inherited: each subclass gets its own instance).

Returns:



43
44
45
# File 'lib/aspera/cli/plugins/base.rb', line 43

def command_registry
  @command_registry ||= CommandRegistry.new
end

.commands_under(parent, description: nil) ⇒ Object

DSL class method: scope block that sets a default parent for nested command() calls. Fully re-entrant: blocks may be nested for multi-level parent paths. If the terminal node of parent has not been declared yet, it is auto-declared as an intermediate command with description: "Manage " (or the given description:).

parent is always resolved relative to the current scope:

Array(@current_parent) + Array(parent)

Parameters:

  • parent (Symbol, Array<Symbol>) —

    one or more path segments, relative to current scope

  • description (String, nil) (defaults to: nil) —

    Description of entity for the auto-declared node

Yield Returns:

  • (void)


178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/aspera/cli/plugins/base.rb', line 178

def commands_under(parent, description: nil)
  # Always relative: append the given segments to the current scope.
  path = Array(@current_parent) + Array(parent)
  unless command_registry[path]
    id = path.last
    desc = description || "Manage #{entity_noun(id, singular: false)}"
    parent_path = path[0..-2]
    saved = @current_parent
    @current_parent = parent_path.empty? ? nil : parent_path
    command(id, description: desc)
    @current_parent = saved
  end
  previous = @current_parent
  @current_parent = path
  yield
ensure
  @current_parent = previous
end

.crud_commands(api:, entity:, operations: nil, name: nil, lookup: nil, id_name: nil, **kwargs) ⇒ Object

DSL class method: declare CRUD commands for a REST entity.

For each verb in operations:, registers one CommandSpec with:

- description: operation_description(verb, name)
- arguments:   [{name: id_name, type: :identifier, lookup: lookup}] for instance verbs
             (:show, :modify, :delete) when not a singleton; none for global verbs
             body of :create and :modify is named after the entity (e.g. <access_key>), passed as data:
- action:      calls entity_<verb>(api:, entity:, **shared_kwargs, **ctx)

api: is resolved at runtime: :@ivar -> instance_variable_get, else -> send. entity: may also be a Symbol — resolved at runtime as a ctx key (e.g. :sf_entity). This covers cases where the entity path is injected by a parent setup: method.

Parameters:

  • api (Symbol, String) —

    Runtime API ref (:@ivar or method name) or literal string

  • entity (String, Symbol) —

    REST sub-path, or ctx key Symbol resolved at runtime

  • operations (Array<Symbol>) (defaults to: nil) —

    Verbs to expose; defaults to Operations::ALL

  • name (String, nil) (defaults to: nil) —

    Singular display name; defaults to last segment of entity (static only)

  • lookup (Symbol, nil) (defaults to: nil) —

    Instance method for percent-selector resolution

  • id_name (Symbol, nil) (defaults to: nil) —

    Name of identifier argument; defaults to _id, or id_as_arg field, or :id

  • kwargs (Hash) —

    Shared params forwarded to every per-verb method



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/aspera/cli/plugins/base.rb', line 107

def crud_commands(api:, entity:, operations: nil, name: nil, lookup: nil, id_name: nil, **kwargs)
  name       ||= entity_noun(entity, singular: !kwargs[:is_singleton]) unless entity.is_a?(Symbol)
  operations ||= Operations::ALL
  # Body argument is named after the entity, e.g. <access_key>, and passed as data: to entity_<verb>
  data_name = name ? name.downcase.tr(' ', '_').to_sym : :data
  # Identifier argument is named after the entity, e.g. <access_key_id>, and passed as id: to entity_<verb>
  id_name ||=
    if kwargs[:id_as_arg].is_a?(String) then kwargs[:id_as_arg].to_sym
    elsif name then :"#{data_name}_id"
    else :id
    end
  operations.each do |verb|
    id_arg = ({name: id_name, type: :identifier, lookup: lookup} if Operations::INSTANCE.include?(verb) && !kwargs[:is_singleton])
    schema_val =
      if kwargs[:body_component] && entity.is_a?(String)
        case verb
        when :create then Schema::Registry.req_body(kwargs[:body_component], "#{entity}.post")
        when :modify then Schema::Registry.req_body(kwargs[:body_component], "#{entity}/{id}.put")
        end
      end
    args =
      case verb
      when :create
        [{name: data_name, type: Hash, bulk: true, schema: schema_val}]
      when :modify
        [id_arg, {name: data_name, type: Hash, schema: schema_val}].compact
      when :delete
        id_arg ? [id_arg.merge(bulk: true)] : nil
      else
        id_arg ? [id_arg] : nil
      end
    action_proc = lambda do |**ctx|
      resolved_api =
        if api.is_a?(Symbol)
          api.to_s.start_with?('@') ? instance_variable_get(api) : send(api)
        elsif api.is_a?(Proc)
          instance_exec(&api)
        else
          api
        end
      resolved_entity = entity.is_a?(Symbol) ? ctx.fetch(entity) : entity
      ctx = ctx.merge(data: ctx[data_name]) if ctx.key?(data_name)
      ctx = ctx.merge(id: ctx[id_name]) if ctx.key?(id_name)
      send(:"entity_#{verb}", api: resolved_api, entity: resolved_entity, **kwargs, **ctx)
    end
    cmd_attrs = {description: operation_description(verb, name || entity.inspect), action: action_proc}
    cmd_attrs[:arguments] = args if args
    cmd_attrs[:query_schema] = Schema::Registry.query_params(kwargs[:query_component], entity) if verb.eql?(:list) && kwargs[:query_component] && entity.is_a?(String)
    command(verb, **cmd_attrs)
  end
end

.declare_options(options, target: nil, parse: false) ⇒ Object

Declare all options of option_sources onto a Parser instance. Skips options already declared on the parser: it is shared across all plugins in a run.

Parameters:

  • options (Aspera::Cli::Parser)
  • target (Base, nil) (defaults to: nil) —

    plugin instance for Symbol and Proc on_set callbacks; nil: such callbacks are not bound

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

    whether to call parse_options! after declaring



237
238
239
240
241
242
243
244
# File 'lib/aspera/cli/plugins/base.rb', line 237

def declare_options(options, target: nil, parse: false)
  option_sources.each do |src|
    src.option_specs.each_value do |spec|
      spec.declare_on(options, target: target) unless options.option_declared?(spec.name)
    end
  end
  options.parse_options! if parse
end

.define_action_method(path) {|keyword| ... } ⇒ Object

DSL class method: define an instance method whose name is derived from a path array. Equivalent to: define_method(CommandSpec.action_method(path), &block)

Parameters:

  • path (Array<Symbol>) —

    command path segments, e.g. [:admin, :user, :list]

Yield Parameters:

  • keyword (Hash) —

    context forwarded from dispatch



163
164
165
# File 'lib/aspera/cli/plugins/base.rb', line 163

def define_action_method(path, &block)
  define_method(CommandSpec.action_method(path), &block)
end

.entity_noun(entity, singular: true) ⇒ String

Derive a lowercase noun from an entity path, singular unless told otherwise. e.g. 'access_keys' -> 'access key', 'data/smtp_server' -> 'SMTP server'

Parameters:

  • entity (String, Symbol) —

    REST path or entity name

  • singular (Boolean) (defaults to: true) —

    Singularize the last word

Returns:



65
66
67
68
69
# File 'lib/aspera/cli/plugins/base.rb', line 65

def entity_noun(entity, singular: true)
  words = entity.to_s.split('/').last.split('_').map { |w| NOUN_WORDS.fetch(w, w) }
  words[-1] = words[-1].sub(/ies\z/, 'y').sub(/(ss|x|sh|ch)es\z/, '\1').sub(/(?<!s)s\z/, '') if singular
  words.join(' ')
end

.file_matcher(match_expression) ⇒ Proc

Build a filter lambda from a match expression (String glob, Regexp, Proc, or nil).

Parameters:

  • match_expression (String, Regexp, Proc, NilClass) —

    as in FILTER_ARGS

Returns:

  • (Proc) —

    lambda(entry) -> Boolean



259
260
261
262
263
264
265
266
267
# File 'lib/aspera/cli/plugins/base.rb', line 259

def file_matcher(match_expression)
  case match_expression
  when Proc    then match_expression
  when Regexp  then ->(f) { f['name'].match?(match_expression) }
  when String  then ->(f) { File.fnmatch(match_expression, f['name'], File::FNM_DOTMATCH) }
  when NilClass then ->(_) { true }
  else Aspera.error_unexpected_value(match_expression.class.name, type: ParameterError)
  end
end

.operation_description(verb, noun) ⇒ String

Standard description of a CRUD operation on an entity. e.g. (:list, 'access key') -> 'List access keys', (:show, 'access key') -> 'Show access key'

Parameters:

  • verb (Symbol) —

    Operation

  • noun (String) —

    Singular noun of entity

Returns:



76
77
78
79
80
81
82
83
84
85
# File 'lib/aspera/cli/plugins/base.rb', line 76

def operation_description(verb, noun)
  return "#{verb.capitalize} #{noun}" unless verb.eql?(:list)
  plural =
    case noun
    when /[^aeiou]y\z/ then noun.sub(/y\z/, 'ies')
    when /(s|x|sh|ch)\z/ then "#{noun}es"
    else "#{noun}s"
    end
  "List #{plural}"
end

.option_sources ⇒ Array<Class, Module>

Classes and modules whose options apply to this plugin: this class, its plugin ancestors and sources added via use_options.

Returns:

  • (Array<Class, Module>) —

    each responds to option_specs



222
223
224
225
226
227
228
229
230
# File 'lib/aspera/cli/plugins/base.rb', line 222

def option_sources
  sources = []
  ancestors.each do |klass|
    next unless klass.is_a?(Class) && klass <= Base
    sources << klass if klass.instance_variable_defined?(:@command_registry)
    sources.concat(klass.used_option_sources)
  end
  sources.uniq
end

.option_specs ⇒ Hash{Symbol => OptionSpec}

Options of this plugin class (option DSL, see OptionDeclarator). Metadata is stored as an OptionSpec at class-load time; the actual options.declare call happens in Base#initialize once the instance exists.

Returns:



201
202
203
# File 'lib/aspera/cli/plugins/base.rb', line 201

def option_specs
  command_registry.option_specs
end

.register_option_spec(spec) ⇒ Object

Store an OptionSpec in the command registry. Raises ArgumentError at class-load time if the same option name is already declared by any ancestor class, preventing silent shadowing.

Parameters:

Raises:

  • (ArgumentError)


209
210
211
212
213
214
215
216
217
# File 'lib/aspera/cli/plugins/base.rb', line 209

def register_option_spec(spec)
  ancestor_owner = ancestors.drop(1).find do |klass|
    klass.is_a?(Class) && klass <= Base &&
      klass.instance_variable_defined?(:@command_registry) &&
      klass.command_registry.option_specs.key?(spec.name)
  end
  raise ArgumentError, "#{self}: option :#{spec.name} already declared in ancestor #{ancestor_owner}" if ancestor_owner
  command_registry.register_option(spec)
end

.use_options(source) ⇒ Object

Include options from another plugin or OptionDeclarator module.

Parameters:

  • source (Class, Module)


37
38
39
# File 'lib/aspera/cli/plugins/base.rb', line 37

def use_options(source)
  used_option_sources << source unless used_option_sources.include?(source)
end

.used_option_sources ⇒ Array<Class, Module>

Option sources added with use_options.

Returns:

  • (Array<Class, Module>)


31
32
33
# File 'lib/aspera/cli/plugins/base.rb', line 31

def used_option_sources
  @used_option_sources ||= []
end

Instance Method Details

#action_for(spec) ⇒ Symbol, Proc

Resolve the action for a leaf CommandSpec. Returns spec.action (Symbol or Proc) if explicitly set; otherwise derives a Symbol from the full path as :action_<path_segment_1><path_segment_2>... (e.g. [:access_key, :list] -> :action_access_key_list).

Parameters:

Returns:

  • (Symbol, Proc)


454
455
456
# File 'lib/aspera/cli/plugins/base.rb', line 454

def action_for(spec)
  spec.action || spec.action_method_name
end

#add_manual_header(_has_options = true) ⇒ Object



315
316
317
318
# File 'lib/aspera/cli/plugins/base.rb', line 315

def add_manual_header(_has_options = true)
  # No-op: the group is set at the start of initialize.
  # Kept for compatibility with Config, which calls add_manual_header(false) from Runner.
end

#bulk_result(items, command:, id_result: 'id', fields: :default) {|item| ... } ⇒ Result::ObjectList, Result::SingleObject

Convenience wrapper: reads :bulk and :bfail from options, normalizes items to an Array, then delegates to Result.bulk. Use this in action methods instead of the three-line boilerplate:

is_bulk = options.get_option(:bulk)
items   = x.is_a?(Array) ? x : [x]
Result.bulk(items, is_bulk: is_bulk, ...)

Parameters:

  • items (Object, Array) —

    Single item or Array; wrapped in Array when needed

  • command (Symbol) —

    Operation name (:create, :delete, ...)

  • id_result (String) (defaults to: 'id') —

    Key used as item identifier in the result row

  • fields (Object) (defaults to: :default) —

    Fields hint passed to Result constructor (non-bulk only)

Yield Parameters:

  • item (Object) —

    Each item in items

Returns:



587
588
589
590
591
592
593
594
595
596
597
598
# File 'lib/aspera/cli/plugins/base.rb', line 587

def bulk_result(items, command:, id_result: 'id', fields: :default, &block)
  items = items.is_a?(Array) ? items : [items]
  Result.bulk(
    items,
    is_bulk:   options.get_option(:bulk),
    command:   command,
    id_result: id_result,
    fields:    fields,
    bfail:     options.get_option(:bfail),
    &block
  )
end

#config ⇒ Aspera::Cli::Plugins::Config



303
# File 'lib/aspera/cli/plugins/base.rb', line 303

def config; @context.config; end

#dispatch_child(current_path, registry, ctx) ⇒ Object

Phase B, child branch: consume the next command argument, then either continue on a mounted plugin instance, or recurse into the child. --help is intercepted at two points:

1. Before get_next_command when no positional arg is pending: raises HelpRequest
 immediately so the subcommand list with descriptions is shown rather than a
 MissingArgument error.
2. After get_next_command when no further args remain: raises HelpRequest scoped
 to the consumed command (e.g. `aoc files find -h`).

Parameters:

Returns:

  • (Object)


387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/aspera/cli/plugins/base.rb', line 387

def dispatch_child(current_path, registry, ctx)
  children  = registry.children_of(current_path)
  # condition: methods belong to the class declaring the spec: only evaluate local ones
  # (mounted children are only walked here for --help, see below).
  # With --help, conditions are not evaluated: they may need the API, which is not built for help.
  available = children.reject { |id, c| c.condition && !@context.help_requested && registry.local?(current_path + [id]) && !send(c.condition) }
  aliases   = children.values.each_with_object({}) do |c, h|
    Array(c.aliases).each { |a| h[a] = c.id } if c.aliases
  end

  # Intercept --help before consuming the command token when no arg is pending.
  # This avoids MissingArgument being raised by get_next_command before HelpRequest.
  if @context.help_requested && options.command_or_arg_empty?
    @help_path = current_path
    raise Cli::HelpRequest, self
  end

  command = options.get_next_command(available.keys, aliases: aliases.empty? ? nil : aliases)

  # Intercept --help after a command was consumed but no further args remain.
  # (e.g. `aoc files find -h`). When further args remain, keep recursing.
  if @context.help_requested && options.command_or_arg_empty?
    @help_path = current_path + [command]
    raise Cli::HelpRequest, self
  end

  # Mounted child: continue on the target plugin instance, in its own namespace.
  # For --help, keep walking the (mount-aware) registry of this class instead, so that
  # no target instance (and thus no API connection) is needed.
  child_path = current_path + [command]
  return dispatch_mount(registry.mount_of(current_path), command, ctx) unless @context.help_requested || registry.local?(child_path)

  # Both intermediate and leaf: arguments + setup are handled by Phase A of the next call
  dispatch_from_registry(child_path, ctx)
end

#dispatch_from_registry(current_path, ctx = {}) ⇒ Object

Two-phase dispatcher: run setup on the current node (Phase A), then either execute a leaf directly or consume the next argument and recurse (Phase B).

Parameters:

  • current_path (Array<Symbol>) —

    path of the node currently being dispatched

  • ctx (Hash) (defaults to: {}) —

    accumulated context passed down from parent nodes

Returns:

  • (Object) —

    result suitable for CLI output



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/aspera/cli/plugins/base.rb', line 332

def dispatch_from_registry(current_path, ctx = {})
  registry = self.class.command_registry
  spec     = registry[current_path]
  is_leaf  = spec && registry.children_of(current_path).empty?

  if @context.help_requested
    # help_requested on an intermediate node: drain positional args without validation
    # so that dispatch_child can still consume the correct sub-command token
    if !is_leaf
      registry.arguments_at(current_path).each do |arg_spec|
        next if ctx.key?(arg_spec.name)
        options.get_next_argument(arg_spec.name.to_s, mandatory: false)
      end
    end
  else
    # Phase A - for intermediate nodes only: resolve all ArgumentSpec declared on this node
    # before dispatching to children (leaf nodes resolve their arguments inside execute_leaf).
    ctx = resolve_arguments(spec.arguments, ctx) if !is_leaf && spec&.arguments
    ctx = ctx.merge(send(spec.setup, **ctx)) if spec&.setup
  end

  # Phase B - leaf fast-path or child dispatch
  if is_leaf
    dispatch_leaf(current_path, spec, ctx)
  else
    dispatch_child(current_path, registry, ctx)
  end
end

#dispatch_leaf(current_path, spec, ctx) ⇒ Object

Phase B, leaf branch: execute a spec that is already a leaf (no children). Intercepts --help before calling execute_leaf.

Parameters:

Returns:

  • (Object)


367
368
369
370
371
372
373
# File 'lib/aspera/cli/plugins/base.rb', line 367

def dispatch_leaf(current_path, spec, ctx)
  if @context.help_requested
    @help_path = current_path
    raise Cli::HelpRequest, self
  end
  execute_leaf(spec, ctx)
end

#dispatch_mount(mount, command, ctx) ⇒ Object

Hand over dispatch of a mounted child to the target plugin instance. Setups of the mount point at and of its ancestors in the target are not executed: the seed ctx returned by the host's instance method replaces them. The mount's own arguments (if any) are read first, and passed to instance in ctx.

Parameters:

  • mount (MountSpec)
  • command (Symbol) —

    mounted child id, already consumed

  • ctx (Hash) —

    host context, passed to the instance method

Returns:

  • (Object)


431
432
433
434
435
436
437
438
# File 'lib/aspera/cli/plugins/base.rb', line 431

def dispatch_mount(mount, command, ctx)
  ctx = resolve_arguments(mount.arguments, ctx)
  target = send(mount.instance, **ctx)
  target, seed = target if target.is_a?(Array)
  Aspera.assert_type(target, mount.plugin)
  target.validate_registry
  target.dispatch_from_registry(mount.at + [command], seed || {})
end

#entity_create(api:, entity:, data:, display_fields: nil) ⇒ Object

Create one or more instances of an entity (supports bulk).

Parameters:

  • api (Aspera::Rest::Client) —

    REST API object

  • entity (String) —

    API sub-path

  • data (Hash, Array<Hash>) —

    Entity data (Array with bulk), from the command's declared data argument

  • display_fields (Array, nil) (defaults to: nil) —

    Fields to display



651
652
653
654
655
656
# File 'lib/aspera/cli/plugins/base.rb', line 651

def entity_create(api:, entity:, data:, display_fields: nil, **)
  data = [data] unless data.is_a?(Array)
  bulk_result(data, command: :create, fields: display_fields) do |params|
    api.create(entity, params)
  end
end

#entity_delete(api:, entity:, id: nil, id_as_arg: false, delete_style: nil, query_component: nil) ⇒ Object

Delete one or more instances of an entity (supports bulk).

Parameters:

  • api (Aspera::Rest::Client) —

    REST API object

  • entity (String) —

    API sub-path

  • id (String, Array, nil) (defaults to: nil) —

    Resource identifier(s)

  • id_as_arg (Boolean, String) (defaults to: false) —

    When set, id is appended as ?<id_as_arg>=

  • delete_style (String, nil) (defaults to: nil) —

    When set, deletes by sending id array in payload

  • query_component (String, nil) (defaults to: nil) —

    Registry key for --query=help schema



678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
# File 'lib/aspera/cli/plugins/base.rb', line 678

def entity_delete(api:, entity:, id: nil, id_as_arg: false, delete_style: nil, query_component: nil, **)
  qs_path = query_component ? Schema::Registry.query_params(query_component, entity) : nil
  if !delete_style.nil?
    ids = id.is_a?(Array) ? id : [id]
    Aspera.assert_type(ids, Array, type: Cli::BadArgument)
    api.delete(entity, nil, content_type: Mime::JSON, body: {delete_style => ids})
    return Result::Status.new('deleted')
  end
  bulk_result(id, command: :delete) do |one_id|
    api.delete(
      id_as_arg ? "#{entity}?#{id_as_arg}=#{one_id}" : "#{entity}/#{one_id}",
      query_read_delete(schema: qs_path)
    )
    {'id' => one_id}
  end
end

#entity_list(api:, entity:, display_fields: nil, items_key: nil, list_query: nil, query_component: nil) ⇒ Object

List all instances of an entity.

Parameters:

  • api (Aspera::Rest::Client) —

    REST API object

  • entity (String) —

    API sub-path

  • display_fields (Array, nil) (defaults to: nil) —

    Fields to display

  • items_key (String, nil) (defaults to: nil) —

    Sub-key in response containing the array

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

    Default query parameters

  • query_component (String, nil) (defaults to: nil) —

    Registry key for --query=help schema



613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
# File 'lib/aspera/cli/plugins/base.rb', line 613

def entity_list(api:, entity:, display_fields: nil, items_key: nil, list_query: nil, query_component: nil, **)
  qs_path = query_component ? Schema::Registry.query_params(query_component, entity) : nil
  data, http = api.read(entity, query_read_delete(default: list_query, schema: qs_path), ret: :both)
  return Result::Empty.new if http.code == '204'
  if !data.is_a?(Hash)
    # already the list
  elsif items_key
    data = data[items_key]
  elsif http['Content-Type'].start_with?(Mime::JSON_API)
    # JSON:API: list is under the entity name
    data = data[entity]
  end
  case data
  when Hash then Result::SingleObject.new(data, fields: display_fields)
  when Array
    return Result::ObjectList.new(data, fields: display_fields) if data.empty? || data.first.is_a?(Hash)
    Result::ValueList.new(data)
  else Aspera.error_unexpected_value(data.class.name) { 'list type' }
  end
end

#entity_modify(api:, entity:, data:, id: nil, is_singleton: false, id_as_arg: false) ⇒ Object

Modify an existing instance of an entity.

Parameters:

  • api (Aspera::Rest::Client) —

    REST API object

  • entity (String) —

    API sub-path

  • id (String, nil) (defaults to: nil) —

    Resource identifier; nil when is_singleton: true

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

    When true, entity is the full path (no id appended)

  • id_as_arg (Boolean, String) (defaults to: false) —

    When set, id is appended as ?<id_as_arg>=

  • data (Hash) —

    Modified fields, from the command's declared data argument



665
666
667
668
669
# File 'lib/aspera/cli/plugins/base.rb', line 665

def entity_modify(api:, entity:, data:, id: nil, is_singleton: false, id_as_arg: false, **)
  path = entity_res_path(entity, id, is_singleton: is_singleton, id_as_arg: id_as_arg)
  api.update(path, data)
  Result::Status.new('modified')
end

#entity_res_path(entity, id, is_singleton: false, id_as_arg: false) ⇒ String

Build the resource path for an instance operation.

Parameters:

  • entity (String) —

    API sub-path

  • id (String, nil) —

    Resource identifier

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

    When true, entity IS the full path

  • id_as_arg (Boolean, String) (defaults to: false) —

    When set, id appended as ?<id_as_arg>=

Returns:



701
702
703
704
705
# File 'lib/aspera/cli/plugins/base.rb', line 701

def entity_res_path(entity, id, is_singleton: false, id_as_arg: false)
  return entity if is_singleton
  return "#{entity}?#{id_as_arg}=#{id}" if id_as_arg
  "#{entity}/#{id}"
end

#entity_show(api:, entity:, id: nil, display_fields: nil, is_singleton: false, id_as_arg: false) ⇒ Object

Show one instance of an entity.

Parameters:

  • api (Aspera::Rest::Client) —

    REST API object

  • entity (String) —

    API sub-path

  • id (String, nil) (defaults to: nil) —

    Resource identifier; nil when is_singleton: true

  • display_fields (Array, nil) (defaults to: nil) —

    Fields to display

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

    When true, entity is the full path (no id appended)

  • id_as_arg (Boolean, String) (defaults to: false) —

    When set, id is appended as ?<id_as_arg>=



641
642
643
644
# File 'lib/aspera/cli/plugins/base.rb', line 641

def entity_show(api:, entity:, id: nil, display_fields: nil, is_singleton: false, id_as_arg: false, **)
  path = entity_res_path(entity, id, is_singleton: is_singleton, id_as_arg: id_as_arg)
  Result::SingleObject.new(api.read(path), fields: display_fields)
end

#execute_action ⇒ Object

Entry point for all DSL-based plugins.



321
322
323
324
325
# File 'lib/aspera/cli/plugins/base.rb', line 321

def execute_action
  @help_path = nil
  validate_registry
  dispatch_from_registry([])
end

#execute_leaf(spec, ctx) ⇒ Object

Execute a leaf CommandSpec: resolve arguments and call action. Arguments already present in ctx (e.g. provided by a caller or a mount seed) are skipped: they are not read again from the command line.

Parameters:

  • spec (CommandSpec) —

    a leaf node (no children)

  • ctx (Hash) —

    accumulated context (pre-resolved keys are not re-consumed)

Returns:

  • (Object)


479
480
481
482
483
484
485
# File 'lib/aspera/cli/plugins/base.rb', line 479

def execute_leaf(spec, ctx)
  a = action_for(spec)
  # Always resolve declared arguments (even when transfer_paths is set — those arguments
  # are consumed first; ts_source_paths then reads whatever remains in the queue).
  ctx = resolve_arguments(spec.arguments, ctx) if spec.arguments
  invoke_action(a, [], ctx)
end

#formatter ⇒ Aspera::Cli::Formatter



305
# File 'lib/aspera/cli/plugins/base.rb', line 305

def formatter; @context.formatter; end

#generate_help(path = []) ⇒ Hash

Build a nested Hash tree of the registered command tree for help display. Conditional commands are included with a '[condition_name]' annotation.

Parameters:

  • path (Array<Symbol>) (defaults to: []) —

    starting path ([] for the full tree)

Returns:

  • (Hash) —

    { command_id => { description:, condition:, children: } }



563
564
565
566
567
568
569
570
571
572
573
# File 'lib/aspera/cli/plugins/base.rb', line 563

def generate_help(path = [])
  self.class.command_registry.children_of(path).to_h do |id, child_spec|
    annotation = child_spec.condition ? " [#{child_spec.condition}]" : ''
    # path + [id], not child_spec.full_path: a mounted spec's full_path is in the target namespace
    [id, {
      description: "#{child_spec.description}#{annotation}",
      condition:   child_spec.condition,
      children:    generate_help(path + [id])
    }]
  end
end

#http_config ⇒ Aspera::Cli::Http

Returns:



311
# File 'lib/aspera/cli/plugins/base.rb', line 311

def http_config; @context.http_config; end

#invoke_action(action, args, ctx) ⇒ Object

Invoke an action (Symbol method or Proc block) with the given positional arguments and keyword context. Procs are executed via instance_exec so they share the plugin's self.

Parameters:

  • action (Symbol, Proc)
  • args (Array) —

    positional arguments

  • ctx (Hash) —

    keyword context

Returns:

  • (Object)


465
466
467
468
469
470
471
# File 'lib/aspera/cli/plugins/base.rb', line 465

def invoke_action(action, args, ctx)
  if action.is_a?(Proc)
    instance_exec(*args, **ctx, &action)
  else
    send(action, *args, **ctx)
  end
end

#options ⇒ Aspera::Cli::Parser

Returns:



299
# File 'lib/aspera/cli/plugins/base.rb', line 299

def options; @context.options; end

#persistency ⇒ Aspera::PersistencyFolder



307
# File 'lib/aspera/cli/plugins/base.rb', line 307

def persistency; @context.persistency; end

#presets ⇒ Aspera::Cli::PresetManager



309
# File 'lib/aspera/cli/plugins/base.rb', line 309

def presets; @context.presets; end

#progress_bar ⇒ Aspera::Cli::TransferProgress?



313
# File 'lib/aspera/cli/plugins/base.rb', line 313

def progress_bar; @context.progress_bar; end

#query_read_delete(default: nil, schema: nil) ⇒ Hash?

Query parameters in URL suitable for REST: list/GET and delete/DELETE

Parameters:

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

    Default query parameters

  • schema (String, nil) (defaults to: nil) —

    Contextual schema path for --query help display

Returns:

  • (Hash, nil) —

    Query parameters



711
712
713
714
715
716
717
718
719
720
721
722
# File 'lib/aspera/cli/plugins/base.rb', line 711

def query_read_delete(default: nil, schema: nil)
  # Dup default, as it could be frozen
  query = options.get_option(:query, schema: schema) || default&.dup
  Log.dump(:query_read_delete, query)
  begin
    # Check it is suitable
    URI.encode_www_form(query) unless query.nil?
  rescue StandardError => e
    raise Cli::BadArgument, "Query must be an extended value (Hash, Array) which can be encoded with URI.encode_www_form. Refer to manual. (#{e.message})"
  end
  return query
end

#resolve_argument(arg_spec) {|field, value| ... } ⇒ Object

Resolve a single positional argument from the CLI argument stream. When arg_spec.bulk is true, always returns an Array (normalized to [value] when non-bulk). For type: :identifier, an optional block provides the percent-selector lookup.

Parameters:

Yield Parameters:

  • field (String) —

    field name from a percent-selector (%field:value)

  • value (String) —

    value from a percent-selector

Yield Returns:

  • (String) —

    resolved identifier

Returns:

  • (Object) —

    the resolved value, or Array when arg_spec.bulk is true



517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
# File 'lib/aspera/cli/plugins/base.rb', line 517

def resolve_argument(arg_spec, &block)
  if arg_spec.bulk
    is_bulk = options.get_option(:bulk)
    if arg_spec.type.eql?(:identifier)
      val = options.instance_identifier(description: arg_spec.name.to_s, &block)
    else
      val = options.get_next_argument(
        arg_spec.name.to_s,
        mandatory: arg_spec.mandatory,
        validation: is_bulk ? Array : arg_spec.type,
        default:   arg_spec.default,
        schema:    arg_spec.schema
      )
      if is_bulk
        Aspera.assert_array_all(val, arg_spec.type, type: Cli::BadArgument) { 'type' } unless arg_spec.type.nil?
      end
    end
    # Always return an Array when bulk: true
    is_bulk ? val : [val]
  else
    case arg_spec.type
    when :identifier
      options.instance_identifier(description: arg_spec.name.to_s, &block)
    else
      # Class or Array<Class> -> pass as validation type
      # When interactive: true, set ask_missing_mandatory so that get_interactive is triggered
      # when no CLI arguments are provided (mandatory is forced to true for the same reason:
      # a non-nil default would short-circuit get_interactive before it is ever called).
      options.ask_missing_mandatory = true if arg_spec.interactive
      options.get_next_argument(
        arg_spec.name.to_s,
        mandatory:   arg_spec.interactive ? true : arg_spec.mandatory,
        multiple:    arg_spec.multiple || false,
        validation:  arg_spec.type,
        accept_list: arg_spec.allowed,
        default:     arg_spec.interactive ? nil : arg_spec.default,
        schema:      arg_spec.schema
      )
    end
  end
end

#resolve_arguments(arg_specs, ctx) ⇒ Hash

Resolve positional arguments from the CLI argument stream, in order. Arguments already present in ctx are not read again. For type: :identifier, the percent-selector lookup receives the ctx accumulated so far.

Parameters:

Returns:

  • (Hash) —

    ctx merged with the resolved arguments



493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'lib/aspera/cli/plugins/base.rb', line 493

def resolve_arguments(arg_specs, ctx)
  arg_specs.each do |arg_spec|
    next if ctx.key?(arg_spec.name)
    lookup_cb = arg_spec.lookup if arg_spec.type.eql?(:identifier)
    current = ctx
    block =
      case lookup_cb
      when nil    then nil
      when Symbol then ->(f, v) { send(lookup_cb, f, v, **current) }
      else ->(f, v) { instance_exec(f, v, **current, &lookup_cb) }
      end
    ctx = ctx.merge(arg_spec.name => resolve_argument(arg_spec, &block))
  end
  ctx
end

#transfer ⇒ Aspera::Cli::TransferAgent



301
# File 'lib/aspera/cli/plugins/base.rb', line 301

def transfer; @context.transfer; end

#validate_registry ⇒ Object

Validate the registry once per class (memoised by the ivar check). Passes the plugin class so implicit action methods can be verified.



442
443
444
445
446
# File 'lib/aspera/cli/plugins/base.rb', line 442

def validate_registry
  return if self.class.instance_variable_defined?(:@registry_validated)
  self.class.command_registry.validate!(plugin_class: self.class)
  self.class.instance_variable_set(:@registry_validated, true)
end