cuprum-cli 0.1.0
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 +7 -0
- data/CHANGELOG.md +34 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE +21 -0
- data/README.md +163 -0
- data/lib/cuprum/cli/argument.rb +172 -0
- data/lib/cuprum/cli/arguments/class_methods.rb +283 -0
- data/lib/cuprum/cli/arguments.rb +16 -0
- data/lib/cuprum/cli/coercion.rb +131 -0
- data/lib/cuprum/cli/command.rb +102 -0
- data/lib/cuprum/cli/commands/ci/report.rb +121 -0
- data/lib/cuprum/cli/commands/ci/rspec_command.rb +108 -0
- data/lib/cuprum/cli/commands/ci/rspec_each_command.rb +185 -0
- data/lib/cuprum/cli/commands/ci.rb +12 -0
- data/lib/cuprum/cli/commands/echo_command.rb +76 -0
- data/lib/cuprum/cli/commands/file/generate_file.rb +141 -0
- data/lib/cuprum/cli/commands/file/new_command.rb +86 -0
- data/lib/cuprum/cli/commands/file/render_erb.rb +88 -0
- data/lib/cuprum/cli/commands/file/resolve_template.rb +136 -0
- data/lib/cuprum/cli/commands/file/templates/rspec.rb.erb +14 -0
- data/lib/cuprum/cli/commands/file/templates/ruby.rb.erb +29 -0
- data/lib/cuprum/cli/commands/file/templates.rb +71 -0
- data/lib/cuprum/cli/commands/file.rb +14 -0
- data/lib/cuprum/cli/commands.rb +12 -0
- data/lib/cuprum/cli/dependencies/file_system/mock.rb +297 -0
- data/lib/cuprum/cli/dependencies/file_system.rb +247 -0
- data/lib/cuprum/cli/dependencies/standard_io/helpers.rb +138 -0
- data/lib/cuprum/cli/dependencies/standard_io/mock.rb +85 -0
- data/lib/cuprum/cli/dependencies/standard_io.rb +110 -0
- data/lib/cuprum/cli/dependencies/system_command/mock.rb +57 -0
- data/lib/cuprum/cli/dependencies/system_command.rb +147 -0
- data/lib/cuprum/cli/dependencies.rb +25 -0
- data/lib/cuprum/cli/errors/files/file_not_writeable.rb +42 -0
- data/lib/cuprum/cli/errors/files/missing_parameter.rb +71 -0
- data/lib/cuprum/cli/errors/files/missing_template.rb +36 -0
- data/lib/cuprum/cli/errors/files/template_error.rb +37 -0
- data/lib/cuprum/cli/errors/files/template_not_resolved.rb +54 -0
- data/lib/cuprum/cli/errors/files.rb +19 -0
- data/lib/cuprum/cli/errors/system_command_failure.rb +44 -0
- data/lib/cuprum/cli/errors.rb +11 -0
- data/lib/cuprum/cli/integrations/thor/arguments_parser.rb +99 -0
- data/lib/cuprum/cli/integrations/thor/registry.rb +42 -0
- data/lib/cuprum/cli/integrations/thor/task.rb +211 -0
- data/lib/cuprum/cli/integrations/thor.rb +14 -0
- data/lib/cuprum/cli/integrations.rb +8 -0
- data/lib/cuprum/cli/metadata.rb +215 -0
- data/lib/cuprum/cli/option.rb +165 -0
- data/lib/cuprum/cli/options/class_methods.rb +232 -0
- data/lib/cuprum/cli/options/quiet.rb +32 -0
- data/lib/cuprum/cli/options/verbose.rb +32 -0
- data/lib/cuprum/cli/options.rb +18 -0
- data/lib/cuprum/cli/registry.rb +141 -0
- data/lib/cuprum/cli/rspec/deferred/arguments_examples.rb +203 -0
- data/lib/cuprum/cli/rspec/deferred/ci/report_examples.rb +450 -0
- data/lib/cuprum/cli/rspec/deferred/ci.rb +8 -0
- data/lib/cuprum/cli/rspec/deferred/dependencies/file_system_examples.rb +1469 -0
- data/lib/cuprum/cli/rspec/deferred/dependencies.rb +8 -0
- data/lib/cuprum/cli/rspec/deferred/metadata_examples.rb +856 -0
- data/lib/cuprum/cli/rspec/deferred/options_examples.rb +234 -0
- data/lib/cuprum/cli/rspec/deferred/registry_examples.rb +451 -0
- data/lib/cuprum/cli/rspec/deferred.rb +8 -0
- data/lib/cuprum/cli/rspec.rb +8 -0
- data/lib/cuprum/cli/version.rb +59 -0
- data/lib/cuprum/cli.rb +47 -0
- metadata +173 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/error'
|
|
4
|
+
|
|
5
|
+
require 'cuprum/cli/errors'
|
|
6
|
+
|
|
7
|
+
module Cuprum::Cli::Errors
|
|
8
|
+
# Error returned when a system command returns a non-success status.
|
|
9
|
+
class SystemCommandFailure < Cuprum::Error
|
|
10
|
+
# Short string used to identify the type of error.
|
|
11
|
+
TYPE = 'cuprum.cli.errors.system_command_failure'
|
|
12
|
+
|
|
13
|
+
# @param command [String] the failed command.
|
|
14
|
+
# @param details [String] the error output from the process, if any.
|
|
15
|
+
# @param exit_status [Integer] the exit code returned by the process.
|
|
16
|
+
def initialize(command:, details: nil, exit_status: nil)
|
|
17
|
+
@command = command
|
|
18
|
+
@details = details
|
|
19
|
+
@exit_status = exit_status
|
|
20
|
+
|
|
21
|
+
super(message: default_message)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_reader :command
|
|
25
|
+
|
|
26
|
+
attr_reader :details
|
|
27
|
+
|
|
28
|
+
attr_reader :exit_status
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def as_json_data
|
|
33
|
+
{
|
|
34
|
+
'command' => command,
|
|
35
|
+
'details' => details,
|
|
36
|
+
'exit_status' => exit_status
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def default_message
|
|
41
|
+
%(system command failed with exit status #{exit_status} - "#{command}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/cli'
|
|
4
|
+
|
|
5
|
+
module Cuprum::Cli
|
|
6
|
+
# Namespace for errors, which represent failure states of commands.
|
|
7
|
+
module Errors
|
|
8
|
+
autoload :Files, 'cuprum/cli/errors/files'
|
|
9
|
+
autoload :SystemCommandFailure, 'cuprum/cli/errors/system_command_failure'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/cli/integrations/thor'
|
|
4
|
+
|
|
5
|
+
module Cuprum::Cli::Integrations::Thor
|
|
6
|
+
# Utility for parsing command-line arguments captured by Thor tasks.
|
|
7
|
+
#
|
|
8
|
+
# Any unrecognized command line flags or options are appended as-is to the
|
|
9
|
+
# arguments array by Thor. Therefore, to handle cases such as variadic options
|
|
10
|
+
# where flags or options cannot be pre-parsed by Thor, we need an additional
|
|
11
|
+
# parsing step to pull any remaining flags or options out of the arguments.
|
|
12
|
+
#
|
|
13
|
+
# This parser supports the following formats:
|
|
14
|
+
#
|
|
15
|
+
# - `-a`, `--all`: Sets the `a` or `all` flag to `true`.
|
|
16
|
+
# - `-abc`: Sets the `a`, `b`, and `c` flags to `true`.
|
|
17
|
+
# - `--skip-all`, `--no-all`: Sets the `a` flag to `false`.
|
|
18
|
+
# - `-a=value`, `--a=value`: Sets the `a` option to `"value"`.
|
|
19
|
+
#
|
|
20
|
+
# The following formats are specifically *not* supported:
|
|
21
|
+
#
|
|
22
|
+
# - `--foo bar`: `--foo` is assumed to be a flag, `bar` is assumed to be a
|
|
23
|
+
# positional argument.
|
|
24
|
+
# - `--str[]=foo --str[]=bar`: Array arguments are not supported.
|
|
25
|
+
# - `--str[foo]=foo --str[bar]=bar`: Hash arguments are not supported.
|
|
26
|
+
#
|
|
27
|
+
# In addition, parsed option values are coerced into their most likely
|
|
28
|
+
# intended types.
|
|
29
|
+
class ArgumentsParser
|
|
30
|
+
# Parses the given argument inputs into arguments and options.
|
|
31
|
+
#
|
|
32
|
+
# @param inputs [Array<String>] the arguments captured by Thor.
|
|
33
|
+
#
|
|
34
|
+
# @return [Array<Array<String>, Hash{Symbol=>Object}>] the parsed arguments
|
|
35
|
+
# and options.
|
|
36
|
+
def call(*inputs)
|
|
37
|
+
raw_options, arguments = inputs.partition { |str| str.start_with?('-') }
|
|
38
|
+
|
|
39
|
+
[arguments, parse_options(raw_options)]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def coerce_value(raw_value)
|
|
45
|
+
Cuprum::Cli::Coercion.coerce(raw_value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def grouped_flags?(raw_key)
|
|
49
|
+
return false if raw_key.start_with?('--')
|
|
50
|
+
|
|
51
|
+
raw_key.length > 2
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def normalize_flag_key(raw_key)
|
|
55
|
+
return raw_key[5..] if raw_key.start_with?('--no-')
|
|
56
|
+
return raw_key[7..] if raw_key.start_with?('--skip-')
|
|
57
|
+
return raw_key[2..] if raw_key.start_with?('--')
|
|
58
|
+
|
|
59
|
+
raw_key[1..]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def normalize_option_key(raw_key)
|
|
63
|
+
return raw_key[2..] if raw_key.start_with?('--')
|
|
64
|
+
|
|
65
|
+
raw_key[1..]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_flag_value(raw_key) # rubocop:disable Naming/PredicateMethod
|
|
69
|
+
return false if raw_key.start_with?('--no-')
|
|
70
|
+
return false if raw_key.start_with?('--skip-')
|
|
71
|
+
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_option(raw_key, raw_value) # rubocop:disable Metrics/MethodLength
|
|
76
|
+
if raw_value.nil? && grouped_flags?(raw_key)
|
|
77
|
+
raw_key[1..].chars.to_h { |char| [char.to_sym, true] }
|
|
78
|
+
elsif raw_value.nil?
|
|
79
|
+
key = normalize_flag_key(raw_key)
|
|
80
|
+
value = parse_flag_value(raw_key)
|
|
81
|
+
|
|
82
|
+
{ key.to_sym => value }
|
|
83
|
+
else
|
|
84
|
+
key = normalize_option_key(raw_key)
|
|
85
|
+
value = coerce_value(raw_value)
|
|
86
|
+
|
|
87
|
+
{ key.to_sym => value }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parse_options(raw_options)
|
|
92
|
+
raw_options.reduce({}) do |options, input|
|
|
93
|
+
raw_key, raw_value = input.split('=')
|
|
94
|
+
|
|
95
|
+
options.merge(parse_option(raw_key, raw_value))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/cli/integrations/thor'
|
|
4
|
+
require 'cuprum/cli/integrations/thor/task'
|
|
5
|
+
require 'cuprum/cli/registry'
|
|
6
|
+
|
|
7
|
+
module Cuprum::Cli::Integrations::Thor
|
|
8
|
+
# Registers CLI commands by name and adds as Thor tasks.
|
|
9
|
+
class Registry < Cuprum::Cli::Registry
|
|
10
|
+
# Registers the command with the registry.
|
|
11
|
+
#
|
|
12
|
+
# Also registers a Thor task with compatible parameters and metadata.
|
|
13
|
+
#
|
|
14
|
+
# @param command [Class] the command class to register.
|
|
15
|
+
# @param config [Hash] options for configuring the command.
|
|
16
|
+
#
|
|
17
|
+
# @option config arguments [Array] arguments to pass to the command on
|
|
18
|
+
# initialization.
|
|
19
|
+
# @option config description [String] the description for the command.
|
|
20
|
+
# @option config full_description [String] the full description for the
|
|
21
|
+
# command.
|
|
22
|
+
# @option config full_name [String] the name under which to register the
|
|
23
|
+
# command. Defaults to the value of command.full_name.
|
|
24
|
+
# @option config options [Hash] options to pass to the command on
|
|
25
|
+
# initialization.
|
|
26
|
+
#
|
|
27
|
+
# @raise [NameError] if a command is already registered with that name.
|
|
28
|
+
#
|
|
29
|
+
# @return [self]
|
|
30
|
+
def register(command, **config)
|
|
31
|
+
super.tap do
|
|
32
|
+
name = config.fetch(:full_name, command.full_name)
|
|
33
|
+
command = commands[name]
|
|
34
|
+
|
|
35
|
+
Cuprum::Cli::Integrations::Thor::Task::Builder
|
|
36
|
+
.new(command)
|
|
37
|
+
.build(full_name: name)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
alias add register
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
|
|
5
|
+
require 'sleeping_king_studios/tools/toolbelt'
|
|
6
|
+
require 'sleeping_king_studios/tools/toolbox/subclass'
|
|
7
|
+
require 'thor'
|
|
8
|
+
|
|
9
|
+
require 'cuprum/cli/integrations/thor'
|
|
10
|
+
require 'cuprum/cli/integrations/thor/arguments_parser'
|
|
11
|
+
|
|
12
|
+
module Cuprum::Cli::Integrations::Thor
|
|
13
|
+
# Thor task wrapping a Cuprum::Cli command.
|
|
14
|
+
class Task < ::Thor
|
|
15
|
+
extend SleepingKingStudios::Tools::Toolbox::Subclass
|
|
16
|
+
|
|
17
|
+
# Generates a Thor::Task wrapping a Cuprum::Cli command class.
|
|
18
|
+
class Builder
|
|
19
|
+
extend Forwardable
|
|
20
|
+
|
|
21
|
+
NUMERIC_TYPES = Set.new(%w[big_decimal integer float]).freeze
|
|
22
|
+
private_constant :NUMERIC_TYPES
|
|
23
|
+
|
|
24
|
+
# @param command_class [Class] the command to execute.
|
|
25
|
+
def initialize(command_class)
|
|
26
|
+
validate_command_class(command_class)
|
|
27
|
+
|
|
28
|
+
@command_class = command_class
|
|
29
|
+
@full_name = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Class] the command to execute.
|
|
33
|
+
attr_reader :command_class
|
|
34
|
+
|
|
35
|
+
def_delegators :@command_class,
|
|
36
|
+
:arguments,
|
|
37
|
+
:description,
|
|
38
|
+
:full_description,
|
|
39
|
+
:full_description?
|
|
40
|
+
|
|
41
|
+
# Generates a Thor::Task wrapping the command class.
|
|
42
|
+
#
|
|
43
|
+
# The generated task will be assigned Thor metadata automatically, based
|
|
44
|
+
# on the configuration of the command class.
|
|
45
|
+
#
|
|
46
|
+
# @return [Class] the generated Task class.
|
|
47
|
+
def build(full_name: nil)
|
|
48
|
+
@full_name = full_name || command_class.full_name
|
|
49
|
+
|
|
50
|
+
tools.assertions.validate_name(@full_name, as: 'full_name')
|
|
51
|
+
|
|
52
|
+
Cuprum::Cli::Integrations::Thor::Task
|
|
53
|
+
.subclass(command_class)
|
|
54
|
+
.tap do |task|
|
|
55
|
+
apply_metadata(task)
|
|
56
|
+
|
|
57
|
+
task.alias_method short_name, :call_command
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
attr_reader :full_name
|
|
64
|
+
|
|
65
|
+
def apply_options(task)
|
|
66
|
+
command_class.options.each_value do |option|
|
|
67
|
+
params = {
|
|
68
|
+
aliases: option.aliases,
|
|
69
|
+
banner: option.parameter_name || option.name.to_s.upcase,
|
|
70
|
+
desc: option.description,
|
|
71
|
+
required: option.required?,
|
|
72
|
+
type: parameter_type(option)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
task.option(option.name, **params)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def apply_metadata(task)
|
|
80
|
+
task.namespace(namespace)
|
|
81
|
+
task.desc(signature, description)
|
|
82
|
+
|
|
83
|
+
task.long_desc(full_description) if full_description?
|
|
84
|
+
|
|
85
|
+
apply_options(task)
|
|
86
|
+
|
|
87
|
+
task
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def argument_signature(argument)
|
|
91
|
+
signature = argument.parameter_name || argument.name.to_s.upcase
|
|
92
|
+
|
|
93
|
+
argument.variadic ? " ...#{signature}" : " #{signature}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def arguments_signature
|
|
97
|
+
arguments.map { |argument| argument_signature(argument) }.join
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def namespace
|
|
101
|
+
return 'default' if full_name.nil? || !full_name.include?(':')
|
|
102
|
+
|
|
103
|
+
full_name&.sub(/:[\w_]+\z/, '')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def parameter_type(parameter)
|
|
107
|
+
type = parameter.type
|
|
108
|
+
type = type.name if type.is_a?(Class)
|
|
109
|
+
type = tools.string_tools.underscore(type)
|
|
110
|
+
|
|
111
|
+
return :numeric if NUMERIC_TYPES.include?(type)
|
|
112
|
+
|
|
113
|
+
type.to_sym
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def short_name
|
|
117
|
+
full_name&.split(':')&.last
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def signature
|
|
121
|
+
"#{short_name}#{arguments_signature}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def tools
|
|
125
|
+
SleepingKingStudios::Tools::Toolbelt.instance
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def validate_command_class(command_class, as: 'command_class')
|
|
129
|
+
tools.assertions.validate_class(command_class, as:)
|
|
130
|
+
tools.assertions.validate_inherits_from(
|
|
131
|
+
command_class,
|
|
132
|
+
as:,
|
|
133
|
+
expected: Cuprum::Cli::Command
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
validate_command_description(command_class, as:)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate_command_description(command_class, as:)
|
|
140
|
+
description = command_class.description
|
|
141
|
+
|
|
142
|
+
return unless description.nil? || description.empty?
|
|
143
|
+
|
|
144
|
+
raise ArgumentError, "#{as} does not have a description"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Ensures that the task exists with a non-zero status code on a failure.
|
|
149
|
+
#
|
|
150
|
+
# @return [true]
|
|
151
|
+
def self.exit_on_failure? = true
|
|
152
|
+
|
|
153
|
+
# @overload initialize(command_class, arguments = [], options = {}, config = {})
|
|
154
|
+
# @param command_class [Class] the command to execute.
|
|
155
|
+
# @param arguments [Array] the arguments passed by the Thor runtime.
|
|
156
|
+
# @param options [Hash] the options passed by the Thor runtime.
|
|
157
|
+
# @param config [Hash] additional configuration passed by the Thor
|
|
158
|
+
# runtime.
|
|
159
|
+
def initialize(
|
|
160
|
+
command_class,
|
|
161
|
+
arguments = [],
|
|
162
|
+
options = {},
|
|
163
|
+
config = {},
|
|
164
|
+
command_dependencies: {}
|
|
165
|
+
)
|
|
166
|
+
super(arguments, options, config)
|
|
167
|
+
|
|
168
|
+
@command_class = command_class
|
|
169
|
+
@command_dependencies = command_dependencies
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# @return [Class] the command to execute.
|
|
173
|
+
attr_reader :command_class
|
|
174
|
+
|
|
175
|
+
no_commands do
|
|
176
|
+
# Calls the wrapped Cuprum::Cli command with the parsed parameters.
|
|
177
|
+
def call_command(*args)
|
|
178
|
+
args, opts =
|
|
179
|
+
Cuprum::Cli::Integrations::Thor::ArgumentsParser.new.call(*args)
|
|
180
|
+
opts = opts.merge(options)
|
|
181
|
+
opts = tools.hash_tools.convert_keys_to_symbols(opts)
|
|
182
|
+
result =
|
|
183
|
+
command_class.new(**command_dependencies).call(*args, **opts)
|
|
184
|
+
|
|
185
|
+
handle_failure(result) if result.failure?
|
|
186
|
+
rescue Cuprum::Cli::Options::UnknownOptionError,
|
|
187
|
+
Cuprum::Cli::Arguments::ExtraArgumentsError => exception
|
|
188
|
+
abort(exception.message)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
attr_reader :command_dependencies
|
|
195
|
+
|
|
196
|
+
def abort(*) = Kernel.abort(*)
|
|
197
|
+
|
|
198
|
+
def handle_failure(result)
|
|
199
|
+
message =
|
|
200
|
+
result
|
|
201
|
+
.error
|
|
202
|
+
&.then { |err| "#{err.class.name}: #{err.message}" } || false
|
|
203
|
+
|
|
204
|
+
abort(message)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def tools
|
|
208
|
+
SleepingKingStudios::Tools::Toolbelt.instance
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/cli/integrations'
|
|
4
|
+
|
|
5
|
+
module Cuprum::Cli::Integrations
|
|
6
|
+
# Integration with the Thor CLI toolkit.
|
|
7
|
+
#
|
|
8
|
+
# @see http://whatisthor.com/
|
|
9
|
+
module Thor
|
|
10
|
+
autoload :ArgumentsParser, 'cuprum/cli/integrations/thor/arguments_parser'
|
|
11
|
+
autoload :Registry, 'cuprum/cli/integrations/thor/registry'
|
|
12
|
+
autoload :Task, 'cuprum/cli/integrations/thor/task'
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sleeping_king_studios/tools/toolbox/mixin'
|
|
4
|
+
|
|
5
|
+
require 'cuprum/cli'
|
|
6
|
+
|
|
7
|
+
module Cuprum::Cli
|
|
8
|
+
# Class methods for describing commands.
|
|
9
|
+
module Metadata
|
|
10
|
+
extend SleepingKingStudios::Tools::Toolbox::Mixin
|
|
11
|
+
|
|
12
|
+
# Format used to validate command names.
|
|
13
|
+
FULL_NAME_FORMAT = /\A[a-z_]+(:[a-z_]+)*\z/
|
|
14
|
+
|
|
15
|
+
UNDEFINED = SleepingKingStudios::Tools::UNDEFINED
|
|
16
|
+
private_constant :UNDEFINED
|
|
17
|
+
|
|
18
|
+
# Raised when performing a protected operation on an abstract Command.
|
|
19
|
+
class AbstractCommandError < StandardError; end
|
|
20
|
+
|
|
21
|
+
# Class methods to extend when including Metadata.
|
|
22
|
+
module ClassMethods
|
|
23
|
+
# Marks the command as abstract.
|
|
24
|
+
def abstract = @abstract = true
|
|
25
|
+
|
|
26
|
+
# @return [true, false] true if the command is abstract and should not be
|
|
27
|
+
# instantiated directly or assigned metadata; otherwise false.
|
|
28
|
+
def abstract? = @abstract.nil? ? false : @abstract
|
|
29
|
+
|
|
30
|
+
# @overload description
|
|
31
|
+
# @return [String] the description for the command.
|
|
32
|
+
#
|
|
33
|
+
# @overload description(value)
|
|
34
|
+
# Sets the description for the command.
|
|
35
|
+
#
|
|
36
|
+
# @param value [String] the description to set.
|
|
37
|
+
#
|
|
38
|
+
# @return [String] the set description.
|
|
39
|
+
def description(value = UNDEFINED)
|
|
40
|
+
return defined_description if value == UNDEFINED
|
|
41
|
+
|
|
42
|
+
if abstract?
|
|
43
|
+
raise AbstractCommandError,
|
|
44
|
+
abstract_command_message('set description')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
tools.assertions.validate_name(value, as: 'description')
|
|
48
|
+
|
|
49
|
+
@description = value
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [true, false] true if the command defines a description;
|
|
53
|
+
# otherwise false.
|
|
54
|
+
def description? = !defined_description.nil?
|
|
55
|
+
|
|
56
|
+
# @overload full_description
|
|
57
|
+
# @return [String] the full description for the command.
|
|
58
|
+
#
|
|
59
|
+
# @overload full_description(value)
|
|
60
|
+
# Sets the full description for the command.
|
|
61
|
+
#
|
|
62
|
+
# @param value [String] the full description to set.
|
|
63
|
+
#
|
|
64
|
+
# @return [String] the set full description.
|
|
65
|
+
def full_description(value = UNDEFINED)
|
|
66
|
+
if value == UNDEFINED
|
|
67
|
+
return defined_full_description || defined_description
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if abstract?
|
|
71
|
+
raise AbstractCommandError,
|
|
72
|
+
abstract_command_message('set full_description')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
tools.assertions.validate_name(value, as: 'full_description')
|
|
76
|
+
|
|
77
|
+
@full_description = value
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [true, false] true if the command defines a full description;
|
|
81
|
+
# otherwise false.
|
|
82
|
+
def full_description? = !defined_full_description.nil?
|
|
83
|
+
|
|
84
|
+
# @overload full_name
|
|
85
|
+
# Returns the name of the command, used when calling from a CLI.
|
|
86
|
+
#
|
|
87
|
+
# Unless another value is set, defaults to the class name of the command
|
|
88
|
+
# with the following format:
|
|
89
|
+
#
|
|
90
|
+
# - Removes the "Commands" namespace and any prior namespace, if any.
|
|
91
|
+
# - Removes a "Command" suffix, if any.
|
|
92
|
+
# - Converts each remaining segment to snake_case and joins with ":".
|
|
93
|
+
#
|
|
94
|
+
# @return [String] the scoped name for the command.
|
|
95
|
+
#
|
|
96
|
+
# @overload full_name(value)
|
|
97
|
+
# Sets the full name for the command.
|
|
98
|
+
#
|
|
99
|
+
# The full name must be in snake_case format joined by ":".
|
|
100
|
+
#
|
|
101
|
+
# @param value [String] the full name to set.
|
|
102
|
+
#
|
|
103
|
+
# @return [String] the set full name.
|
|
104
|
+
def full_name(value = UNDEFINED) # rubocop:disable Metrics/MethodLength
|
|
105
|
+
return defined_full_name if value == UNDEFINED
|
|
106
|
+
|
|
107
|
+
if abstract?
|
|
108
|
+
raise AbstractCommandError, abstract_command_message('set full_name')
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
tools.assertions.validate_name(value, as: 'full_name')
|
|
112
|
+
tools.assertions.validate_matches(
|
|
113
|
+
value,
|
|
114
|
+
as: 'full_name',
|
|
115
|
+
expected: FULL_NAME_FORMAT,
|
|
116
|
+
message: invalid_full_name_format_message
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@full_name = value
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The namespace for the command.
|
|
123
|
+
#
|
|
124
|
+
# A command's namespace is defined as the part of the full name prior to
|
|
125
|
+
# the last segment.
|
|
126
|
+
#
|
|
127
|
+
# - For a command with full name "custom", the namespace will be nil.
|
|
128
|
+
# - For a command with full name "category:sub_category:do_something", the
|
|
129
|
+
# namespace will be "category:sub_category".
|
|
130
|
+
#
|
|
131
|
+
# @return [String, nil] the namespace for the command, or nil if the
|
|
132
|
+
# command's full name is unscoped.
|
|
133
|
+
#
|
|
134
|
+
# @see #short_name.
|
|
135
|
+
def namespace
|
|
136
|
+
return if full_name.nil? || !full_name.include?(':')
|
|
137
|
+
|
|
138
|
+
full_name&.sub(/:[\w_]+\z/, '')
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [true, false] true if the command defines a namespace; otherwise
|
|
142
|
+
# false.
|
|
143
|
+
def namespace? = !namespace.nil?
|
|
144
|
+
|
|
145
|
+
# The short name for the command.
|
|
146
|
+
#
|
|
147
|
+
# A command's short_name is the last segment of the full name.
|
|
148
|
+
#
|
|
149
|
+
# - For a command with full name "custom", the short_name will be
|
|
150
|
+
# "custom".
|
|
151
|
+
# - For a command with full name "category:sub_category:do_something", the
|
|
152
|
+
# short_name will be "do_something".
|
|
153
|
+
#
|
|
154
|
+
# @return [String] the short name for the command.
|
|
155
|
+
def short_name = full_name&.split(':')&.last
|
|
156
|
+
|
|
157
|
+
protected
|
|
158
|
+
|
|
159
|
+
def abstract_command_message(short_message)
|
|
160
|
+
class_name =
|
|
161
|
+
ancestors
|
|
162
|
+
.find { |ancestor| ancestor.is_a?(Class) && ancestor.name }
|
|
163
|
+
.name
|
|
164
|
+
|
|
165
|
+
"unable to #{short_message} - #{class_name} is an abstract class"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def defined_description
|
|
169
|
+
return @description if @description
|
|
170
|
+
|
|
171
|
+
return unless superclass.respond_to?(:defined_description, true)
|
|
172
|
+
|
|
173
|
+
superclass.defined_description
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def defined_full_description
|
|
177
|
+
return @full_description if @full_description
|
|
178
|
+
|
|
179
|
+
return unless superclass.respond_to?(:defined_full_description, true)
|
|
180
|
+
|
|
181
|
+
superclass.defined_full_description
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def defined_full_name
|
|
185
|
+
return @full_name if @full_name ||= default_name
|
|
186
|
+
|
|
187
|
+
return unless superclass.respond_to?(:defined_full_name, true)
|
|
188
|
+
|
|
189
|
+
return if superclass.name == 'Cuprum::Cli::Command'
|
|
190
|
+
|
|
191
|
+
superclass.defined_full_name
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
def default_name
|
|
197
|
+
return if name.nil?
|
|
198
|
+
|
|
199
|
+
name
|
|
200
|
+
.split('Commands::')
|
|
201
|
+
.last
|
|
202
|
+
.sub(/Command\z/, '')
|
|
203
|
+
.split('::')
|
|
204
|
+
.map { |str| tools.string_tools.underscore(str) }
|
|
205
|
+
.join(':')
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def invalid_full_name_format_message
|
|
209
|
+
'full_name does not match format category:sub_category:do_something'
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def tools = SleepingKingStudios::Tools::Toolbelt.instance
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|