Class: Aspera::Cli::Plugins::Base
- Inherits:
-
Object
- Object
- Aspera::Cli::Plugins::Base
- Extended by:
- OptionDeclarator
- Defined in:
- lib/aspera/cli/plugins/base.rb
Overview
Base class for command plugins
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
-
#context ⇒ Object
readonly
Global objects.
-
#help_path ⇒ Object
readonly
Path reached in the command tree at the moment --help was intercepted.
Class Method Summary collapse
-
.application_name(name = nil) ⇒ String
DSL class method: declare the human-readable application name shown in wizards.
-
.command(id, **kwargs) ⇒ Object
DSL class method: register a command in this plugin's registry.
-
.command_registry ⇒ CommandRegistry
Per-class DSL registry (not inherited: each subclass gets its own instance).
-
.commands_under(parent, description: nil) ⇒ Object
DSL class method: scope block that sets a default parent for nested command() calls.
-
.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.
-
.declare_options(options, target: nil, parse: false) ⇒ Object
Declare all options of
option_sourcesonto a Parser instance. -
.define_action_method(path) {|keyword| ... } ⇒ Object
DSL class method: define an instance method whose name is derived from a path array.
-
.entity_noun(entity, singular: true) ⇒ String
Derive a lowercase noun from an entity path, singular unless told otherwise.
-
.file_matcher(match_expression) ⇒ Proc
Build a filter lambda from a match expression (String glob, Regexp, Proc, or nil).
-
.operation_description(verb, noun) ⇒ String
Standard description of a CRUD operation on an entity.
-
.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. -
.option_specs ⇒ Hash{Symbol => OptionSpec}
Options of this plugin class (
optionDSL, see OptionDeclarator). -
.register_option_spec(spec) ⇒ Object
Store an OptionSpec in the command registry.
-
.use_options(source) ⇒ Object
Include options from another plugin or OptionDeclarator module.
-
.used_option_sources ⇒ Array<Class, Module>
Option sources added with
use_options.
Instance Method Summary collapse
-
#action_for(spec) ⇒ Symbol, Proc
Resolve the action for a leaf CommandSpec.
- #add_manual_header(_has_options = true) ⇒ Object
-
#bulk_result(items, command:, id_result: 'id', fields: :default) {|item| ... } ⇒ Result::ObjectList, Result::SingleObject
Convenience wrapper: reads :bulk and :bfail from options, normalizes
itemsto an Array, then delegates to Result.bulk. - #config ⇒ Aspera::Cli::Plugins::Config
-
#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.
-
#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).
-
#dispatch_leaf(current_path, spec, ctx) ⇒ Object
Phase B, leaf branch: execute a spec that is already a leaf (no children).
-
#dispatch_mount(mount, command, ctx) ⇒ Object
Hand over dispatch of a mounted child to the target plugin instance.
-
#entity_create(api:, entity:, data:, display_fields: nil) ⇒ Object
Create one or more instances of an entity (supports bulk).
-
#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).
-
#entity_list(api:, entity:, display_fields: nil, items_key: nil, list_query: nil, query_component: nil) ⇒ Object
List all instances of an entity.
-
#entity_modify(api:, entity:, data:, id: nil, is_singleton: false, id_as_arg: false) ⇒ Object
Modify an existing instance of an entity.
-
#entity_res_path(entity, id, is_singleton: false, id_as_arg: false) ⇒ String
Build the resource path for an instance operation.
-
#entity_show(api:, entity:, id: nil, display_fields: nil, is_singleton: false, id_as_arg: false) ⇒ Object
Show one instance of an entity.
-
#execute_action ⇒ Object
Entry point for all DSL-based plugins.
-
#execute_leaf(spec, ctx) ⇒ Object
Execute a leaf CommandSpec: resolve arguments and call action.
- #formatter ⇒ Aspera::Cli::Formatter
-
#generate_help(path = []) ⇒ Hash
Build a nested Hash tree of the registered command tree for help display.
- #http_config ⇒ Aspera::Cli::Http
-
#initialize(context:) ⇒ Base
constructor
A new instance of Base.
-
#invoke_action(action, args, ctx) ⇒ Object
Invoke an action (Symbol method or Proc block) with the given positional arguments and keyword context.
- #options ⇒ Aspera::Cli::Parser
- #persistency ⇒ Aspera::PersistencyFolder
- #presets ⇒ Aspera::Cli::PresetManager
- #progress_bar ⇒ Aspera::Cli::TransferProgress?
-
#query_read_delete(default: nil, schema: nil) ⇒ Hash?
Query parameters in URL suitable for REST: list/
GETand delete/DELETE. -
#resolve_argument(arg_spec) {|field, value| ... } ⇒ Object
Resolve a single positional argument from the CLI argument stream.
-
#resolve_arguments(arg_specs, ctx) ⇒ Hash
Resolve positional arguments from the CLI argument stream, in order.
- #transfer ⇒ Aspera::Cli::TransferAgent
-
#validate_registry ⇒ Object
Validate the registry once per class (memoised by the ivar check).
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. .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.(, 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.
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.
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).
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
parent is always resolved relative to the current scope:
Array(@current_parent) + Array(parent)
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.
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.
237 238 239 240 241 242 243 244 |
# File 'lib/aspera/cli/plugins/base.rb', line 237 def (, target: nil, parse: false) option_sources.each do |src| src.option_specs.each_value do |spec| spec.declare_on(, target: target) unless .option_declared?(spec.name) end end . 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)
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'
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).
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'
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.
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.
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.
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.
37 38 39 |
# File 'lib/aspera/cli/plugins/base.rb', line 37 def (source) used_option_sources << source unless used_option_sources.include?(source) end |
.used_option_sources ⇒ Array<Class, Module>
Option sources added with use_options.
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).
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( = 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, ...)
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: .get_option(:bulk), command: command, id_result: id_result, fields: fields, bfail: .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`).
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 && .command_or_arg_empty? @help_path = current_path raise Cli::HelpRequest, self end command = .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 && .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).
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) .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.
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.
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).
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).
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.
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.
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.
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.
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.
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.
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
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.
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
299 |
# File 'lib/aspera/cli/plugins/base.rb', line 299 def ; @context.; 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 ; @context.; end |
#query_read_delete(default: nil, schema: nil) ⇒ Hash?
Query parameters in URL suitable for REST: list/GET and delete/DELETE
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 = .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.})" 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.
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 = .get_option(:bulk) if arg_spec.type.eql?(:identifier) val = .instance_identifier(description: arg_spec.name.to_s, &block) else val = .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 .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). .ask_missing_mandatory = true if arg_spec.interactive .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.
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 |