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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +34 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE +21 -0
  5. data/README.md +163 -0
  6. data/lib/cuprum/cli/argument.rb +172 -0
  7. data/lib/cuprum/cli/arguments/class_methods.rb +283 -0
  8. data/lib/cuprum/cli/arguments.rb +16 -0
  9. data/lib/cuprum/cli/coercion.rb +131 -0
  10. data/lib/cuprum/cli/command.rb +102 -0
  11. data/lib/cuprum/cli/commands/ci/report.rb +121 -0
  12. data/lib/cuprum/cli/commands/ci/rspec_command.rb +108 -0
  13. data/lib/cuprum/cli/commands/ci/rspec_each_command.rb +185 -0
  14. data/lib/cuprum/cli/commands/ci.rb +12 -0
  15. data/lib/cuprum/cli/commands/echo_command.rb +76 -0
  16. data/lib/cuprum/cli/commands/file/generate_file.rb +141 -0
  17. data/lib/cuprum/cli/commands/file/new_command.rb +86 -0
  18. data/lib/cuprum/cli/commands/file/render_erb.rb +88 -0
  19. data/lib/cuprum/cli/commands/file/resolve_template.rb +136 -0
  20. data/lib/cuprum/cli/commands/file/templates/rspec.rb.erb +14 -0
  21. data/lib/cuprum/cli/commands/file/templates/ruby.rb.erb +29 -0
  22. data/lib/cuprum/cli/commands/file/templates.rb +71 -0
  23. data/lib/cuprum/cli/commands/file.rb +14 -0
  24. data/lib/cuprum/cli/commands.rb +12 -0
  25. data/lib/cuprum/cli/dependencies/file_system/mock.rb +297 -0
  26. data/lib/cuprum/cli/dependencies/file_system.rb +247 -0
  27. data/lib/cuprum/cli/dependencies/standard_io/helpers.rb +138 -0
  28. data/lib/cuprum/cli/dependencies/standard_io/mock.rb +85 -0
  29. data/lib/cuprum/cli/dependencies/standard_io.rb +110 -0
  30. data/lib/cuprum/cli/dependencies/system_command/mock.rb +57 -0
  31. data/lib/cuprum/cli/dependencies/system_command.rb +147 -0
  32. data/lib/cuprum/cli/dependencies.rb +25 -0
  33. data/lib/cuprum/cli/errors/files/file_not_writeable.rb +42 -0
  34. data/lib/cuprum/cli/errors/files/missing_parameter.rb +71 -0
  35. data/lib/cuprum/cli/errors/files/missing_template.rb +36 -0
  36. data/lib/cuprum/cli/errors/files/template_error.rb +37 -0
  37. data/lib/cuprum/cli/errors/files/template_not_resolved.rb +54 -0
  38. data/lib/cuprum/cli/errors/files.rb +19 -0
  39. data/lib/cuprum/cli/errors/system_command_failure.rb +44 -0
  40. data/lib/cuprum/cli/errors.rb +11 -0
  41. data/lib/cuprum/cli/integrations/thor/arguments_parser.rb +99 -0
  42. data/lib/cuprum/cli/integrations/thor/registry.rb +42 -0
  43. data/lib/cuprum/cli/integrations/thor/task.rb +211 -0
  44. data/lib/cuprum/cli/integrations/thor.rb +14 -0
  45. data/lib/cuprum/cli/integrations.rb +8 -0
  46. data/lib/cuprum/cli/metadata.rb +215 -0
  47. data/lib/cuprum/cli/option.rb +165 -0
  48. data/lib/cuprum/cli/options/class_methods.rb +232 -0
  49. data/lib/cuprum/cli/options/quiet.rb +32 -0
  50. data/lib/cuprum/cli/options/verbose.rb +32 -0
  51. data/lib/cuprum/cli/options.rb +18 -0
  52. data/lib/cuprum/cli/registry.rb +141 -0
  53. data/lib/cuprum/cli/rspec/deferred/arguments_examples.rb +203 -0
  54. data/lib/cuprum/cli/rspec/deferred/ci/report_examples.rb +450 -0
  55. data/lib/cuprum/cli/rspec/deferred/ci.rb +8 -0
  56. data/lib/cuprum/cli/rspec/deferred/dependencies/file_system_examples.rb +1469 -0
  57. data/lib/cuprum/cli/rspec/deferred/dependencies.rb +8 -0
  58. data/lib/cuprum/cli/rspec/deferred/metadata_examples.rb +856 -0
  59. data/lib/cuprum/cli/rspec/deferred/options_examples.rb +234 -0
  60. data/lib/cuprum/cli/rspec/deferred/registry_examples.rb +451 -0
  61. data/lib/cuprum/cli/rspec/deferred.rb +8 -0
  62. data/lib/cuprum/cli/rspec.rb +8 -0
  63. data/lib/cuprum/cli/version.rb +59 -0
  64. data/lib/cuprum/cli.rb +47 -0
  65. metadata +173 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f0569f2b0f6fd69ffae0141d299421c0a42d91182e8d795151ab0d8994838cf5
