domainic-command 0.1.0.alpha.1.0.0 → 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 +4 -4
- data/.yardopts +11 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +1 -1
- data/README.md +93 -2
- data/lib/domainic/command/class_methods.rb +181 -0
- data/lib/domainic/command/context/attribute.rb +157 -0
- data/lib/domainic/command/context/attribute_set.rb +96 -0
- data/lib/domainic/command/context/behavior.rb +132 -0
- data/lib/domainic/command/context/input_context.rb +55 -0
- data/lib/domainic/command/context/output_context.rb +55 -0
- data/lib/domainic/command/context/runtime_context.rb +126 -0
- data/lib/domainic/command/errors/error.rb +23 -0
- data/lib/domainic/command/errors/execution_error.rb +40 -0
- data/lib/domainic/command/instance_methods.rb +92 -0
- data/lib/domainic/command/result/error_set.rb +272 -0
- data/lib/domainic/command/result/status.rb +49 -0
- data/lib/domainic/command/result.rb +194 -0
- data/lib/domainic/command.rb +77 -0
- data/lib/domainic-command.rb +3 -0
- data/sig/domainic/command/class_methods.rbs +100 -0
- data/sig/domainic/command/context/attribute.rbs +104 -0
- data/sig/domainic/command/context/attribute_set.rbs +69 -0
- data/sig/domainic/command/context/behavior.rbs +82 -0
- data/sig/domainic/command/context/input_context.rbs +40 -0
- data/sig/domainic/command/context/output_context.rbs +40 -0
- data/sig/domainic/command/context/runtime_context.rbs +90 -0
- data/sig/domainic/command/errors/error.rbs +21 -0
- data/sig/domainic/command/errors/execution_error.rbs +32 -0
- data/sig/domainic/command/instance_methods.rbs +56 -0
- data/sig/domainic/command/result/error_set.rbs +186 -0
- data/sig/domainic/command/result/status.rbs +47 -0
- data/sig/domainic/command/result.rbs +149 -0
- data/sig/domainic/command.rbs +67 -0
- data/sig/domainic-command.rbs +1 -0
- data/sig/manifest.yaml +1 -0
- metadata +50 -13
@@ -0,0 +1,272 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Domainic
|
4
|
+
module Command
|
5
|
+
class Result
|
6
|
+
# A flexible container for managing and formatting command errors. The ErrorSet provides a consistent
|
7
|
+
# interface for working with errors from various sources including simple strings, arrays, hashes,
|
8
|
+
# standard errors, and objects implementing a compatible `to_h` interface (like ActiveModel::Errors).
|
9
|
+
#
|
10
|
+
# @example Basic usage
|
11
|
+
# errors = ErrorSet.new("Something went wrong")
|
12
|
+
# errors[:generic] #=> ["Something went wrong"]
|
13
|
+
# errors.full_messages #=> ["generic Something went wrong"]
|
14
|
+
#
|
15
|
+
# @example Hash-style errors
|
16
|
+
# errors = ErrorSet.new(
|
17
|
+
# name: "can't be blank",
|
18
|
+
# email: ["invalid format", "already taken"]
|
19
|
+
# )
|
20
|
+
# errors[:name] #=> ["can't be blank"]
|
21
|
+
# errors[:email] #=> ["invalid format", "already taken"]
|
22
|
+
#
|
23
|
+
# @example ActiveModel compatibility
|
24
|
+
# user = User.new
|
25
|
+
# user.valid? #=> false
|
26
|
+
# errors = ErrorSet.new(user.errors)
|
27
|
+
# errors[:email] #=> ["can't be blank"]
|
28
|
+
#
|
29
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
30
|
+
# @since 0.1.0
|
31
|
+
class ErrorSet
|
32
|
+
# @rbs @lookup: Hash[Symbol, Array[String]]
|
33
|
+
|
34
|
+
# Creates a new ErrorSet instance
|
35
|
+
#
|
36
|
+
# @param errors [String, Array, Hash, StandardError, #to_h, nil] The errors to parse
|
37
|
+
#
|
38
|
+
# @raise [ArgumentError] If the errors cannot be parsed
|
39
|
+
# @return [ErrorSet] the new ErrorSet instance
|
40
|
+
# @rbs (?untyped? errors) -> void
|
41
|
+
def initialize(errors = nil)
|
42
|
+
@lookup = Parser.new(errors).parse!
|
43
|
+
end
|
44
|
+
|
45
|
+
# Retrieves error messages for a specific key
|
46
|
+
#
|
47
|
+
# @param key [String, Symbol] The error key to lookup
|
48
|
+
# @return [Array<String>, nil] The error messages for the key
|
49
|
+
# @rbs (String | Symbol key) -> Array[String]?
|
50
|
+
def [](key)
|
51
|
+
@lookup[key.to_sym]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Adds a new error message for a specific key
|
55
|
+
#
|
56
|
+
# @param key [String, Symbol] The error key
|
57
|
+
# @param message [String, Array<String>] The error message(s)
|
58
|
+
#
|
59
|
+
# @return [void]
|
60
|
+
# @rbs (String | Symbol key, Array[String] | String message) -> void
|
61
|
+
def add(key, message)
|
62
|
+
key = key.to_sym
|
63
|
+
@lookup[key] ||= []
|
64
|
+
@lookup[key].concat(Array(message)) # steep:ignore ArgumentTypeMismatch
|
65
|
+
end
|
66
|
+
|
67
|
+
# Clear all errors from the set
|
68
|
+
#
|
69
|
+
# @return [void]
|
70
|
+
# @rbs () -> void
|
71
|
+
def clear
|
72
|
+
@lookup = {}
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check if the error set is empty
|
76
|
+
#
|
77
|
+
# @return [Boolean] `true` if the error set is empty, `false` otherwise
|
78
|
+
# @rbs () -> bool
|
79
|
+
def empty?
|
80
|
+
@lookup.empty?
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns all error messages with their keys
|
84
|
+
#
|
85
|
+
# @return [Array<String>] All error messages prefixed with their keys
|
86
|
+
# @rbs () -> Array[String]
|
87
|
+
def full_messages
|
88
|
+
@lookup.each_with_object([]) do |(key, messages), result|
|
89
|
+
result.concat(messages.map { |message| "#{key} #{message}" })
|
90
|
+
end
|
91
|
+
end
|
92
|
+
alias to_a full_messages
|
93
|
+
alias to_array full_messages
|
94
|
+
alias to_ary full_messages
|
95
|
+
|
96
|
+
# Returns a hash of all error messages
|
97
|
+
#
|
98
|
+
# @return [Hash{Symbol => Array<String>}] All error messages grouped by key
|
99
|
+
# @rbs () -> Hash[Symbol, Array[String]]
|
100
|
+
def messages
|
101
|
+
@lookup.dup.freeze
|
102
|
+
end
|
103
|
+
alias to_h messages
|
104
|
+
alias to_hash messages
|
105
|
+
|
106
|
+
# A utility class for parsing various error formats into a consistent structure
|
107
|
+
#
|
108
|
+
# @!visibility private
|
109
|
+
# @api private
|
110
|
+
#
|
111
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
112
|
+
# @since 0.1.0
|
113
|
+
class Parser
|
114
|
+
# Mapping of classes to their parsing strategy methods
|
115
|
+
#
|
116
|
+
# @return [Hash{Class, Module => Symbol}]
|
117
|
+
TYPE_STRATEGIES = {
|
118
|
+
Array => :parse_array,
|
119
|
+
Hash => :parse_hash,
|
120
|
+
String => :parse_generic_error,
|
121
|
+
StandardError => :parse_standard_error
|
122
|
+
}.freeze #: Hash[Class | Module, Symbol]
|
123
|
+
|
124
|
+
# Mapping of method names to their parsing strategy methods
|
125
|
+
#
|
126
|
+
# @return [Hash{Symbol => Symbol}]
|
127
|
+
RESPOND_TO_STRATEGIES = { to_h: :parse_to_h }.freeze #: Hash[Symbol, Symbol]
|
128
|
+
|
129
|
+
# Create a new Parser instance
|
130
|
+
#
|
131
|
+
# @param errors [Object] the errors to parse
|
132
|
+
#
|
133
|
+
# @return [Parser] the new Parser instance
|
134
|
+
# @rbs (untyped errors) -> void
|
135
|
+
def initialize(errors)
|
136
|
+
@errors = errors
|
137
|
+
@parsed = {}
|
138
|
+
end
|
139
|
+
|
140
|
+
# Parses the errors into a consistent format
|
141
|
+
#
|
142
|
+
# @raise [ArgumentError] If the errors cannot be parsed
|
143
|
+
# @return [Hash{Symbol => Array<String>}]
|
144
|
+
# @rbs () -> Hash[Symbol, Array[String]]
|
145
|
+
def parse!
|
146
|
+
parse_errors!
|
147
|
+
@parsed.transform_values { |errors| Array(errors) }
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
# Parses an array of errors into the generic error category
|
153
|
+
#
|
154
|
+
# @param errors [Array<String, StandardError>] Array of errors to parse
|
155
|
+
#
|
156
|
+
# @raise [ArgumentError] If any array element is not a String or StandardError
|
157
|
+
# @return [void]
|
158
|
+
# @rbs (Array[String | StandardError] errors) -> void
|
159
|
+
def parse_array(errors)
|
160
|
+
errors.each do |error|
|
161
|
+
case error
|
162
|
+
when String then parse_generic_error(error)
|
163
|
+
when StandardError then parse_standard_error(error)
|
164
|
+
else raise_invalid_errors!
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Determines the appropriate parsing strategy and executes it
|
170
|
+
#
|
171
|
+
# @raise [ArgumentError] If no valid parsing strategy is found
|
172
|
+
# @return [void]
|
173
|
+
# @rbs () -> void
|
174
|
+
def parse_errors!
|
175
|
+
return if @errors.nil?
|
176
|
+
|
177
|
+
TYPE_STRATEGIES.each_pair { |type, strategy| return send(strategy, @errors) if @errors.is_a?(type) }
|
178
|
+
|
179
|
+
RESPOND_TO_STRATEGIES.each_pair do |method, strategy|
|
180
|
+
return send(strategy, @errors) if @errors.respond_to?(method)
|
181
|
+
end
|
182
|
+
|
183
|
+
raise_invalid_errors!
|
184
|
+
end
|
185
|
+
|
186
|
+
# Parses a string or array of strings into the generic error category
|
187
|
+
#
|
188
|
+
# @param errors [String, Array<String>] The error(s) to parse
|
189
|
+
#
|
190
|
+
# @return [void]
|
191
|
+
# @rbs (Array[String] | String errors) -> void
|
192
|
+
def parse_generic_error(errors)
|
193
|
+
@parsed[:generic] ||= []
|
194
|
+
@parsed[:generic].concat(Array(errors))
|
195
|
+
end
|
196
|
+
|
197
|
+
# Parses a hash of errors into categorized messages
|
198
|
+
#
|
199
|
+
# @param errors [Hash{String, Symbol => Array<String>, String}] Hash of errors
|
200
|
+
#
|
201
|
+
# @raise [ArgumentError] If any value cannot be parsed
|
202
|
+
# @return [void]
|
203
|
+
# @rbs (Hash[String | Symbol, Array[StandardError] | Array[String] | StandardError | String] errors) -> void
|
204
|
+
def parse_hash(errors)
|
205
|
+
@parsed.merge!(errors.transform_keys(&:to_sym).transform_values { |value| parse_hash_value(value) })
|
206
|
+
end
|
207
|
+
|
208
|
+
# Parses a single value from a hash array
|
209
|
+
#
|
210
|
+
# @param value [String, StandardError] The value to parse
|
211
|
+
#
|
212
|
+
# @raise [ArgumentError] If the value is neither a String nor StandardError
|
213
|
+
# @return [String] The parsed error message
|
214
|
+
# @rbs (String | StandardError value) -> String
|
215
|
+
def parse_hash_array_value(value)
|
216
|
+
case value
|
217
|
+
when String then value
|
218
|
+
when StandardError then value.message
|
219
|
+
else raise_invalid_errors!
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Parses a value from a hash of errors
|
224
|
+
#
|
225
|
+
# @param value [String, StandardError, Array<String, StandardError>] The value to parse
|
226
|
+
#
|
227
|
+
# @raise [ArgumentError] If the value cannot be parsed
|
228
|
+
# @return [Array<String>] The parsed error message(s)
|
229
|
+
# @rbs (String | StandardError | Array[String | StandardError] value) -> Array[String]
|
230
|
+
def parse_hash_value(value)
|
231
|
+
case value
|
232
|
+
when String then [value]
|
233
|
+
when StandardError then [value.message]
|
234
|
+
when Array then value.map { |array_value| parse_hash_array_value(array_value) }
|
235
|
+
else raise_invalid_errors!
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Parses a StandardError into the generic error category
|
240
|
+
#
|
241
|
+
# @param errors [StandardError] The error to parse
|
242
|
+
#
|
243
|
+
# @return [void]
|
244
|
+
# @rbs (StandardError errors) -> void
|
245
|
+
def parse_standard_error(errors)
|
246
|
+
parse_generic_error(errors.message)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Parses an object that responds to to_h
|
250
|
+
#
|
251
|
+
# @param errors [#to_h] The object to parse
|
252
|
+
#
|
253
|
+
# @raise [ArgumentError] If the hash cannot be parsed
|
254
|
+
# @return [void]
|
255
|
+
# @rbs (untyped errors) -> void
|
256
|
+
def parse_to_h(errors)
|
257
|
+
parse_hash(errors.to_h)
|
258
|
+
end
|
259
|
+
|
260
|
+
# Raises an invalid errors exception
|
261
|
+
#
|
262
|
+
# @raise [ArgumentError] Always raises with an invalid errors message
|
263
|
+
# @return [void]
|
264
|
+
# @rbs () -> void
|
265
|
+
def raise_invalid_errors!
|
266
|
+
raise ArgumentError, "invalid errors: #{@errors}"
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Domainic
|
4
|
+
module Command
|
5
|
+
class Result
|
6
|
+
# Defines status codes for command execution results. These codes follow Unix exit code conventions,
|
7
|
+
# making them suitable for CLI applications while remaining useful for other contexts.
|
8
|
+
#
|
9
|
+
# The status codes are specifically chosen to provide meaningful feedback about where in the command
|
10
|
+
# lifecycle a failure occurred:
|
11
|
+
# * 0 (SUCCESS) - The command completed successfully
|
12
|
+
# * 1 (FAILED_AT_RUNTIME) - The command failed during execution
|
13
|
+
# * 2 (FAILED_AT_INPUT) - The command failed during input validation
|
14
|
+
# * 3 (FAILED_AT_OUTPUT) - The command failed during output validation
|
15
|
+
#
|
16
|
+
# @example Using with CLI
|
17
|
+
# class MyCLI
|
18
|
+
# def self.run
|
19
|
+
# result = MyCommand.call(args)
|
20
|
+
# exit(result.status_code)
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
25
|
+
# @since 0.1.0
|
26
|
+
module STATUS
|
27
|
+
# Indicates successful command execution
|
28
|
+
#
|
29
|
+
# @return [Integer] status code 0
|
30
|
+
SUCCESS = 0 #: Integer
|
31
|
+
|
32
|
+
# Indicates a failure during command execution
|
33
|
+
#
|
34
|
+
# @return [Integer] status code 1
|
35
|
+
FAILED_AT_RUNTIME = 1 #: Integer
|
36
|
+
|
37
|
+
# Indicates a failure during input validation
|
38
|
+
#
|
39
|
+
# @return [Integer] status code 2
|
40
|
+
FAILED_AT_INPUT = 2 #: Integer
|
41
|
+
|
42
|
+
# Indicates a failure during output validation
|
43
|
+
#
|
44
|
+
# @return [Integer] status code 3
|
45
|
+
FAILED_AT_OUTPUT = 3 #: Integer
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'domainic/command/result/error_set'
|
4
|
+
require 'domainic/command/result/status'
|
5
|
+
|
6
|
+
module Domainic
|
7
|
+
module Command
|
8
|
+
# A value object representing the outcome of a command execution. The Result class provides
|
9
|
+
# a consistent interface for handling both successful and failed command executions, including
|
10
|
+
# return data and error information.
|
11
|
+
#
|
12
|
+
# Results are created through factory methods rather than direct instantiation, making the
|
13
|
+
# intent of the result clear:
|
14
|
+
#
|
15
|
+
# @example Creating a success result
|
16
|
+
# result = Result.success(value: 42)
|
17
|
+
# result.successful? #=> true
|
18
|
+
# result.value #=> 42
|
19
|
+
#
|
20
|
+
# @example Creating a failure result
|
21
|
+
# result = Result.failure_at_input(
|
22
|
+
# { name: "can't be blank" },
|
23
|
+
# context: { attempted_name: nil }
|
24
|
+
# )
|
25
|
+
# result.failure? #=> true
|
26
|
+
# result.errors[:name] #=> ["can't be blank"]
|
27
|
+
#
|
28
|
+
# Results use status codes that align with Unix exit codes, making them suitable for
|
29
|
+
# CLI applications:
|
30
|
+
# * 0 - Successful execution
|
31
|
+
# * 1 - Runtime failure
|
32
|
+
# * 2 - Input validation failure
|
33
|
+
# * 3 - Output validation failure
|
34
|
+
#
|
35
|
+
# @example CLI usage
|
36
|
+
# def self.run
|
37
|
+
# result = MyCommand.call(args)
|
38
|
+
# puts result.errors.full_messages if result.failure?
|
39
|
+
# exit(result.status_code)
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
43
|
+
# @since 0.1.0
|
44
|
+
class Result
|
45
|
+
# @rbs @data: Struct
|
46
|
+
# @rbs @errors: ErrorSet
|
47
|
+
# @rbs @status_code: Integer
|
48
|
+
|
49
|
+
# The structured data returned by the command
|
50
|
+
#
|
51
|
+
# @return [Struct] A frozen struct containing the command's output data
|
52
|
+
attr_reader :data #: Struct
|
53
|
+
|
54
|
+
# The errors that occurred during command execution
|
55
|
+
#
|
56
|
+
# @return [ErrorSet] The set of errors from the command
|
57
|
+
attr_reader :errors #: ErrorSet
|
58
|
+
|
59
|
+
# The status code indicating the result of the command execution
|
60
|
+
#
|
61
|
+
# @return [Integer] The status code (0 for success, non-zero for failures)
|
62
|
+
attr_reader :status_code #: Integer
|
63
|
+
|
64
|
+
# Creates a new failure result with the given status
|
65
|
+
#
|
66
|
+
# @param errors [Object] The errors that caused the failure
|
67
|
+
# @param context [Hash] Optional context data for the failure
|
68
|
+
# @param status [Integer] The status code for the failure (defaults to FAILED_AT_RUNTIME)
|
69
|
+
#
|
70
|
+
# @return [Result] A new failure result
|
71
|
+
# @rbs (untyped errors, ?Hash[String | Symbol, untyped] context, ?status: Integer) -> Result
|
72
|
+
def self.failure(errors, context = {}, status: STATUS::FAILED_AT_RUNTIME)
|
73
|
+
new(status, context:, errors:)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Creates a new input validation failure result
|
77
|
+
#
|
78
|
+
# @param errors [Object] The validation errors
|
79
|
+
# @param context [Hash] Optional context data for the failure
|
80
|
+
#
|
81
|
+
# @return [Result] A new input validation failure result
|
82
|
+
# @rbs (untyped errors, ?Hash[String | Symbol, untyped] context) -> Result
|
83
|
+
def self.failure_at_input(errors, context = {})
|
84
|
+
new(STATUS::FAILED_AT_INPUT, context:, errors:)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Creates a new output validation failure result
|
88
|
+
#
|
89
|
+
# @param errors [Object] The validation errors
|
90
|
+
# @param context [Hash] Optional context data for the failure
|
91
|
+
#
|
92
|
+
# @return [Result] A new output validation failure result
|
93
|
+
# @rbs (untyped errors, ?Hash[String | Symbol, untyped] context) -> Result
|
94
|
+
def self.failure_at_output(errors, context = {})
|
95
|
+
new(STATUS::FAILED_AT_OUTPUT, context:, errors:)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Creates a new success result
|
99
|
+
#
|
100
|
+
# @param context [Hash] The successful result data
|
101
|
+
#
|
102
|
+
# @return [Result] A new success result
|
103
|
+
# @rbs (Hash[String | Symbol, untyped] context) -> Result
|
104
|
+
def self.success(context)
|
105
|
+
new(STATUS::SUCCESS, context:)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Creates a new result instance
|
109
|
+
#
|
110
|
+
# @param status_code [Integer] The status code for the result
|
111
|
+
# @param context [Hash] The data context for the result
|
112
|
+
# @param errors [Object, nil] Any errors that occurred
|
113
|
+
#
|
114
|
+
# @raise [ArgumentError] If status_code is invalid or context is not a Hash
|
115
|
+
# @return [void]
|
116
|
+
# @rbs (Integer status, ?context: Hash[String | Symbol, untyped], ?errors: untyped) -> void
|
117
|
+
def initialize(status_code, context: {}, errors: nil)
|
118
|
+
initialize_status_code(status_code)
|
119
|
+
initialize_data(context)
|
120
|
+
@errors = ErrorSet.new(errors)
|
121
|
+
end
|
122
|
+
private_class_method :new
|
123
|
+
|
124
|
+
# Indicates whether the command failed
|
125
|
+
#
|
126
|
+
# @return [Boolean] true if the command failed; false otherwise
|
127
|
+
# @rbs () -> bool
|
128
|
+
def failure?
|
129
|
+
status_code != STATUS::SUCCESS
|
130
|
+
end
|
131
|
+
alias failed? failure?
|
132
|
+
|
133
|
+
# Indicates whether the command succeeded
|
134
|
+
#
|
135
|
+
# @return [Boolean] true if the command succeeded; false otherwise
|
136
|
+
# @rbs () -> bool
|
137
|
+
def successful?
|
138
|
+
status_code == STATUS::SUCCESS
|
139
|
+
end
|
140
|
+
alias success? successful?
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# Initializes the data struct from the context hash
|
145
|
+
#
|
146
|
+
# @param context [Hash] The context hash to convert to a struct
|
147
|
+
# @raise [ArgumentError] If context is not a Hash
|
148
|
+
# @return [void]
|
149
|
+
def initialize_data(context)
|
150
|
+
raise ArgumentError, ':context must be a Hash' unless context.is_a?(Hash)
|
151
|
+
|
152
|
+
context = context.transform_keys(&:to_sym)
|
153
|
+
@data = Struct.new(nil).new.freeze if context.empty?
|
154
|
+
@data = Struct.new(*context.keys, keyword_init: true).new(**context).freeze unless context.empty?
|
155
|
+
end
|
156
|
+
|
157
|
+
# Validates and initializes the status code
|
158
|
+
#
|
159
|
+
# @param status_code [Integer] The status code to validate
|
160
|
+
# @raise [ArgumentError] If the status code is not valid
|
161
|
+
# @return [void]
|
162
|
+
def initialize_status_code(status_code)
|
163
|
+
unless STATUS.constants.map { |c| STATUS.const_get(c) }.include?(status_code)
|
164
|
+
raise ArgumentError, "invalid status code: #{status_code}"
|
165
|
+
end
|
166
|
+
|
167
|
+
@status_code = status_code
|
168
|
+
end
|
169
|
+
|
170
|
+
# Delegate method calls to the data struct
|
171
|
+
#
|
172
|
+
# @param method_name [String, Symbol] The method name to call
|
173
|
+
#
|
174
|
+
# @return [Object] The result of the method call
|
175
|
+
# @rbs override
|
176
|
+
def method_missing(method_name, ...)
|
177
|
+
return super unless respond_to_missing?(method_name)
|
178
|
+
|
179
|
+
data.public_send(method_name.to_sym)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Indicates whether the data struct responds to the given method
|
183
|
+
#
|
184
|
+
# @param method_name [String, Symbol] The method name to check
|
185
|
+
# @param _include_private [Boolean] Whether to include private methods
|
186
|
+
#
|
187
|
+
# @return [Boolean] `true` if the data struct responds to the method; `false` otherwise
|
188
|
+
# @rbs (String | Symbol method_name, ?bool include_private) -> bool
|
189
|
+
def respond_to_missing?(method_name, _include_private = false)
|
190
|
+
data.respond_to?(method_name.to_sym) || super
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'domainic/command/class_methods'
|
4
|
+
require 'domainic/command/instance_methods'
|
5
|
+
|
6
|
+
module Domainic
|
7
|
+
# A module that implements the Command pattern, providing a structured way to encapsulate business operations.
|
8
|
+
# Commands are single-purpose objects that perform a specific action, validating their inputs and outputs while
|
9
|
+
# maintaining a consistent interface for error handling and result reporting.
|
10
|
+
#
|
11
|
+
# @abstract Including classes must implement an {#execute} method that defines the command's business logic.
|
12
|
+
# The {#execute} method has access to validated inputs via the {#context} accessor and should set any output values
|
13
|
+
# on the context before returning.
|
14
|
+
#
|
15
|
+
# @example Basic command definition
|
16
|
+
# class CreateUser
|
17
|
+
# include Domainic::Command
|
18
|
+
#
|
19
|
+
# argument :login, String, "The user's login", required: true
|
20
|
+
# argument :password, String, "The user's password", required: true
|
21
|
+
#
|
22
|
+
# output :user, User, "The created user", required: true
|
23
|
+
# output :created_at, Time, "When the user was created"
|
24
|
+
#
|
25
|
+
# def execute
|
26
|
+
# user = User.create!(login: context.login, password: context.password)
|
27
|
+
# context.user = user
|
28
|
+
# context.created_at = Time.current
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# @example Using external context classes
|
33
|
+
# class CreateUserInput < Domainic::Command::Context::InputContext
|
34
|
+
# argument :login, String, "The user's login", required: true
|
35
|
+
# argument :password, String, "The user's password", required: true
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# class CreateUserOutput < Domainic::Command::Context::OutputContext
|
39
|
+
# field :user, User, "The created user", required: true
|
40
|
+
# field :created_at, Time, "When the user was created"
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# class CreateUser
|
44
|
+
# include Domainic::Command
|
45
|
+
#
|
46
|
+
# accepts_arguments_matching CreateUserInput
|
47
|
+
# returns_output_matching CreateUserOutput
|
48
|
+
#
|
49
|
+
# def execute
|
50
|
+
# user = User.create!(login: context.login, password: context.password)
|
51
|
+
# context.user = user
|
52
|
+
# context.created_at = Time.current
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# @example Command usage
|
57
|
+
# # Successful execution
|
58
|
+
# result = CreateUser.call(login: "user@example.com", password: "secret123")
|
59
|
+
# result.successful? #=> true
|
60
|
+
# result.user #=> #<User id: 1, login: "user@example.com">
|
61
|
+
#
|
62
|
+
# # Failed execution
|
63
|
+
# result = CreateUser.call(login: "invalid")
|
64
|
+
# result.failure? #=> true
|
65
|
+
# result.errors[:password] #=> ["is required"]
|
66
|
+
#
|
67
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
68
|
+
# @since 0.1.0
|
69
|
+
module Command
|
70
|
+
# @rbs override
|
71
|
+
def self.included(base)
|
72
|
+
super
|
73
|
+
base.include(InstanceMethods)
|
74
|
+
base.extend(ClassMethods)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Domainic
|
2
|
+
module Command
|
3
|
+
# Class methods that are extended onto any class that includes {Command}. These methods provide
|
4
|
+
# the DSL for defining command inputs and outputs, as well as class-level execution methods.
|
5
|
+
#
|
6
|
+
# @author {https://aaronmallen.me Aaron Allen}
|
7
|
+
# @since 0.1.0
|
8
|
+
module ClassMethods
|
9
|
+
@input_context_class: singleton(Context::InputContext)
|
10
|
+
|
11
|
+
@runtime_context_class: singleton(Context::RuntimeContext)
|
12
|
+
|
13
|
+
@output_context_class: singleton(Context::OutputContext)
|
14
|
+
|
15
|
+
# Specifies an external input context class for the command
|
16
|
+
#
|
17
|
+
# @param input_context_class [Class] A subclass of {Context::InputContext}
|
18
|
+
#
|
19
|
+
# @raise [ArgumentError] If the provided class is not a subclass of {Context::InputContext}
|
20
|
+
# @return [void]
|
21
|
+
def accepts_arguments_matching: (singleton(Context::InputContext) input_context_class) -> void
|
22
|
+
|
23
|
+
# Defines an input argument for the command
|
24
|
+
#
|
25
|
+
# @overload argument(name, *type_validator_and_description, **options)
|
26
|
+
# @param name [String, Symbol] The name of the argument
|
27
|
+
# @param type_validator_and_description [Array<Class, Module, Object, Proc, String, nil>] Type validator or
|
28
|
+
# description arguments
|
29
|
+
# @param options [Hash] Configuration options for the argument
|
30
|
+
# @option options [Object] :default A static default value
|
31
|
+
# @option options [Proc] :default_generator A proc that generates the default value
|
32
|
+
# @option options [Object] :default_value Alias for :default
|
33
|
+
# @option options [String, nil] :desc Short description of the argument
|
34
|
+
# @option options [String, nil] :description Full description of the argument
|
35
|
+
# @option options [Boolean] :required Whether the argument is required
|
36
|
+
# @option options [Class, Module, Object, Proc] :type A type validator
|
37
|
+
#
|
38
|
+
# @return [void]
|
39
|
+
def argument: (String | Symbol name, *(Class | Module | Object | Proc | String)? type_validator_and_description, ?default: untyped, ?default_generator: untyped, ?default_value: untyped, ?desc: String?, ?description: String?, ?required: bool, ?type: Class | Module | Object | Proc) -> void
|
40
|
+
|
41
|
+
# Executes the command with the given context, handling any errors
|
42
|
+
#
|
43
|
+
# @param context [Hash] The input context for the command
|
44
|
+
#
|
45
|
+
# @return [Result] The result of the command execution
|
46
|
+
def call: (**untyped context) -> Result
|
47
|
+
|
48
|
+
# Executes the command with the given context, raising any errors
|
49
|
+
#
|
50
|
+
# @param context [Hash] The input context for the command
|
51
|
+
#
|
52
|
+
# @raise [ExecutionError] If the command execution fails
|
53
|
+
# @return [Result] The result of the command execution
|
54
|
+
def call!: (**untyped context) -> Result
|
55
|
+
|
56
|
+
# Defines an output field for the command
|
57
|
+
#
|
58
|
+
# @overload output(name, *type_validator_and_description, **options)
|
59
|
+
# @param name [String, Symbol] The name of the output field
|
60
|
+
# @param type_validator_and_description [Array<Class, Module, Object, Proc, String, nil>] Type validator or
|
61
|
+
# description arguments
|
62
|
+
# @param options [Hash] Configuration options for the output
|
63
|
+
# @option options [Object] :default A static default value
|
64
|
+
# @option options [Proc] :default_generator A proc that generates the default value
|
65
|
+
# @option options [Object] :default_value Alias for :default
|
66
|
+
# @option options [String, nil] :desc Short description of the output
|
67
|
+
# @option options [String, nil] :description Full description of the output
|
68
|
+
# @option options [Boolean] :required Whether the output is required
|
69
|
+
# @option options [Class, Module, Object, Proc] :type A type validator
|
70
|
+
#
|
71
|
+
# @return [void]
|
72
|
+
def output: (String | Symbol name, *(Class | Module | Object | Proc | String)? type_validator_and_description, ?default: untyped, ?default_generator: untyped, ?default_value: untyped, ?desc: String?, ?description: String?, ?required: bool, ?type: Class | Module | Object | Proc) -> void
|
73
|
+
|
74
|
+
# Specifies an external output context class for the command
|
75
|
+
#
|
76
|
+
# @param output_context_class [Class] A subclass of {Context::OutputContext}
|
77
|
+
#
|
78
|
+
# @raise [ArgumentError] If the provided class is not a subclass of {Context::OutputContext}
|
79
|
+
# @return [void]
|
80
|
+
def returns_data_matching: (singleton(Context::OutputContext) output_context_class) -> void
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Returns the input context class for the command
|
85
|
+
#
|
86
|
+
# @return [Class] A subclass of {Context::InputContext}
|
87
|
+
def input_context_class: () -> singleton(Context::InputContext)
|
88
|
+
|
89
|
+
# Returns the output context class for the command
|
90
|
+
#
|
91
|
+
# @return [Class] A subclass of {Context::OutputContext}
|
92
|
+
def output_context_class: () -> singleton(Context::OutputContext)
|
93
|
+
|
94
|
+
# Returns the runtime context class for the command
|
95
|
+
#
|
96
|
+
# @return [Class] A subclass of {Context::RuntimeContext}
|
97
|
+
def runtime_context_class: () -> singleton(Context::RuntimeContext)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|