aspera-cli 4.26.2 → 4.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +310 -21
- data/CONTRIBUTING.md +99 -14
- data/TODO.md +50 -0
- data/bin/ascli +3 -3
- data/docs/README.md +10977 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +29 -7
- data/lib/aspera/agent/connect.rb +58 -21
- data/lib/aspera/agent/desktop.rb +53 -18
- data/lib/aspera/agent/direct.rb +71 -40
- data/lib/aspera/agent/factory.rb +3 -3
- data/lib/aspera/agent/httpgw.rb +55 -14
- data/lib/aspera/agent/node.rb +34 -2
- data/lib/aspera/agent/transferd.rb +48 -12
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +42 -44
- data/lib/aspera/api/cos_node.rb +10 -9
- data/lib/aspera/api/faspex.rb +16 -15
- data/lib/aspera/api/httpgw.rb +23 -23
- data/lib/aspera/api/node.rb +51 -70
- data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
- data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
- data/lib/aspera/ascmd.rb +32 -30
- data/lib/aspera/ascp/installation.rb +75 -69
- data/lib/aspera/ascp/management.rb +7 -8
- data/lib/aspera/assert.rb +12 -13
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +125 -0
- data/lib/aspera/cli/bootstrapper.rb +197 -0
- data/lib/aspera/cli/command_registry.rb +145 -0
- data/lib/aspera/cli/command_spec.rb +141 -0
- data/lib/aspera/cli/context.rb +19 -6
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +27 -26
- data/lib/aspera/cli/formatter.rb +95 -59
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +3 -3
- data/lib/aspera/cli/http.rb +74 -30
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +225 -0
- data/lib/aspera/cli/option_declarator.rb +77 -0
- data/lib/aspera/cli/options.schema.yaml +634 -7
- data/lib/aspera/cli/parser.rb +1217 -0
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1178 -881
- data/lib/aspera/cli/plugins/ats.rb +205 -162
- data/lib/aspera/cli/plugins/base.rb +703 -175
- data/lib/aspera/cli/plugins/basic_auth.rb +7 -9
- data/lib/aspera/cli/plugins/config.rb +434 -757
- data/lib/aspera/cli/plugins/console.rb +107 -65
- data/lib/aspera/cli/plugins/cos.rb +46 -33
- data/lib/aspera/cli/plugins/factory.rb +6 -6
- data/lib/aspera/cli/plugins/faspex5.rb +627 -388
- data/lib/aspera/cli/plugins/faspio.rb +54 -51
- data/lib/aspera/cli/plugins/httpgw.rb +18 -25
- data/lib/aspera/cli/plugins/mcp.rb +279 -0
- data/lib/aspera/cli/plugins/node.rb +988 -840
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +112 -135
- data/lib/aspera/cli/plugins/preview.rb +195 -157
- data/lib/aspera/cli/plugins/server.rb +141 -91
- data/lib/aspera/cli/plugins/shares.rb +343 -110
- data/lib/aspera/cli/preset_actions.rb +159 -0
- data/lib/aspera/cli/preset_manager.rb +82 -39
- data/lib/aspera/cli/result.rb +78 -26
- data/lib/aspera/cli/runner.rb +323 -153
- data/lib/aspera/cli/secret_finder.rb +40 -0
- data/lib/aspera/cli/special_values.rb +1 -0
- data/lib/aspera/cli/sync_actions.rb +89 -64
- data/lib/aspera/cli/terminal_formatter.rb +3 -3
- data/lib/aspera/cli/transfer_actions.rb +90 -0
- data/lib/aspera/cli/transfer_agent.rb +129 -64
- data/lib/aspera/cli/transfer_progress.rb +9 -9
- data/lib/aspera/cli/vault_manager.rb +79 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +28 -30
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +34 -34
- data/lib/aspera/command_line_converter.rb +1 -1
- data/lib/aspera/coverage.rb +1 -2
- data/lib/aspera/data_repository.rb +2 -1
- data/lib/aspera/dot_container.rb +12 -12
- data/lib/aspera/environment.rb +30 -23
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +5 -5
- data/lib/aspera/faspex_postproc.rb +16 -10
- data/lib/aspera/graphql.rb +37 -0
- data/lib/aspera/hash_ext.rb +8 -2
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/base.rb +10 -3
- data/lib/aspera/keychain/encrypted_hash.rb +92 -17
- data/lib/aspera/keychain/factory.rb +20 -8
- data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +23 -25
- data/lib/aspera/keychain/one_password_api.rb +86 -0
- data/lib/aspera/keychain/one_password_base.rb +34 -0
- data/lib/aspera/keychain/one_password_cli.rb +98 -0
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +29 -15
- data/lib/aspera/markdown.rb +90 -7
- data/lib/aspera/nagios.rb +8 -8
- data/lib/aspera/node_simulator.rb +35 -27
- data/lib/aspera/oauth/base.rb +9 -10
- data/lib/aspera/oauth/boot.rb +5 -5
- data/lib/aspera/oauth/factory.rb +12 -10
- data/lib/aspera/oauth/jwt.rb +9 -11
- data/lib/aspera/oauth/web.rb +6 -6
- data/lib/aspera/persistency_action_once.rb +12 -12
- data/lib/aspera/persistency_folder.rb +18 -11
- data/lib/aspera/preview/file_types.rb +5 -5
- data/lib/aspera/preview/generator.rb +52 -49
- data/lib/aspera/preview/options.rb +3 -2
- data/lib/aspera/preview/terminal.rb +5 -5
- data/lib/aspera/preview/utils.rb +66 -19
- data/lib/aspera/products/connect.rb +2 -2
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +3 -3
- data/lib/aspera/products/transferd.rb +4 -4
- data/lib/aspera/proxy_auto_config.rb +15 -14
- data/lib/aspera/rest.rb +159 -151
- data/lib/aspera/rest_error_analyzer.rb +6 -6
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +11 -10
- data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
- data/lib/aspera/schema/documentation.rb +65 -28
- data/lib/aspera/schema/reader.rb +136 -13
- data/lib/aspera/schema/registry.rb +60 -8
- data/lib/aspera/secret_hider.rb +12 -7
- data/lib/aspera/ssh.rb +64 -31
- data/lib/aspera/ssl.rb +5 -5
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/database.rb +2 -2
- data/lib/aspera/sync/operations.rb +22 -25
- data/lib/aspera/temp_file_manager.rb +25 -5
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +36 -34
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +10 -10
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +12 -3
- data/lib/aspera/uri_reader.rb +52 -15
- data/lib/aspera/web_auth.rb +8 -7
- data/lib/aspera/web_server_simple.rb +15 -12
- data/lib/aspera/yaml.rb +5 -4
- data.tar.gz.sig +0 -0
- metadata +34 -6
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/manager.rb +0 -766
- data/lib/aspera/cli/plugins/faspex.rb +0 -539
- data/lib/aspera/json_rpc.rb +0 -52
- data/lib/aspera/transfer/uri.rb +0 -56
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'aspera/cli/extended_value'
|
|
4
|
+
require 'aspera/cli/parser'
|
|
4
5
|
require 'aspera/assert'
|
|
5
6
|
require 'aspera/cli/result'
|
|
7
|
+
require 'aspera/cli/command_registry'
|
|
8
|
+
require 'aspera/cli/option_declarator'
|
|
9
|
+
require 'aspera/schema/registry'
|
|
6
10
|
|
|
7
11
|
module Aspera
|
|
8
12
|
module Cli
|
|
@@ -20,25 +24,323 @@ module Aspera
|
|
|
20
24
|
ALL = (GLOBAL + INSTANCE).freeze
|
|
21
25
|
end
|
|
22
26
|
class << self
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
# Per-class DSL registry (not inherited: each subclass gets its own instance).
|
|
28
|
+
# @return [CommandRegistry]
|
|
29
|
+
def used_option_sources
|
|
30
|
+
@used_option_sources ||= []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Include options from another plugin or OptionDeclarator module.
|
|
34
|
+
# @param source [Class, Module]
|
|
35
|
+
def use_options(source)
|
|
36
|
+
used_option_sources << source unless used_option_sources.include?(source)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def command_registry
|
|
40
|
+
@command_registry ||= CommandRegistry.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# DSL class method: register a command in this plugin's registry.
|
|
44
|
+
# Inherits parent from the enclosing commands_under block when parent: is omitted.
|
|
45
|
+
# @param id [Symbol]
|
|
46
|
+
# @param kwargs [Hash] forwarded to CommandSpec
|
|
47
|
+
def command(id, **kwargs)
|
|
48
|
+
kwargs[:parent] = @current_parent if kwargs[:parent].nil? && @current_parent
|
|
49
|
+
command_registry.register(CommandSpec.new(id: id, **kwargs))
|
|
50
|
+
end
|
|
51
|
+
|
|
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
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# DSL class method: declare CRUD commands for a REST entity.
|
|
60
|
+
#
|
|
61
|
+
# 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
|
|
64
|
+
# (:show, :modify, :delete) when not a singleton; none for global verbs
|
|
65
|
+
# - action: calls entity_<verb>(api:, entity:, **shared_kwargs, **ctx)
|
|
66
|
+
#
|
|
67
|
+
# api: is resolved at runtime: :@ivar -> instance_variable_get, else -> send.
|
|
68
|
+
# entity: may also be a Symbol — resolved at runtime as a ctx key (e.g. :sf_entity).
|
|
69
|
+
# This covers cases where the entity path is injected by a parent setup: method.
|
|
70
|
+
#
|
|
71
|
+
# @param api [Symbol, String] Runtime API ref (:@ivar or method name) or literal string
|
|
72
|
+
# @param entity [String, Symbol] REST sub-path, or ctx key Symbol resolved at runtime
|
|
73
|
+
# @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)
|
|
75
|
+
# @param lookup [Symbol, nil] Instance method for percent-selector resolution
|
|
76
|
+
# @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)
|
|
79
|
+
operations ||= Operations::ALL
|
|
80
|
+
operations.each do |verb|
|
|
81
|
+
id_arg = ({name: :id, type: :identifier, lookup: lookup} if Operations::INSTANCE.include?(verb) && !kwargs[:is_singleton])
|
|
82
|
+
schema_val =
|
|
83
|
+
if kwargs[:body_component] && entity.is_a?(String)
|
|
84
|
+
case verb
|
|
85
|
+
when :create then Schema::Registry.req_body(kwargs[:body_component], "#{entity}.post")
|
|
86
|
+
when :modify then Schema::Registry.req_body(kwargs[:body_component], "#{entity}/{id}.put")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
args =
|
|
90
|
+
case verb
|
|
91
|
+
when :create
|
|
92
|
+
[{name: :data, type: Hash, bulk: true, schema: schema_val}]
|
|
93
|
+
when :modify
|
|
94
|
+
[id_arg, {name: :data, type: Hash, schema: schema_val}].compact
|
|
95
|
+
when :delete
|
|
96
|
+
id_arg ? [id_arg.merge(bulk: true)] : nil
|
|
97
|
+
else
|
|
98
|
+
id_arg ? [id_arg] : nil
|
|
99
|
+
end
|
|
100
|
+
action_proc = lambda do |**ctx|
|
|
101
|
+
resolved_api =
|
|
102
|
+
if api.is_a?(Symbol)
|
|
103
|
+
api.to_s.start_with?('@') ? instance_variable_get(api) : send(api)
|
|
104
|
+
elsif api.is_a?(Proc)
|
|
105
|
+
instance_exec(&api)
|
|
106
|
+
else
|
|
107
|
+
api
|
|
108
|
+
end
|
|
109
|
+
resolved_entity = entity.is_a?(Symbol) ? ctx.fetch(entity) : entity
|
|
110
|
+
send(:"entity_#{verb}", api: resolved_api, entity: resolved_entity, **kwargs, **ctx)
|
|
111
|
+
end
|
|
112
|
+
cmd_attrs = {description: "#{verb.capitalize} #{name || entity.inspect}", action: action_proc}
|
|
113
|
+
cmd_attrs[:arguments] = args if args
|
|
114
|
+
command(verb, **cmd_attrs)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# DSL class method: define an instance method whose name is derived from a path array.
|
|
119
|
+
# Equivalent to: define_method(CommandSpec.action_method(path), &block)
|
|
120
|
+
# @param path [Array<Symbol>] command path segments, e.g. [:admin, :user, :list]
|
|
121
|
+
# @yieldparam [Hash] keyword context forwarded from dispatch
|
|
122
|
+
def define_action_method(path, &block)
|
|
123
|
+
define_method(CommandSpec.action_method(path), &block)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# DSL class method: scope block that sets a default parent for nested command() calls.
|
|
127
|
+
# Fully re-entrant: blocks may be nested for multi-level parent paths.
|
|
128
|
+
# If the terminal node of `parent` has not been declared yet, it is auto-declared
|
|
129
|
+
# as an intermediate command with description: "Manage <name>" (or the given description:).
|
|
130
|
+
#
|
|
131
|
+
# `parent` is always resolved relative to the current scope:
|
|
132
|
+
# Array(@current_parent) + Array(parent)
|
|
133
|
+
#
|
|
134
|
+
# @param parent [Symbol, Array<Symbol>] one or more path segments, relative to current scope
|
|
135
|
+
# @param description [String, nil] Description of entity for the auto-declared node
|
|
136
|
+
# @yieldreturn [void]
|
|
137
|
+
def commands_under(parent, description: nil)
|
|
138
|
+
# Always relative: append the given segments to the current scope.
|
|
139
|
+
path = Array(@current_parent) + Array(parent)
|
|
140
|
+
unless command_registry[path]
|
|
141
|
+
id = path.last
|
|
142
|
+
desc = description || "Manage #{entity_display_name(id)}"
|
|
143
|
+
parent_path = path[0..-2]
|
|
144
|
+
saved = @current_parent
|
|
145
|
+
@current_parent = parent_path.empty? ? nil : parent_path
|
|
146
|
+
command(id, description: desc)
|
|
147
|
+
@current_parent = saved
|
|
148
|
+
end
|
|
149
|
+
previous = @current_parent
|
|
150
|
+
@current_parent = path
|
|
151
|
+
yield
|
|
152
|
+
ensure
|
|
153
|
+
@current_parent = previous
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# DSL class method: declare an option in this plugin's registry.
|
|
157
|
+
# Metadata is stored as an OptionSpec at class-load time; the actual
|
|
158
|
+
# options.declare call happens in Base#initialize once the instance exists.
|
|
159
|
+
#
|
|
160
|
+
# Raises ArgumentError at class-load time if the same option name is already
|
|
161
|
+
# 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)
|
|
178
|
+
ancestor_owner = ancestors.drop(1).find do |klass|
|
|
179
|
+
klass.is_a?(Class) && klass <= Base &&
|
|
180
|
+
klass.instance_variable_defined?(:@command_registry) &&
|
|
181
|
+
klass.command_registry.option_specs.key?(name)
|
|
182
|
+
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
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
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 = []
|
|
204
|
+
ancestors.each do |klass|
|
|
205
|
+
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)
|
|
208
|
+
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
|
+
)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
options.parse_options! if parse
|
|
237
|
+
end
|
|
238
|
+
|
|
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
|
+
# DSL class method: declare the human-readable application name shown in wizards.
|
|
252
|
+
# When called with an argument, sets the name. When called with no argument, returns it.
|
|
253
|
+
# Falls back to the last component of the class name if never set.
|
|
254
|
+
# @param name [String, nil]
|
|
255
|
+
# @return [String]
|
|
256
|
+
def application_name(name = nil)
|
|
257
|
+
@application_name = name unless name.nil?
|
|
258
|
+
@application_name || self.name.split('::').last
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Build a filter lambda from a match expression (String glob, Regexp, Proc, or nil).
|
|
262
|
+
# @param match_expression [String, Regexp, Proc, NilClass] as in FILTER_ARGS
|
|
263
|
+
# @return [Proc] lambda(entry) -> Boolean
|
|
264
|
+
def file_matcher(match_expression)
|
|
265
|
+
case match_expression
|
|
266
|
+
when Proc then match_expression
|
|
267
|
+
when Regexp then ->(f) { f['name'].match?(match_expression) }
|
|
268
|
+
when String then ->(f) { File.fnmatch(match_expression, f['name'], File::FNM_DOTMATCH) }
|
|
269
|
+
when NilClass then ->(_) { true }
|
|
270
|
+
else Aspera.error_unexpected_value(match_expression.class.name, type: ParameterError)
|
|
271
|
+
end
|
|
27
272
|
end
|
|
28
273
|
end
|
|
29
274
|
|
|
275
|
+
# Shared positional argument for commands that accept an optional file name filter.
|
|
276
|
+
# Accepted types: String (shell glob matched against entry name), Regexp, or Proc.
|
|
277
|
+
# Used by node files find, and preview scan/events/trevents.
|
|
278
|
+
FILTER_ARGS = [{name: :filter, type: [String, Regexp, Proc], description: 'File name filter: String (glob), Regexp, or Proc', mandatory: false, default: nil}].freeze
|
|
279
|
+
|
|
280
|
+
option :query, description: 'Additional filter for for some commands (list/delete)', allowed: [Hash, Array, NilClass]
|
|
281
|
+
option :bulk, description: 'Bulk operation (only some)', allowed: Type::BOOLEAN, default: false
|
|
282
|
+
option :bfail, description: 'Bulk operation error handling', allowed: Type::BOOLEAN, default: true
|
|
283
|
+
|
|
30
284
|
def initialize(context:)
|
|
31
|
-
|
|
32
|
-
Aspera.
|
|
33
|
-
Aspera.assert(self.class.const_defined?(:ACTIONS), type: InternalError){"Missing constant 'ACTIONS' in #{self.class}"}
|
|
285
|
+
Aspera.assert_type(context, Context) { 'context' }
|
|
286
|
+
Aspera.assert_type(context.man_header, TrueClass, FalseClass) { 'context.man_header' }
|
|
34
287
|
@context = context
|
|
35
|
-
|
|
288
|
+
# Switch to the plugin-specific options group so that all options declared
|
|
289
|
+
# below (DSL-registered and imperative) appear under the plugin section in
|
|
290
|
+
# --help output, separate from the global options.
|
|
291
|
+
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
|
|
36
335
|
end
|
|
37
336
|
|
|
38
337
|
# Global objects
|
|
39
338
|
attr_reader :context
|
|
339
|
+
# Path reached in the command tree at the moment --help was intercepted.
|
|
340
|
+
# Nil until set by dispatch_from_registry.
|
|
341
|
+
attr_reader :help_path
|
|
40
342
|
|
|
41
|
-
# @return [Aspera::Cli::
|
|
343
|
+
# @return [Aspera::Cli::Parser]
|
|
42
344
|
def options; @context.options; end
|
|
43
345
|
# @return [Aspera::Cli::TransferAgent]
|
|
44
346
|
def transfer; @context.transfer; end
|
|
@@ -52,160 +354,414 @@ module Aspera
|
|
|
52
354
|
def presets; @context.presets; end
|
|
53
355
|
# @return [Aspera::Cli::Http]
|
|
54
356
|
def http_config; @context.http_config; end
|
|
357
|
+
# @return [Aspera::Cli::TransferProgress, nil]
|
|
358
|
+
def progress_bar; @context.progress_bar; end
|
|
359
|
+
|
|
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
|
+
# Entry point for all DSL-based plugins.
|
|
366
|
+
def execute_action
|
|
367
|
+
@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)
|
|
382
|
+
end
|
|
55
383
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Aspera.assert(block_given?, 'missing block')
|
|
76
|
-
is_bulk = options.get_option(:bulk)
|
|
77
|
-
case values
|
|
78
|
-
when :identifier
|
|
79
|
-
values = options.instance_identifier(description: descr)
|
|
80
|
-
when Class
|
|
81
|
-
values = value_create_modify(command: command, description: descr, type: values, bulk: is_bulk, schema: schema)
|
|
82
|
-
end
|
|
83
|
-
# If not bulk, there is a single value
|
|
84
|
-
params = is_bulk ? values : [values]
|
|
85
|
-
Log.log.warn('Empty list given for bulk operation') if params.empty?
|
|
86
|
-
Log.dump(:bulk_operation, params)
|
|
87
|
-
result_list = []
|
|
88
|
-
params.each do |param|
|
|
89
|
-
# Init for delete
|
|
90
|
-
result = {id_result => param}
|
|
91
|
-
begin
|
|
92
|
-
# Execute custom code
|
|
93
|
-
res = yield(param)
|
|
94
|
-
# If block returns a hash, let's use this (create)
|
|
95
|
-
result = res if res.is_a?(Hash)
|
|
96
|
-
# TODO: remove when faspio gw api fixes this
|
|
97
|
-
result = res.first if res.is_a?(Array) && res.first.is_a?(Hash)
|
|
98
|
-
# Create -> created
|
|
99
|
-
result['status'] = "#{command}#{'e' unless command.to_s.end_with?('e')}d".gsub(/yed$/, 'ied')
|
|
100
|
-
rescue StandardError => e
|
|
101
|
-
raise e if options.get_option(:bfail)
|
|
102
|
-
result['status'] = e.to_s
|
|
384
|
+
# Two-phase dispatcher: run setup on the current node (Phase A), then either
|
|
385
|
+
# execute a leaf directly or consume the next argument and recurse (Phase B).
|
|
386
|
+
# @param current_path [Array<Symbol>] path of the node currently being dispatched
|
|
387
|
+
# @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
|
+
# @return [Object] result suitable for CLI output
|
|
390
|
+
def dispatch_from_registry(current_path, ctx = {}, skip_setup: false)
|
|
391
|
+
registry = self.class.command_registry
|
|
392
|
+
spec = registry[current_path]
|
|
393
|
+
is_leaf = spec && registry.children_of(current_path).empty?
|
|
394
|
+
|
|
395
|
+
if options.help_requested || skip_setup
|
|
396
|
+
# help_requested on an intermediate node: drain positional args without validation
|
|
397
|
+
# 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|
|
|
400
|
+
next if ctx.key?(arg_spec.name)
|
|
401
|
+
options.get_next_argument(arg_spec.name.to_s, mandatory: false)
|
|
402
|
+
end
|
|
103
403
|
end
|
|
104
|
-
|
|
404
|
+
else
|
|
405
|
+
# Phase A - for intermediate nodes only: resolve all ArgumentSpec declared on this node
|
|
406
|
+
# 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
|
|
425
|
+
ctx = ctx.merge(send(spec.setup, **ctx)) if spec&.setup
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Phase B - leaf fast-path or child dispatch
|
|
429
|
+
if is_leaf
|
|
430
|
+
dispatch_leaf(current_path, spec, ctx)
|
|
431
|
+
else
|
|
432
|
+
dispatch_child(current_path, registry, ctx)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Phase B, leaf branch: execute a spec that is already a leaf (no children).
|
|
437
|
+
# Intercepts --help before calling execute_leaf.
|
|
438
|
+
# @param current_path [Array<Symbol>]
|
|
439
|
+
# @param spec [CommandSpec]
|
|
440
|
+
# @param ctx [Hash]
|
|
441
|
+
# @return [Object]
|
|
442
|
+
def dispatch_leaf(current_path, spec, ctx)
|
|
443
|
+
if options.help_requested
|
|
444
|
+
@help_path = current_path
|
|
445
|
+
raise Cli::HelpRequest, self
|
|
446
|
+
end
|
|
447
|
+
execute_leaf(spec, ctx)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Phase B, child branch: consume the next command argument, resolve the matching
|
|
451
|
+
# child spec, handle delegation, and recurse or execute.
|
|
452
|
+
# --help is intercepted at two points:
|
|
453
|
+
# 1. Before get_next_command when no positional arg is pending: raises HelpRequest
|
|
454
|
+
# immediately so the subcommand list with descriptions is shown rather than a
|
|
455
|
+
# MissingArgument error.
|
|
456
|
+
# 2. After get_next_command when no further args remain: raises HelpRequest scoped
|
|
457
|
+
# to the consumed command (e.g. `aoc files find -h`).
|
|
458
|
+
# @param current_path [Array<Symbol>]
|
|
459
|
+
# @param registry [CommandRegistry]
|
|
460
|
+
# @param ctx [Hash]
|
|
461
|
+
# @return [Object]
|
|
462
|
+
def dispatch_child(current_path, registry, ctx)
|
|
463
|
+
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|
|
|
466
|
+
Array(c.aliases).each { |a| h[a] = c.id } if c.aliases
|
|
105
467
|
end
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
468
|
+
|
|
469
|
+
# Intercept --help before consuming the command token when no arg is pending.
|
|
470
|
+
# This avoids MissingArgument being raised by get_next_command before HelpRequest.
|
|
471
|
+
if options.help_requested && options.command_or_arg_empty?
|
|
472
|
+
@help_path = current_path
|
|
473
|
+
raise Cli::HelpRequest, self
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
command = options.get_next_command(available.keys, aliases: aliases.empty? ? nil : aliases)
|
|
477
|
+
child = available[command]
|
|
478
|
+
|
|
479
|
+
# Intercept --help after a command was consumed but no further args remain.
|
|
480
|
+
# (e.g. `aoc files find -h`). When further args remain, keep recursing.
|
|
481
|
+
if options.help_requested && options.command_or_arg_empty?
|
|
482
|
+
@help_path = current_path + [command]
|
|
483
|
+
raise Cli::HelpRequest, self
|
|
484
|
+
end
|
|
485
|
+
|
|
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
|
|
492
|
+
|
|
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)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Resolve the action for a leaf CommandSpec.
|
|
498
|
+
# Returns spec.action (Symbol or Proc) if explicitly set; otherwise derives a Symbol
|
|
499
|
+
# from the full path as :action_<path_segment_1>_<path_segment_2>_...
|
|
500
|
+
# (e.g. [:access_key, :list] -> :action_access_key_list).
|
|
501
|
+
# @param spec [CommandSpec]
|
|
502
|
+
# @return [Symbol, Proc]
|
|
503
|
+
def action_for(spec)
|
|
504
|
+
spec.action || spec.action_method_name
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Invoke an action (Symbol method or Proc block) with the given positional
|
|
508
|
+
# arguments and keyword context.
|
|
509
|
+
# Procs are executed via instance_exec so they share the plugin's `self`.
|
|
510
|
+
# @param action [Symbol, Proc]
|
|
511
|
+
# @param args [Array] positional arguments
|
|
512
|
+
# @param ctx [Hash] keyword context
|
|
513
|
+
# @return [Object]
|
|
514
|
+
def invoke_action(action, args, ctx)
|
|
515
|
+
if action.is_a?(Proc)
|
|
516
|
+
instance_exec(*args, **ctx, &action)
|
|
109
517
|
else
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
# @param
|
|
120
|
-
# @param
|
|
121
|
-
# @
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
schema: nil,
|
|
139
|
-
&block
|
|
140
|
-
)
|
|
141
|
-
command = options.get_next_command(Operations::ALL) if command.nil?
|
|
142
|
-
if is_singleton
|
|
143
|
-
one_res_path = entity
|
|
144
|
-
elsif Operations::INSTANCE.include?(command)
|
|
145
|
-
one_res_id = options.instance_identifier(&block)
|
|
146
|
-
one_res_path = "#{entity}/#{one_res_id}"
|
|
147
|
-
one_res_path = "#{entity}?#{id_as_arg}=#{one_res_id}" if id_as_arg
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
case command
|
|
151
|
-
when :create
|
|
152
|
-
raise BadArgument, 'cannot create singleton' if is_singleton
|
|
153
|
-
return do_bulk_operation(command: command, descr: 'data', fields: display_fields, schema: schema) do |params|
|
|
154
|
-
api.create(entity, params)
|
|
518
|
+
send(action, *args, **ctx)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# 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.
|
|
527
|
+
# @param spec [CommandSpec] a leaf node (no children)
|
|
528
|
+
# @param ctx [Hash] accumulated context (pre-resolved keys are not re-consumed)
|
|
529
|
+
# @return [Object]
|
|
530
|
+
def execute_leaf(spec, ctx)
|
|
531
|
+
a = action_for(spec)
|
|
532
|
+
# Always resolve declared arguments (even when transfer_paths is set — those arguments
|
|
533
|
+
# 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))
|
|
155
546
|
end
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
547
|
+
end
|
|
548
|
+
invoke_action(a, [], ctx)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Resolve a single positional argument from the CLI argument stream.
|
|
552
|
+
# When arg_spec.bulk is true, always returns an Array (normalized to [value] when non-bulk).
|
|
553
|
+
# For type: :identifier, an optional block provides the percent-selector lookup.
|
|
554
|
+
# @param arg_spec [ArgumentSpec]
|
|
555
|
+
# @yieldparam field [String] field name from a percent-selector (%field:value)
|
|
556
|
+
# @yieldparam value [String] value from a percent-selector
|
|
557
|
+
# @yieldreturn [String] resolved identifier
|
|
558
|
+
# @return [Object] the resolved value, or Array when arg_spec.bulk is true
|
|
559
|
+
def resolve_argument(arg_spec, &block)
|
|
560
|
+
if arg_spec.bulk
|
|
561
|
+
is_bulk = options.get_option(:bulk)
|
|
562
|
+
if arg_spec.type.eql?(:identifier)
|
|
563
|
+
val = options.instance_identifier(description: arg_spec.name.to_s, &block)
|
|
564
|
+
else
|
|
565
|
+
val = options.get_next_argument(
|
|
566
|
+
arg_spec.name.to_s,
|
|
567
|
+
mandatory: arg_spec.mandatory,
|
|
568
|
+
validation: is_bulk ? Array : arg_spec.type,
|
|
569
|
+
default: arg_spec.default,
|
|
570
|
+
schema: arg_spec.schema
|
|
166
571
|
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
api.delete("#{entity}/#{one_id}", query_read_delete)
|
|
171
|
-
{'id' => one_id}
|
|
572
|
+
if is_bulk
|
|
573
|
+
Aspera.assert_array_all(val, arg_spec.type, type: Cli::BadArgument) { 'type' } unless arg_spec.type.nil?
|
|
574
|
+
end
|
|
172
575
|
end
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if http['Content-Type'].start_with?('application/vnd.api+json')
|
|
180
|
-
Log.log.debug('is vnd.api')
|
|
181
|
-
data = data[entity]
|
|
182
|
-
end
|
|
183
|
-
data = data[items_key] if items_key
|
|
184
|
-
case data
|
|
185
|
-
when Hash
|
|
186
|
-
return Result::SingleObject.new(data, fields: display_fields)
|
|
187
|
-
when Array
|
|
188
|
-
return Result::ObjectList.new(data, fields: display_fields) if data.empty? || data.first.is_a?(Hash)
|
|
189
|
-
return Result::ValueList.new(data)
|
|
576
|
+
# Always return an Array when bulk: true
|
|
577
|
+
is_bulk ? val : [val]
|
|
578
|
+
else
|
|
579
|
+
case arg_spec.type
|
|
580
|
+
when :identifier
|
|
581
|
+
options.instance_identifier(description: arg_spec.name.to_s, &block)
|
|
190
582
|
else
|
|
191
|
-
|
|
583
|
+
# 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
|
+
)
|
|
192
597
|
end
|
|
193
|
-
when :modify
|
|
194
|
-
parameters = value_create_modify(command: command, schema: schema)
|
|
195
|
-
api.update(one_res_path, parameters)
|
|
196
|
-
return Result::Status.new('modified')
|
|
197
|
-
else
|
|
198
|
-
Aspera.error_unexpected_value(command){'command'}
|
|
199
598
|
end
|
|
200
599
|
end
|
|
201
600
|
|
|
601
|
+
# Build a nested Hash tree of the registered command tree for help display.
|
|
602
|
+
# Conditional commands are included with a '[condition_name]' annotation.
|
|
603
|
+
# @param path [Array<Symbol>] starting path ([] for the full tree)
|
|
604
|
+
# @return [Hash] { command_id => { description:, condition:, children: } }
|
|
605
|
+
def generate_help(path = [])
|
|
606
|
+
self.class.command_registry.children_of(path).transform_values do |child_spec|
|
|
607
|
+
annotation = child_spec.condition ? " [#{child_spec.condition}]" : ''
|
|
608
|
+
{
|
|
609
|
+
description: "#{child_spec.description}#{annotation}",
|
|
610
|
+
condition: child_spec.condition,
|
|
611
|
+
children: generate_help(child_spec.full_path)
|
|
612
|
+
}
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# Convenience wrapper: reads :bulk and :bfail from options, normalises +items+
|
|
617
|
+
# to an Array, then delegates to Result.bulk.
|
|
618
|
+
# Use this in action methods instead of the three-line boilerplate:
|
|
619
|
+
# is_bulk = options.get_option(:bulk)
|
|
620
|
+
# items = x.is_a?(Array) ? x : [x]
|
|
621
|
+
# Result.bulk(items, is_bulk: is_bulk, ...)
|
|
622
|
+
# @param items [Object, Array] Single item or Array; wrapped in Array when needed
|
|
623
|
+
# @param command [Symbol] Operation name (:create, :delete, ...)
|
|
624
|
+
# @param id_result [String] Key used as item identifier in the result row
|
|
625
|
+
# @param fields [Object] Fields hint passed to Result constructor (non-bulk only)
|
|
626
|
+
# @yieldparam item [Object] Each item in +items+
|
|
627
|
+
# @return [Result::ObjectList, Result::SingleObject]
|
|
628
|
+
def bulk_result(items, command:, id_result: 'id', fields: :default, &block)
|
|
629
|
+
items = items.is_a?(Array) ? items : [items]
|
|
630
|
+
Result.bulk(
|
|
631
|
+
items,
|
|
632
|
+
is_bulk: options.get_option(:bulk),
|
|
633
|
+
command: command,
|
|
634
|
+
id_result: id_result,
|
|
635
|
+
fields: fields,
|
|
636
|
+
bfail: options.get_option(:bfail),
|
|
637
|
+
&block
|
|
638
|
+
)
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# --- Per-verb entity action methods ---
|
|
642
|
+
# Each method handles exactly one CRUD verb.
|
|
643
|
+
# The resource id (when needed) is received as `id:` from ctx — it must be
|
|
644
|
+
# resolved upstream via an ArgumentSpec(type: :identifier) on the command,
|
|
645
|
+
# NOT read from the CLI queue inside the method.
|
|
646
|
+
|
|
647
|
+
# List all instances of an entity.
|
|
648
|
+
# @param api [Aspera::Rest] REST API object
|
|
649
|
+
# @param entity [String] API sub-path
|
|
650
|
+
# @param display_fields [Array, nil] Fields to display
|
|
651
|
+
# @param items_key [String, nil] Sub-key in response containing the array
|
|
652
|
+
# @param list_query [Hash, nil] Default query parameters
|
|
653
|
+
# @param query_component [String, nil] Registry key for --query=help schema
|
|
654
|
+
def entity_list(api:, entity:, display_fields: nil, items_key: nil, list_query: nil, query_component: nil, **)
|
|
655
|
+
qs_path = query_component ? Schema::Registry.query_params(query_component, entity) : nil
|
|
656
|
+
data, http = api.read(entity, query_read_delete(default: list_query, schema: qs_path), ret: :both)
|
|
657
|
+
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')
|
|
661
|
+
data = data[entity]
|
|
662
|
+
end
|
|
663
|
+
data = data[items_key] if items_key
|
|
664
|
+
case data
|
|
665
|
+
when Hash then Result::SingleObject.new(data, fields: display_fields)
|
|
666
|
+
when Array
|
|
667
|
+
return Result::ObjectList.new(data, fields: display_fields) if data.empty? || data.first.is_a?(Hash)
|
|
668
|
+
Result::ValueList.new(data)
|
|
669
|
+
else Aspera.error_unexpected_value(data.class.name) { 'list type' }
|
|
670
|
+
end
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Show one instance of an entity.
|
|
674
|
+
# @param api [Aspera::Rest] REST API object
|
|
675
|
+
# @param entity [String] API sub-path
|
|
676
|
+
# @param id [String, nil] Resource identifier; nil when is_singleton: true
|
|
677
|
+
# @param display_fields [Array, nil] Fields to display
|
|
678
|
+
# @param is_singleton [Boolean] When true, entity is the full path (no id appended)
|
|
679
|
+
# @param id_as_arg [Boolean, String] When set, id is appended as ?<id_as_arg>=<id>
|
|
680
|
+
def entity_show(api:, entity:, id: nil, display_fields: nil, is_singleton: false, id_as_arg: false, **)
|
|
681
|
+
path = entity_res_path(entity, id, is_singleton: is_singleton, id_as_arg: id_as_arg)
|
|
682
|
+
Result::SingleObject.new(api.read(path), fields: display_fields)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# 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|
|
|
701
|
+
api.create(entity, params)
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# Modify an existing instance of an entity.
|
|
706
|
+
# @param api [Aspera::Rest] REST API object
|
|
707
|
+
# @param entity [String] API sub-path
|
|
708
|
+
# @param id [String, nil] Resource identifier; nil when is_singleton: true
|
|
709
|
+
# @param is_singleton [Boolean] When true, entity is the full path (no id appended)
|
|
710
|
+
# @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
|
|
715
|
+
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)
|
|
718
|
+
Result::Status.new('modified')
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
# Delete one or more instances of an entity (supports bulk).
|
|
722
|
+
# @param api [Aspera::Rest] REST API object
|
|
723
|
+
# @param entity [String] API sub-path
|
|
724
|
+
# @param id [String, Array, nil] Resource identifier(s)
|
|
725
|
+
# @param id_as_arg [Boolean, String] When set, id is appended as ?<id_as_arg>=<id>
|
|
726
|
+
# @param delete_style [String, nil] When set, deletes by sending id array in payload
|
|
727
|
+
# @param query_component [String, nil] Registry key for --query=help schema
|
|
728
|
+
def entity_delete(api:, entity:, id: nil, id_as_arg: false, delete_style: nil, query_component: nil, **)
|
|
729
|
+
qs_path = query_component ? Schema::Registry.query_params(query_component, entity) : nil
|
|
730
|
+
if !delete_style.nil?
|
|
731
|
+
ids = id.is_a?(Array) ? id : [id]
|
|
732
|
+
Aspera.assert_type(ids, Array, type: Cli::BadArgument)
|
|
733
|
+
api.delete(entity, nil, content_type: Mime::JSON, body: {delete_style => ids})
|
|
734
|
+
return Result::Status.new('deleted')
|
|
735
|
+
end
|
|
736
|
+
bulk_result(id, command: :delete) do |one_id|
|
|
737
|
+
api.delete(
|
|
738
|
+
id_as_arg ? "#{entity}?#{id_as_arg}=#{one_id}" : "#{entity}/#{one_id}",
|
|
739
|
+
query_read_delete(schema: qs_path)
|
|
740
|
+
)
|
|
741
|
+
{'id' => one_id}
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Build the resource path for an instance operation.
|
|
746
|
+
# @param entity [String] API sub-path
|
|
747
|
+
# @param id [String, nil] Resource identifier
|
|
748
|
+
# @param is_singleton [Boolean] When true, entity IS the full path
|
|
749
|
+
# @param id_as_arg [Boolean, String] When set, id appended as ?<id_as_arg>=<id>
|
|
750
|
+
# @return [String]
|
|
751
|
+
def entity_res_path(entity, id, is_singleton: false, id_as_arg: false)
|
|
752
|
+
return entity if is_singleton
|
|
753
|
+
return "#{entity}?#{id_as_arg}=#{id}" if id_as_arg
|
|
754
|
+
"#{entity}/#{id}"
|
|
755
|
+
end
|
|
756
|
+
|
|
202
757
|
# Query parameters in URL suitable for REST: list/`GET` and delete/`DELETE`
|
|
203
758
|
# @param default [Hash, nil] Default query parameters
|
|
759
|
+
# @param schema [String, nil] Contextual schema path for --query help display
|
|
204
760
|
# @return [Hash, nil] Query parameters
|
|
205
|
-
def query_read_delete(default: nil)
|
|
761
|
+
def query_read_delete(default: nil, schema: nil)
|
|
206
762
|
# Dup default, as it could be frozen
|
|
207
|
-
query = options.get_option(:query) || default
|
|
208
|
-
Log.
|
|
763
|
+
query = options.get_option(:query, schema: schema) || default&.dup
|
|
764
|
+
Log.dump(:query_read_delete, query)
|
|
209
765
|
begin
|
|
210
766
|
# Check it is suitable
|
|
211
767
|
URI.encode_www_form(query) unless query.nil?
|
|
@@ -214,34 +770,6 @@ module Aspera
|
|
|
214
770
|
end
|
|
215
771
|
return query
|
|
216
772
|
end
|
|
217
|
-
|
|
218
|
-
# Retrieves an extended value from command line.
|
|
219
|
-
# Used for creation or modification of entities.
|
|
220
|
-
# @param command [Symbol] Command name for error message
|
|
221
|
-
# @param description [String, nil] Description of the value
|
|
222
|
-
# @param type [Class, nil] Expected type of value
|
|
223
|
-
# @param bulk [Boolean] If `true`, value must be an Array of `type`
|
|
224
|
-
# @param default [Object, nil] Default value if not provided
|
|
225
|
-
# @param schema [Hash, nil] JSON schema for validation
|
|
226
|
-
# @return [Hash, Array<Hash>] The value(s) to create object(s)
|
|
227
|
-
def value_create_modify(command:, description: nil, type: Hash, bulk: false, default: nil, schema: nil)
|
|
228
|
-
value = options.get_next_argument(
|
|
229
|
-
"parameters for #{command}#{" (#{description})" unless description.nil?}",
|
|
230
|
-
mandatory: default.nil?,
|
|
231
|
-
validation: bulk ? Array : type,
|
|
232
|
-
schema: schema
|
|
233
|
-
)
|
|
234
|
-
value = default if value.nil?
|
|
235
|
-
unless type.nil?
|
|
236
|
-
Aspera.assert_type(type, Class){'type'}
|
|
237
|
-
if bulk
|
|
238
|
-
Aspera.assert_array_all(value, type, type: Cli::BadArgument){'type'}
|
|
239
|
-
else
|
|
240
|
-
Aspera.assert_type(value, type, type: Cli::BadArgument){'type'}
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
return value
|
|
244
|
-
end
|
|
245
773
|
end
|
|
246
774
|
end
|
|
247
775
|
end
|