domainic-command 0.1.0.alpha.1.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +11 -0
  3. data/CHANGELOG.md +19 -0
  4. data/LICENSE +1 -1
  5. data/README.md +93 -2
  6. data/lib/domainic/command/class_methods.rb +181 -0
  7. data/lib/domainic/command/context/attribute.rb +157 -0
  8. data/lib/domainic/command/context/attribute_set.rb +96 -0
  9. data/lib/domainic/command/context/behavior.rb +132 -0
  10. data/lib/domainic/command/context/input_context.rb +55 -0
  11. data/lib/domainic/command/context/output_context.rb +55 -0
  12. data/lib/domainic/command/context/runtime_context.rb +126 -0
  13. data/lib/domainic/command/errors/error.rb +23 -0
  14. data/lib/domainic/command/errors/execution_error.rb +40 -0
  15. data/lib/domainic/command/instance_methods.rb +92 -0
  16. data/lib/domainic/command/result/error_set.rb +272 -0
  17. data/lib/domainic/command/result/status.rb +49 -0
  18. data/lib/domainic/command/result.rb +194 -0
  19. data/lib/domainic/command.rb +77 -0
  20. data/lib/domainic-command.rb +3 -0
  21. data/sig/domainic/command/class_methods.rbs +100 -0
  22. data/sig/domainic/command/context/attribute.rbs +104 -0
  23. data/sig/domainic/command/context/attribute_set.rbs +69 -0
  24. data/sig/domainic/command/context/behavior.rbs +82 -0
  25. data/sig/domainic/command/context/input_context.rbs +40 -0
  26. data/sig/domainic/command/context/output_context.rbs +40 -0
  27. data/sig/domainic/command/context/runtime_context.rbs +90 -0
  28. data/sig/domainic/command/errors/error.rbs +21 -0
  29. data/sig/domainic/command/errors/execution_error.rbs +32 -0
  30. data/sig/domainic/command/instance_methods.rbs +56 -0
  31. data/sig/domainic/command/result/error_set.rbs +186 -0
  32. data/sig/domainic/command/result/status.rbs +47 -0
  33. data/sig/domainic/command/result.rbs +149 -0
  34. data/sig/domainic/command.rbs +67 -0
  35. data/sig/domainic-command.rbs +1 -0
  36. data/sig/manifest.yaml +1 -0
  37. metadata +50 -13
