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.
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: []