4
+ data.tar.gz: d585735c4712bd25a0729c09fb8547e46b0e4a2222d3cf1476c178ea17d54bc1
5
+ SHA512:
6
+ metadata.gz: b65e0e51ef3d04c4adb58998dc62e3a153e9e8cb2c44f21fd540798993bf13633e7079c07df56a03e30d0d0d5300f31b3eeacf3b2575ae4c88f513ecd60f5e1f
7
+ data.tar.gz: d0b99e59cc83645160bad0fec52432cee1d10b1f7efaf4f5771dff0c7a1df747dd2e9364ac27994ba73b17aaa6c74070711ff048e4185512a085a2d85346d6ee
data/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial version.
6
+
7
+ ## Commands
8
+
9
+ Implemented `Cuprum::Cli::Command`
10
+
11
+ - Added support for positional arguments
12
+ - Added support for keyword options and flags
13
+
14
+ ### Built-In Commands
15
+
16
+ - Implemented `Cuprum::Cli::Ci::RSpecCommand`
17
+ - Implemented `Cuprum::Cli::Ci::RSpecEachCommand`
18
+ - Implemented `Cuprum::Cli::File::NewCommand`
19
+
20
+ ### Dependencies
21
+
22
+ - Implemented `Cuprum::Cli::Dependencies::FileSystem`
23
+ - Implemented `Cuprum::Cli::Dependencies::StandardIo`
24
+ - Implemented `Cuprum::Cli::Dependencies::SystemCommand`
25
+
26
+ ### Registries
27
+
28
+ - Implemented `Cuprum::Cli::Registry`
29
+
30
+ ## Integrations
31
+
32
+ Added `Thor` integration
33
+
34
+ - Implemented `Cuprum::Cli::Integrations::Thor::Registry`
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "cuprum-cli" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["sleepingkingstudios@gmail.com"](mailto:"sleepingkingstudios@gmail.com").
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Rob Smith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # Cuprum::Cli
2
+
3
+ A command-line utility powered by [Cuprum](https://www.sleepingkingstudios.com/cuprum/) that provides tools and utilities for defining command-line tools.
4
+
5
+ <blockquote>
6
+ Read The
7
+ <a href="https://www.sleepingkingstudios.com/cuprum-cli" target="_blank">
8
+ Documentation
9
+ </a>
10
+ </blockquote>
11
+
12
+ ## About
13
+
14
+ ### Documentation
15
+
16
+ Documentation is generated using [YARD](https://yardoc.org/), and can be generated locally using the `yard` gem.
17
+
18
+ ### License
19
+
20
+ Copyright (c) 2026 Rob Smith
21
+
22
+ `Cuprum::Cli` is released under the [MIT License](https://opensource.org/licenses/MIT).
23
+
24
+ ### Contribute
25
+
26
+ The canonical repository for this gem is located at https://github.com/sleepingkingstudios/cuprum-cli.
27
+
28
+ To report a bug or submit a feature request, please use the [Issue Tracker](https://github.com/sleepingkingstudios/cuprum-cli/issues).
29
+
30
+ To contribute code, please fork the repository, make the desired updates, and then provide a [Pull Request](https://github.com/sleepingkingstudios/cuprum-cli/pulls). Pull requests must include appropriate tests for consideration, and all code must be properly formatted.
31
+
32
+ ### Code of Conduct
33
+
34
+ Please note that the `Cuprum::Cli` project is released with a [Contributor Code of Conduct](https://github.com/sleepingkingstudios/cuprum-cli/blob/master/CODE_OF_CONDUCT.md). By contributing to this project, you agree to abide by its terms.
35
+
36
+ ### Local Development
37
+
38
+ The test suite for `Cuprum::Cli` is written using [RSpec](https://rspec.info/), with optional integration dependencies managed using [Appraisal](https://github.com/thoughtbot/appraisal).
39
+
40
+ To run the general test suite:
41
+
42
+ ```bash
43
+ bundle exec rspec
44
+ ```
45
+
46
+ To run the test suite including the `Thor` integration:
47
+
48
+ ```bash
49
+ BUNDLER_GEMFILE=gemfiles/integrations_thor.gemfile INTEGRATION=thor bundle exec rspec
50
+ ```
51
+
52
+ To run the [RuboCop](https://rubocop.org/) linter:
53
+
54
+ ```bash
55
+ bundle exec rubocop
56
+ ```
57
+
58
+ ## Getting Started
59
+
60
+ Add the gem to your Gemfile or gemspec:
61
+
62
+ ```ruby
63
+ group :development, :test do
64
+ gem 'cuprum-cli'
65
+ end
66
+ ```
67
+
68
+ To ensure that dependent libraries are loaded, call the `Cuprum::Cli` initializer:
69
+
70
+ - In the initializer for your project:
71
+
72
+ ```ruby
73
+ module Space
74
+ @initializer = SleepingKingStudios::Tools::Toolbox::Initializer.new do
75
+ Cuprum::Cli.initializer.call
76
+ end
77
+ end
78
+ ```
79
+
80
+ - Or, in the entry points of your application (such as a `bin` script or `spec_helper.rb`).
81
+
82
+ Set up a <a href="https://www.sleepingkingstudios.com/cuprum-cli/integrations" target="_blank">CLI integration</a> and register your commands:
83
+
84
+ ```ruby
85
+ # In tasks.thor:
86
+ require 'cuprum/cli/integrations/thor/registry'
87
+
88
+ Cuprum::Cli.initializer.call
89
+
90
+ registry = Cuprum::Cli::Integrations::Thor::Registry.new
91
+
92
+ registry.register Cuprum::Cli::Commands::Ci::RSpecCommand
93
+ registry.register Cuprum::Cli::Commands::Ci::RSpecCommand,
94
+ full_name: 'ci:rspec:sinatra4',
95
+ description: 'Runs the RSpec tests against Sinatra 4.X',
96
+ options: { gemfile: 'gemfiles/sinatra_4.gemfile' }
97
+ ```
98
+
99
+ Finally, you can call the commands from your CLI tool:
100
+
101
+ ```
102
+ % bundle exec thor list
103
+ ci
104
+ --
105
+ thor ci:rspec ...FILE_PATTERNS # Runs an RSpec command.
106
+ thor ci:rspec:sinatra4 ...FILE_PATTERNS # Runs the RSpec tests against Sinatra 4.X
107
+ ```
108
+
109
+ ### Defining Commands
110
+
111
+ You can also define custom CLI commands using the `Cuprum::Cli::Command` class. `Cuprum::Cli` defines a powerful DSL for quickly defining and configuring commands.
112
+
113
+ ```ruby
114
+ class PingCommand < Cuprum::Cli::Command
115
+ dependency :system_command
116
+
117
+ argument :service_url,
118
+ default: 'www.example.com',
119
+ description: 'The URL of the remote service',
120
+ type: String
121
+
122
+ option :interval,
123
+ aliases: 'i',
124
+ default: 0.1,
125
+ description: 'The interval between pings',
126
+ type: Float
127
+
128
+ option :max_count,
129
+ aliases: %w[c],
130
+ default: 5,
131
+ description: 'The total number of pings sent to the server',
132
+ type: Integer
133
+
134
+ private
135
+
136
+ def format_options
137
+ # The ping command uses a non-standard options format.
138
+ options = +''
139
+
140
+ options << "-c#{max_count}"
141
+ options << "-i#{interval}"
142
+ options << '-q' # Only display the summary line.
143
+ end
144
+
145
+ def process
146
+ system_command.capture(
147
+ 'ping',
148
+ arguments: [format_options, service_url]
149
+ )
150
+ end
151
+ end
152
+ ```
153
+
154
+ Now that we've defined a custom command, we can register it in our CLI integration:
155
+
156
+ ```ruby
157
+ registry.register PingCommand
158
+ registry.register PingCommand,
159
+ full_name: 'ping:github',
160
+ options: { service_url: 'github.com' }
161
+ ```
162
+
163
+ For more information on defining commands, see the <a href="https://www.sleepingkingstudios.com/cuprum-cli/commands" target="_blank">commands documentation</a>.
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/cli'
4
+ require 'cuprum/cli/arguments'
5
+
6
+ module Cuprum::Cli # rubocop:disable Metrics/ModuleLength
7
+ # Data object representing a positional command argument.
8
+ Argument = Data.define(
9
+ :default,
10
+ :description,
11
+ :name,
12
+ :parameter_name,
13
+ :required,
14
+ :type,
15
+ :variadic
16
+ ) do
17
+ # @param name [String, Symbol] the name of the argument.
18
+ # @param default [Object, Proc] the default value for the argument. If given
19
+ # and the value of the argument is nil, sets the argument value to the
20
+ # default value.
21
+ # @param description [String] a short, human-readable description of the
22
+ # argument.
23
+ # @param parameter_name [String] a representation of the possible values for
24
+ # the argument.
25
+ # @param required [true, false] if true, raises an exception if the argument
26
+ # is not provided to the command.
27
+ # @param type [Class, String, Symbol] the expected type of the argument
28
+ # value as a Class or class name. If given, raises an exception if the
29
+ # argument value is not an instance of the type. Defaults to :string.
30
+ # @param variadic [true, false] if true, the argument is variadic and
31
+ # represents an array of arguments provided to the command. Defaults to
32
+ # false.
33
+ def initialize( # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
34
+ name:,
35
+ default: nil,
36
+ description: nil,
37
+ parameter_name: nil,
38
+ required: false,
39
+ type: :string,
40
+ variadic: false
41
+ )
42
+ name = name.to_sym
43
+ required = required ? true : false
44
+ type = type.to_sym if type.is_a?(String)
45
+ variadic = variadic ? true : false
46
+
47
+ super(
48
+ default:,
49
+ description:,
50
+ name:,
51
+ parameter_name:,
52
+ required:,
53
+ type:,
54
+ variadic:
55
+ )
56
+ end
57
+
58
+ alias_method :required?, :required
59
+
60
+ alias_method :variadic?, :variadic
61
+
62
+ # @overload def resolve(value)
63
+ # Validates the value for the current argument.
64
+ #
65
+ # If the value is nil, applies the argument default (if any).
66
+ #
67
+ # @param value [Object] the value to validate.
68
+ #
69
+ # @return [Object] the validated argument value.
70
+ #
71
+ # @raise [Cuprum::Cli::Arguments::InvalidArgumentError] if the value is
72
+ # missing (for a required argument) or invalid.
73
+ def resolve(original_value) # rubocop:disable Metrics/CyclomaticComplexity
74
+ return resolve_variadic(original_value) if variadic?
75
+
76
+ value = original_value
77
+ value = default_value if blank?(value)
78
+ value = value.to_s if value.is_a?(Symbol)
79
+
80
+ return (type == :boolean ? false : nil) if value.nil? && !required?
81
+
82
+ return value if valid_argument?(value)
83
+
84
+ raise Cuprum::Cli::Arguments::InvalidArgumentError,
85
+ invalid_argument_message(original_value)
86
+ end
87
+
88
+ private
89
+
90
+ def blank?(value)
91
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
92
+ end
93
+
94
+ def default_value
95
+ default.is_a?(Proc) ? default.call : default
96
+ end
97
+
98
+ def expected_array_type # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
99
+ message = required? ? 'a non-empty Array of' : 'an Array of'
100
+
101
+ case type
102
+ when :boolean
103
+ "#{message} true or false"
104
+ when Class
105
+ name = tools.string_tools.pluralize(type.name)
106
+
107
+ "#{message} #{tools.string_tools.camelize(name)}"
108
+ else
109
+ name = tools.string_tools.pluralize(type.to_s)
110
+
111
+ "#{message} #{tools.string_tools.camelize(name)}"
112
+ end
113
+ end
114
+
115
+ def expected_type
116
+ case type
117
+ when :boolean
118
+ 'true or false'
119
+ when Class
120
+ "an instance of #{type.name}"
121
+ else
122
+ "an instance of #{tools.string_tools.camelize(type.to_s)}"
123
+ end
124
+ end
125
+
126
+ def invalid_argument_message(value)
127
+ "invalid value for argument :#{name} - expected #{expected_type}, " \
128
+ "received #{value.inspect}"
129
+ end
130
+
131
+ def invalid_variadic_argument_message(value)
132
+ "invalid value for variadic argument :#{name} - expected " \
133
+ "#{expected_array_type}, received #{value.inspect}"
134
+ end
135
+
136
+ def resolve_variadic(original_value)
137
+ value = original_value
138
+ value = default_value if blank?(value)
139
+
140
+ return [] if value.nil? && !required?
141
+
142
+ return value if valid_arguments?(value)
143
+
144
+ raise Cuprum::Cli::Arguments::InvalidArgumentError,
145
+ invalid_variadic_argument_message(original_value)
146
+ end
147
+
148
+ def tools
149
+ SleepingKingStudios::Tools::Toolbelt.instance
150
+ end
151
+
152
+ def valid_argument?(value)
153
+ case type
154
+ when :boolean
155
+ value == true || value == false # rubocop:disable Style/MultipleComparison
156
+ when Class
157
+ value.is_a?(type)
158
+ else
159
+ expected = tools.string_tools.camelize(type.to_s)
160
+ expected = Object.const_get(expected)
161
+
162
+ value.is_a?(expected)
163
+ end
164
+ end
165
+
166
+ def valid_arguments?(value)
167
+ return false unless value.is_a?(Array)
168
+
169
+ value.all? { |item| valid_argument?(item) }
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'weakref'
4
+
5
+ require 'cuprum/cli/arguments'
6
+
7
+ module Cuprum::Cli::Arguments
8
+ # Methods used to extend command class functionality for defining arguments.
9
+ module ClassMethods
10
+ # Helper class for defining command arguments.
11
+ class Builder
12
+ # @param command_class [Class] the command class.
13
+ # @param defined_arguments [Array<Cuprum::Cli::Argument>] the arguments
14
+ # defined for the command.
15
+ def initialize(command_class:, defined_arguments:)
16
+ @command_class = command_class
17
+ @defined_arguments = defined_arguments
18
+ end
19
+
20
+ # @return [Class] the command class.
21
+ attr_reader :command_class
22
+
23
+ # @return [Array<Cuprum::Cli::Argument>] the arguments defined for the
24
+ # command.
25
+ attr_reader :defined_arguments
26
+
27
+ # (see Cuprum::Cli::Arguments::ClassMethods#argument)
28
+ def call(name, define_method: nil, define_predicate: nil, **options)
29
+ argument = Cuprum::Cli::Argument.new(name:, **options)
30
+
31
+ defined_arguments << argument
32
+
33
+ define_method = (options[:type] != :boolean) if define_method.nil?
34
+ define_predicate = (options[:type] == :boolean) if define_predicate.nil?
35
+
36
+ define_method_for(argument) if define_method
37
+ define_predicate_for(argument) if define_predicate
38
+
39
+ argument.name
40
+ end
41
+
42
+ private
43
+
44
+ def define_method_for(argument)
45
+ command_class.define_method(argument.name) { @arguments[argument.name] }
46
+ end
47
+
48
+ def define_predicate_for(argument)
49
+ command_class.define_method(:"#{argument.name}?") do
50
+ value = @arguments[argument.name]
51
+
52
+ return false if value.nil? || value == false
53
+
54
+ !(value.respond_to?(:empty?) && value.empty?)
55
+ end
56
+ end
57
+ end
58
+
59
+ # @private
60
+ class VariadicArgumentsResolver
61
+ def initialize(defined_arguments)
62
+ @defined_arguments = defined_arguments
63
+ @variadic_index = defined_arguments.index(&:variadic?)
64
+ @variadic_argument = defined_arguments[variadic_index]
65
+ @before_arguments = defined_arguments[...variadic_index]
66
+ @after_arguments = defined_arguments[(1 + variadic_index)..]
67
+ @resolved = {}
68
+ end
69
+
70
+ # @private
71
+ def call(*values)
72
+ values = resolve_before(values)
73
+ values = resolve_after(values)
74
+
75
+ resolved[variadic_argument.name] = values
76
+
77
+ resolved
78
+ end
79
+
80
+ private
81
+
82
+ attr_reader :after_arguments
83
+
84
+ attr_reader :before_arguments
85
+
86
+ attr_reader :resolved
87
+
88
+ attr_reader :variadic_argument
89
+
90
+ attr_reader :variadic_index
91
+
92
+ def resolve_after(values) # rubocop:disable Metrics/AbcSize
93
+ extra_count = values.count - after_arguments.count
94
+
95
+ if extra_count.negative?
96
+ values.concat(Array.new(-extra_count))
97
+
98
+ extra_count = 0
99
+ end
100
+
101
+ after_arguments&.each&.with_index(extra_count) do |argument, index|
102
+ resolved[argument.name] = argument.resolve(values[index])
103
+ end
104
+
105
+ values[...extra_count]
106
+ end
107
+
108
+ def resolve_before(values)
109
+ before_arguments&.each&.with_index do |argument, index|
110
+ resolved[argument.name] = argument.resolve(values[index])
111
+ end
112
+
113
+ values[before_arguments.count...] || []
114
+ end
115
+ end
116
+ private_constant :VariadicArgumentsResolver
117
+
118
+ # @overload argument(name, default: nil, description: nil, required: false, type: :string, variadic: false, **options)
119
+ # Defines an argument for the command class.
120
+ #
121
+ # @param name [String, Symbol] the name of the argument.
122
+ # @param default [Object, Proc] the default value for the argument. If
123
+ # given and the value of the argument is nil, sets the argument value to
124
+ # the default value.
125
+ # @param description [String] a short, human-readable description of the
126
+ # argument.
127
+ # @param required [true, false] if true, raises an exception if the
128
+ # argument is not provided to the command. Defaults to false.
129
+ # @param type [Class, String, Symbol] the expected type of the argument
130
+ # value as a Class or class name. If given, raises an exception if the
131
+ # argument value is not an instance of the type. Defaults to :string.
132
+ # @param variadic [true, false] if true, the argument is variadic and
133
+ # represents an array of arguments provided to the command. Defaults to
134
+ # false.
135
+ # @param options [Hash] additional options for defining the argument.
136
+ #
137
+ # @option options define_method [true, false] if true, defines a reader
138
+ # method for the argument. Defaults to false for boolean arguments and
139
+ # true for all other arguments.
140
+ # @option options define_predicate [true, false] if true, defines a
141
+ # predicate method for the argument, which returns true if the argument
142
+ # is not nil and not empty. Defaults to true for boolean arguments and
143
+ # false for all other arguments.
144
+ #
145
+ # @raise [ArgumentError] if variadic is true and the command already
146
+ # defines a variadic argument.
147
+ def argument(name, define_method: nil, define_predicate: nil, **)
148
+ handle_multiple_variadic_arguments(**)
149
+
150
+ arguments_builder.call(name, define_method:, define_predicate:, **)
151
+ end
152
+
153
+ # Appends a predefined argument value for the command.
154
+ #
155
+ # @param value [Object] the argument value to append.
156
+ #
157
+ # @return [void]
158
+ def argument_value(value)
159
+ defined_argument_values << value
160
+
161
+ nil
162
+ end
163
+
164
+ # @return [Array<Object>] predefined argument values for the command.
165
+ def argument_values = defined_argument_values
166
+
167
+ # @overload arguments()
168
+ # The defined arguments for the command class.
169
+ #
170
+ # @return [Array<Cuprum::Cli::Argument>] the defined arguments.
171
+ #
172
+ # @overload arguments(name, default: nil, description: nil, required: false, type: :string, **options)
173
+ # Defines a variadic argument for the command class.
174
+ #
175
+ # @param name [String, Symbol] the name of the argument.
176
+ # @param default [Object, Proc] the default value for the argument. If
177
+ # given and the value of the argument is nil, sets the argument value to
178
+ # the default value.
179
+ # @param description [String] a short, human-readable description of the
180
+ # argument.
181
+ # @param required [true, false] if true, raises an exception if the
182
+ # argument is not provided to the command.
183
+ # @param type [Class, String, Symbol] the expected type of the argument
184
+ # value as a Class or class name. If given, raises an exception if the
185
+ # argument value is not an instance of the type. Defaults to :string.
186
+ # @param options [Hash] additional options for defining the argument.
187
+ #
188
+ # @option options define_method [true, false] if true, defines a reader
189
+ # method for the argument. Defaults to false for boolean arguments and
190
+ # true for all other arguments.
191
+ # @option options define_predicate [true, false] if true, defines a
192
+ # predicate method for the argument, which returns true if the argument
193
+ # is not nil and not empty. Defaults to true for boolean arguments and
194
+ # false for all other arguments.
195
+ def arguments(name = nil, **)
196
+ if name.nil?
197
+ return defined_arguments unless defined_arguments.empty?
198
+
199
+ return superclass.arguments if superclass.respond_to?(:arguments)
200
+
201
+ return []
202
+ end
203
+
204
+ argument(name, **, variadic: true)
205
+ end
206
+
207
+ # Validates the given argument values against the defined class arguments.
208
+ #
209
+ # Also applies any default values from the defined arguments.
210
+ #
211
+ # @param values [Array] the arguments values to resolve.
212
+ #
213
+ # @return [Array] the arguments values with applied defaults.
214
+ #
215
+ # @raise [Cuprum::Cli::Arguments::ExtraArgumentsError] if provided more
216
+ # arguments than the command class defines arguments.
217
+ # @raise [Cuprum::Cli::Arguments::InvalidArgumentError] if any value does
218
+ # not match the expected argument type, or any required value is missing.
219
+ def resolve_arguments(*values) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
220
+ values = argument_values + values
221
+ defined_arguments = arguments
222
+
223
+ if defined_arguments.any?(&:variadic?)
224
+ return VariadicArgumentsResolver.new(defined_arguments).call(*values)
225
+ end
226
+
227
+ if values.size > defined_arguments.size
228
+ raise Cuprum::Cli::Arguments::ExtraArgumentsError,
229
+ extra_arguments_message(values.size)
230
+ end
231
+
232
+ defined_arguments.each.with_index.to_h do |argument, index|
233
+ [argument.name, argument.resolve(values[index])]
234
+ end
235
+ end
236
+
237
+ private
238
+
239
+ def arguments_builder
240
+ (
241
+ @arguments_builder ||= WeakRef.new(
242
+ Builder.new(command_class: self, defined_arguments:)
243
+ )
244
+ ).__getobj__
245
+ rescue RefError
246
+ # :nocov:
247
+ @arguments_builder = WeakRef.new(
248
+ Builder.new(command_class: self, defined_arguments:)
249
+ )
250
+ @arguments_builder.__getobj__
251
+ # :nocov:
252
+ end
253
+
254
+ def defined_argument_values
255
+ @defined_argument_values ||= []
256
+ end
257
+
258
+ def defined_arguments
259
+ @defined_arguments ||= []
260
+ end
261
+
262
+ def extra_arguments_message(count)
263
+ total_count = defined_arguments.size
264
+ last_required = (arguments.rindex(&:required?) || -1) + 1
265
+ expected =
266
+ last_required == total_count ? total_count : last_required..total_count
267
+
268
+ "wrong number of arguments (given #{count}, expected #{expected})"
269
+ end
270
+
271
+ def handle_multiple_variadic_arguments(variadic: false, **)
272
+ return unless variadic
273
+
274
+ matching = defined_arguments.find(&:variadic?)
275
+
276
+ return unless matching
277
+
278
+ message = "command already defines variadic argument :#{matching.name}"
279
+
280
+ raise ArgumentError, message
281
+ end
282
+ end
283
+ end