@@ -0,0 +1,186 @@
1
+ module Domainic
2
+ module Command
3
+ class Result
4
+ # A flexible container for managing and formatting command errors. The ErrorSet provides a consistent
5
+ # interface for working with errors from various sources including simple strings, arrays, hashes,
6
+ # standard errors, and objects implementing a compatible `to_h` interface (like ActiveModel::Errors).
7
+ #
8
+ # @example Basic usage
9
+ # errors = ErrorSet.new("Something went wrong")
10
+ # errors[:generic] #=> ["Something went wrong"]
11
+ # errors.full_messages #=> ["generic Something went wrong"]
12
+ #
13
+ # @example Hash-style errors
14
+ # errors = ErrorSet.new(
15
+ # name: "can't be blank",
16
+ # email: ["invalid format", "already taken"]
17
+ # )
18
+ # errors[:name] #=> ["can't be blank"]
19
+ # errors[:email] #=> ["invalid format", "already taken"]
20
+ #
21
+ # @example ActiveModel compatibility
22
+ # user = User.new
23
+ # user.valid? #=> false
24
+ # errors = ErrorSet.new(user.errors)
25
+ # errors[:email] #=> ["can't be blank"]
26
+ #
27
+ # @author {https://aaronmallen.me Aaron Allen}
28
+ # @since 0.1.0
29
+ class ErrorSet
30
+ @lookup: Hash[Symbol, Array[String]]
31
+
32
+ # Creates a new ErrorSet instance
33
+ #
34
+ # @param errors [String, Array, Hash, StandardError, #to_h, nil] The errors to parse
35
+ #
36
+ # @raise [ArgumentError] If the errors cannot be parsed
37
+ # @return [ErrorSet] the new ErrorSet instance
38
+ def initialize: (?untyped? errors) -> void
39
+
40
+ # Retrieves error messages for a specific key
41
+ #
42
+ # @param key [String, Symbol] The error key to lookup
43
+ # @return [Array<String>, nil] The error messages for the key
44
+ def []: (String | Symbol key) -> Array[String]?
45
+
46
+ # Adds a new error message for a specific key
47
+ #
48
+ # @param key [String, Symbol] The error key
49
+ # @param message [String, Array<String>] The error message(s)
50
+ #
51
+ # @return [void]
52
+ def add: (String | Symbol key, Array[String] | String message) -> void
53
+
54
+ # Clear all errors from the set
55
+ #
56
+ # @return [void]
57
+ def clear: () -> void
58
+
59
+ # Check if the error set is empty
60
+ #
61
+ # @return [Boolean] `true` if the error set is empty, `false` otherwise
62
+ def empty?: () -> bool
63
+
64
+ # Returns all error messages with their keys
65
+ #
66
+ # @return [Array<String>] All error messages prefixed with their keys
67
+ def full_messages: () -> Array[String]
68
+
69
+ alias to_a full_messages
70
+
71
+ alias to_array full_messages
72
+
73
+ alias to_ary full_messages
74
+
75
+ # Returns a hash of all error messages
76
+ #
77
+ # @return [Hash{Symbol => Array<String>}] All error messages grouped by key
78
+ def messages: () -> Hash[Symbol, Array[String]]
79
+
80
+ alias to_h messages
81
+
82
+ alias to_hash messages
83
+
84
+ # A utility class for parsing various error formats into a consistent structure
85
+ #
86
+ # @!visibility private
87
+ # @api private
88
+ #
89
+ # @author {https://aaronmallen.me Aaron Allen}
90
+ # @since 0.1.0
91
+ class Parser
92
+ # Mapping of classes to their parsing strategy methods
93
+ #
94
+ # @return [Hash{Class, Module => Symbol}]
95
+ TYPE_STRATEGIES: Hash[Class | Module, Symbol]
96
+
97
+ # Mapping of method names to their parsing strategy methods
98
+ #
99
+ # @return [Hash{Symbol => Symbol}]
100
+ RESPOND_TO_STRATEGIES: Hash[Symbol, Symbol]
101
+
102
+ # Create a new Parser instance
103
+ #
104
+ # @param errors [Object] the errors to parse
105
+ #
106
+ # @return [Parser] the new Parser instance
107
+ def initialize: (untyped errors) -> void
108
+
109
+ # Parses the errors into a consistent format
110
+ #
111
+ # @raise [ArgumentError] If the errors cannot be parsed
112
+ # @return [Hash{Symbol => Array<String>}]
113
+ def parse!: () -> Hash[Symbol, Array[String]]
114
+
115
+ private
116
+
117
+ # Parses an array of errors into the generic error category
118
+ #
119
+ # @param errors [Array<String, StandardError>] Array of errors to parse
120
+ #
121
+ # @raise [ArgumentError] If any array element is not a String or StandardError
122
+ # @return [void]
123
+ def parse_array: (Array[String | StandardError] errors) -> void
124
+
125
+ # Determines the appropriate parsing strategy and executes it
126
+ #
127
+ # @raise [ArgumentError] If no valid parsing strategy is found
128
+ # @return [void]
129
+ def parse_errors!: () -> void
130
+
131
+ # Parses a string or array of strings into the generic error category
132
+ #
133
+ # @param errors [String, Array<String>] The error(s) to parse
134
+ #
135
+ # @return [void]
136
+ def parse_generic_error: (Array[String] | String errors) -> void
137
+
138
+ # Parses a hash of errors into categorized messages
139
+ #
140
+ # @param errors [Hash{String, Symbol => Array<String>, String}] Hash of errors
141
+ #
142
+ # @raise [ArgumentError] If any value cannot be parsed
143
+ # @return [void]
144
+ def parse_hash: (Hash[String | Symbol, Array[StandardError] | Array[String] | StandardError | String] errors) -> void
145
+
146
+ # Parses a single value from a hash array
147
+ #
148
+ # @param value [String, StandardError] The value to parse
149
+ #
150
+ # @raise [ArgumentError] If the value is neither a String nor StandardError
151
+ # @return [String] The parsed error message
152
+ def parse_hash_array_value: (String | StandardError value) -> String
153
+
154
+ # Parses a value from a hash of errors
155
+ #
156
+ # @param value [String, StandardError, Array<String, StandardError>] The value to parse
157
+ #
158
+ # @raise [ArgumentError] If the value cannot be parsed
159
+ # @return [Array<String>] The parsed error message(s)
160
+ def parse_hash_value: (String | StandardError | Array[String | StandardError] value) -> Array[String]
161
+
162
+ # Parses a StandardError into the generic error category
163
+ #
164
+ # @param errors [StandardError] The error to parse
165
+ #
166
+ # @return [void]
167
+ def parse_standard_error: (StandardError errors) -> void
168
+
169
+ # Parses an object that responds to to_h
170
+ #
171
+ # @param errors [#to_h] The object to parse
172
+ #
173
+ # @raise [ArgumentError] If the hash cannot be parsed
174
+ # @return [void]
175
+ def parse_to_h: (untyped errors) -> void
176
+
177
+ # Raises an invalid errors exception
178
+ #
179
+ # @raise [ArgumentError] Always raises with an invalid errors message
180
+ # @return [void]
181
+ def raise_invalid_errors!: () -> void
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,47 @@
1
+ module Domainic
2
+ module Command
3
+ class Result
4
+ # Defines status codes for command execution results. These codes follow Unix exit code conventions,
5
+ # making them suitable for CLI applications while remaining useful for other contexts.
6
+ #
7
+ # The status codes are specifically chosen to provide meaningful feedback about where in the command
8
+ # lifecycle a failure occurred:
9
+ # * 0 (SUCCESS) - The command completed successfully
10
+ # * 1 (FAILED_AT_RUNTIME) - The command failed during execution
11
+ # * 2 (FAILED_AT_INPUT) - The command failed during input validation
12
+ # * 3 (FAILED_AT_OUTPUT) - The command failed during output validation
13
+ #
14
+ # @example Using with CLI
15
+ # class MyCLI
16
+ # def self.run
17
+ # result = MyCommand.call(args)
18
+ # exit(result.status_code)
19
+ # end
20
+ # end
21
+ #
22
+ # @author {https://aaronmallen.me Aaron Allen}
23
+ # @since 0.1.0
24
+ module STATUS
25
+ # Indicates successful command execution
26
+ #
27
+ # @return [Integer] status code 0
28
+ SUCCESS: Integer
29
+
30
+ # Indicates a failure during command execution
31
+ #
32
+ # @return [Integer] status code 1
33
+ FAILED_AT_RUNTIME: Integer
34
+
35
+ # Indicates a failure during input validation
36
+ #
37
+ # @return [Integer] status code 2
38
+ FAILED_AT_INPUT: Integer
39
+
40
+ # Indicates a failure during output validation
41
+ #
42
+ # @return [Integer] status code 3
43
+ FAILED_AT_OUTPUT: Integer
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,149 @@
1
+ module Domainic
2
+ module Command
3
+ # A value object representing the outcome of a command execution. The Result class provides
4
+ # a consistent interface for handling both successful and failed command executions, including
5
+ # return data and error information.
6
+ #
7
+ # Results are created through factory methods rather than direct instantiation, making the
8
+ # intent of the result clear:
9
+ #
10
+ # @example Creating a success result
11
+ # result = Result.success(value: 42)
12
+ # result.successful? #=> true
13
+ # result.value #=> 42
14
+ #
15
+ # @example Creating a failure result
16
+ # result = Result.failure_at_input(
17
+ # { name: "can't be blank" },
18
+ # context: { attempted_name: nil }
19
+ # )
20
+ # result.failure? #=> true
21
+ # result.errors[:name] #=> ["can't be blank"]
22
+ #
23
+ # Results use status codes that align with Unix exit codes, making them suitable for
24
+ # CLI applications:
25
+ # * 0 - Successful execution
26
+ # * 1 - Runtime failure
27
+ # * 2 - Input validation failure
28
+ # * 3 - Output validation failure
29
+ #
30
+ # @example CLI usage
31
+ # def self.run
32
+ # result = MyCommand.call(args)
33
+ # puts result.errors.full_messages if result.failure?
34
+ # exit(result.status_code)
35
+ # end
36
+ #
37
+ # @author {https://aaronmallen.me Aaron Allen}
38
+ # @since 0.1.0
39
+ class Result
40
+ @data: Struct
41
+
42
+ @status_code: Integer
43
+
44
+ @errors: ErrorSet
45
+
46
+ # The structured data returned by the command
47
+ #
48
+ # @return [Struct] A frozen struct containing the command's output data
49
+ attr_reader data: Struct
50
+
51
+ # The errors that occurred during command execution
52
+ #
53
+ # @return [ErrorSet] The set of errors from the command
54
+ attr_reader errors: ErrorSet
55
+
56
+ # The status code indicating the result of the command execution
57
+ #
58
+ # @return [Integer] The status code (0 for success, non-zero for failures)
59
+ attr_reader status_code: Integer
60
+
61
+ # Creates a new failure result with the given status
62
+ #
63
+ # @param errors [Object] The errors that caused the failure
64
+ # @param context [Hash] Optional context data for the failure
65
+ # @param status [Integer] The status code for the failure (defaults to FAILED_AT_RUNTIME)
66
+ #
67
+ # @return [Result] A new failure result
68
+ def self.failure: (untyped errors, ?Hash[String | Symbol, untyped] context, ?status: Integer) -> Result
69
+
70
+ # Creates a new input validation failure result
71
+ #
72
+ # @param errors [Object] The validation errors
73
+ # @param context [Hash] Optional context data for the failure
74
+ #
75
+ # @return [Result] A new input validation failure result
76
+ def self.failure_at_input: (untyped errors, ?Hash[String | Symbol, untyped] context) -> Result
77
+
78
+ # Creates a new output validation failure result
79
+ #
80
+ # @param errors [Object] The validation errors
81
+ # @param context [Hash] Optional context data for the failure
82
+ #
83
+ # @return [Result] A new output validation failure result
84
+ def self.failure_at_output: (untyped errors, ?Hash[String | Symbol, untyped] context) -> Result
85
+
86
+ # Creates a new success result
87
+ #
88
+ # @param context [Hash] The successful result data
89
+ #
90
+ # @return [Result] A new success result
91
+ def self.success: (Hash[String | Symbol, untyped] context) -> Result
92
+
93
+ # Creates a new result instance
94
+ #
95
+ # @param status_code [Integer] The status code for the result
96
+ # @param context [Hash] The data context for the result
97
+ # @param errors [Object, nil] Any errors that occurred
98
+ #
99
+ # @raise [ArgumentError] If status_code is invalid or context is not a Hash
100
+ # @return [void]
101
+ def initialize: (Integer status, ?context: Hash[String | Symbol, untyped], ?errors: untyped) -> void
102
+
103
+ # Indicates whether the command failed
104
+ #
105
+ # @return [Boolean] true if the command failed; false otherwise
106
+ def failure?: () -> bool
107
+
108
+ alias failed? failure?
109
+
110
+ # Indicates whether the command succeeded
111
+ #
112
+ # @return [Boolean] true if the command succeeded; false otherwise
113
+ def successful?: () -> bool
114
+
115
+ alias success? successful?
116
+
117
+ private
118
+
119
+ # Initializes the data struct from the context hash
120
+ #
121
+ # @param context [Hash] The context hash to convert to a struct
122
+ # @raise [ArgumentError] If context is not a Hash
123
+ # @return [void]
124
+ def initialize_data: (untyped context) -> untyped
125
+
126
+ # Validates and initializes the status code
127
+ #
128
+ # @param status_code [Integer] The status code to validate
129
+ # @raise [ArgumentError] If the status code is not valid
130
+ # @return [void]
131
+ def initialize_status_code: (untyped status_code) -> untyped
132
+
133
+ # Delegate method calls to the data struct
134
+ #
135
+ # @param method_name [String, Symbol] The method name to call
136
+ #
137
+ # @return [Object] The result of the method call
138
+ def method_missing: ...
139
+
140
+ # Indicates whether the data struct responds to the given method
141
+ #
142
+ # @param method_name [String, Symbol] The method name to check
143
+ # @param _include_private [Boolean] Whether to include private methods
144
+ #
145
+ # @return [Boolean] `true` if the data struct responds to the method; `false` otherwise
146
+ def respond_to_missing?: (String | Symbol method_name, ?bool include_private) -> bool
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,67 @@
1
+ module Domainic
2
+ # A module that implements the Command pattern, providing a structured way to encapsulate business operations.
3
+ # Commands are single-purpose objects that perform a specific action, validating their inputs and outputs while
4
+ # maintaining a consistent interface for error handling and result reporting.
5
+ #
6
+ # @abstract Including classes must implement an {#execute} method that defines the command's business logic.
7
+ # The {#execute} method has access to validated inputs via the {#context} accessor and should set any output values
8
+ # on the context before returning.
9
+ #
10
+ # @example Basic command definition
11
+ # class CreateUser
12
+ # include Domainic::Command
13
+ #
14
+ # argument :login, String, "The user's login", required: true
15
+ # argument :password, String, "The user's password", required: true
16
+ #
17
+ # output :user, User, "The created user", required: true
18
+ # output :created_at, Time, "When the user was created"
19
+ #
20
+ # def execute
21
+ # user = User.create!(login: context.login, password: context.password)
22
+ # context.user = user
23
+ # context.created_at = Time.current
24
+ # end
25
+ # end
26
+ #
27
+ # @example Using external context classes
28
+ # class CreateUserInput < Domainic::Command::Context::InputContext
29
+ # argument :login, String, "The user's login", required: true
30
+ # argument :password, String, "The user's password", required: true
31
+ # end
32
+ #
33
+ # class CreateUserOutput < Domainic::Command::Context::OutputContext
34
+ # field :user, User, "The created user", required: true
35
+ # field :created_at, Time, "When the user was created"
36
+ # end
37
+ #
38
+ # class CreateUser
39
+ # include Domainic::Command
40
+ #
41
+ # accepts_arguments_matching CreateUserInput
42
+ # returns_output_matching CreateUserOutput
43
+ #
44
+ # def execute
45
+ # user = User.create!(login: context.login, password: context.password)
46
+ # context.user = user
47
+ # context.created_at = Time.current
48
+ # end
49
+ # end
50
+ #
51
+ # @example Command usage
52
+ # # Successful execution
53
+ # result = CreateUser.call(login: "user@example.com", password: "secret123")
54
+ # result.successful? #=> true
55
+ # result.user #=> #<User id: 1, login: "user@example.com">
56
+ #
57
+ # # Failed execution
58
+ # result = CreateUser.call(login: "invalid")
59
+ # result.failure? #=> true
60
+ # result.errors[:password] #=> ["is required"]
61
+ #
62
+ # @author {https://aaronmallen.me Aaron Allen}
63
+ # @since 0.1.0
64
+ module Command
65
+ def self.included: ...
66
+ end
67
+ end
@@ -0,0 +1 @@
1
+
data/sig/manifest.yaml ADDED
@@ -0,0 +1 @@
1
+ dependencies: []
metadata CHANGED
@@ -1,34 +1,70 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: domainic-command
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha.1.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Allen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-21 00:00:00.000000000 Z
11
+ date: 2024-12-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description:
13
+ description: Stop scattering your business logic across controllers and models! Domainic::Command
14
+ brings clarity to your domain operations with type-safe, self-documenting command
15
+ objects that actually tell you what went wrong. From simple CRUD to complex workflows,
16
+ make your business operations work for you, not against you!
14
17
  email:
15
18
  - hello@aaronmallen.me
16
19
  executables: []
17
20
  extensions: []
18
21
  extra_rdoc_files: []
19
22
  files:
23
+ - ".yardopts"
24
+ - CHANGELOG.md
20
25
  - LICENSE
21
26
  - README.md
22
- homepage: https://github.com/domainic/domainic/tree/domainic-command-v0.1.0-alpha.1.0.0/domainic-command
27
+ - lib/domainic-command.rb
28
+ - lib/domainic/command.rb
29
+ - lib/domainic/command/class_methods.rb
30
+ - lib/domainic/command/context/attribute.rb
31
+ - lib/domainic/command/context/attribute_set.rb
32
+ - lib/domainic/command/context/behavior.rb
33
+ - lib/domainic/command/context/input_context.rb
34
+ - lib/domainic/command/context/output_context.rb
35
+ - lib/domainic/command/context/runtime_context.rb
36
+ - lib/domainic/command/errors/error.rb
37
+ - lib/domainic/command/errors/execution_error.rb
38
+ - lib/domainic/command/instance_methods.rb
39
+ - lib/domainic/command/result.rb
40
+ - lib/domainic/command/result/error_set.rb
41
+ - lib/domainic/command/result/status.rb
42
+ - sig/domainic-command.rbs
43
+ - sig/domainic/command.rbs
44
+ - sig/domainic/command/class_methods.rbs
45
+ - sig/domainic/command/context/attribute.rbs
46
+ - sig/domainic/command/context/attribute_set.rbs
47
+ - sig/domainic/command/context/behavior.rbs
48
+ - sig/domainic/command/context/input_context.rbs
49
+ - sig/domainic/command/context/output_context.rbs
50
+ - sig/domainic/command/context/runtime_context.rbs
51
+ - sig/domainic/command/errors/error.rbs
52
+ - sig/domainic/command/errors/execution_error.rbs
53
+ - sig/domainic/command/instance_methods.rbs
54
+ - sig/domainic/command/result.rbs
55
+ - sig/domainic/command/result/error_set.rbs
56
+ - sig/domainic/command/result/status.rbs
57
+ - sig/manifest.yaml
58
+ homepage: https://github.com/domainic/domainic/tree/domainic-command-v0.1.0/domainic-command
23
59
  licenses:
24
60
  - MIT
25
61
  metadata:
26
62
  bug_tracker_uri: https://github.com/domainic/domainic/issues
27
- changelog_uri: https://github.com/domainic/domainic/releases/tag/domainic-command-v0.1.0-alpha.1.0.0
28
- homepage_uri: https://github.com/domainic/domainic/tree/domainic-command-v0.1.0-alpha.1.0.0/domainic-command
63
+ changelog_uri: https://github.com/domainic/domainic/releases/tag/domainic-command-v0.1.0
64
+ documentation_uri: https://rubydoc.info/gems/domainic-command/0.1.0
65
+ homepage_uri: https://github.com/domainic/domainic/tree/domainic-command-v0.1.0/domainic-command
29
66
  rubygems_mfa_required: 'true'
30
- source_code_uri: https://github.com/domainic/domainic/tree/domainic-command-v0.1.0-alpha.1.0.0/domainic-command
31
- wiki_uri: https://github.com/domainic/domainic/wiki
67
+ source_code_uri: https://github.com/domainic/domainic/tree/domainic-command-v0.1.0/domainic-command
32
68
  post_install_message:
33
69
  rdoc_options: []
34
70
  require_paths:
@@ -37,15 +73,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
37
73
  requirements:
38
74
  - - ">="
39
75
  - !ruby/object:Gem::Version
40
- version: 3.1.0
76
+ version: '3.1'
41
77
  required_rubygems_version: !ruby/object:Gem::Requirement
42
78
  requirements:
43
- - - ">"
79
+ - - ">="
44
80
  - !ruby/object:Gem::Version
45
- version: 1.3.1
81
+ version: '0'
46
82
  requirements: []
47
- rubygems_version: 3.3.3
83
+ rubygems_version: 3.3.27
48
84
  signing_key:
49
85
  specification_version: 4
50
- summary: Coming Soon
86
+ summary: A robust implementation of the command pattern in Ruby, offering type-safe,
87
+ composable business operations with standardized error handling.
51
88
  test_files: []