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,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/command/context/attribute'
4
+ require 'domainic/command/context/attribute_set'
5
+
6
+ module Domainic
7
+ module Command
8
+ module Context
9
+ # A module that provides attribute management for command contexts. When included in a class, it provides
10
+ # a DSL for defining and managing typed attributes with validation, default values, and thread-safe access.
11
+ #
12
+ # ## Thread Safety
13
+ # The attribute system is designed to be thread-safe during class definition and inheritance. A class-level
14
+ # mutex protects the attribute registry during:
15
+ # * Definition of new attributes via the DSL
16
+ # * Inheritance of attributes to subclasses
17
+ #
18
+ # @author {https://aaronmallen.me Aaron Allen}
19
+ # @since 0.1.0
20
+ module Behavior
21
+ # @rbs (Class | Module base) -> void
22
+ def self.included(base)
23
+ super
24
+ base.extend(ClassMethods)
25
+ end
26
+
27
+ # Provides class-level methods for defining and managing attributes. These methods are
28
+ # automatically extended onto any class that includes {Behavior}.
29
+ #
30
+ # @since 0.1.0
31
+ module ClassMethods
32
+ # @rbs @attributes: AttributeSet
33
+ # @rbs @attribute_lock: Mutex
34
+
35
+ private
36
+
37
+ # Defines a new attribute for the context.
38
+ #
39
+ # @overload attribute(name, *type_validator_and_description, **options)
40
+ # @param name [String, Symbol] The name of the attribute
41
+ # @param type_validator_and_description [Array<Class, Module, Object, Proc, String, nil>] Type validator or
42
+ # description arguments
43
+ # @param options [Hash] Configuration options for the attribute
44
+ # @option options [Object] :default A static default value
45
+ # @option options [Proc] :default_generator A proc that generates the default value
46
+ # @option options [Object] :default_value Alias for :default
47
+ # @option options [String, nil] :desc Short description of the attribute
48
+ # @option options [String, nil] :description Full description of the attribute
49
+ # @option options [Boolean] :required Whether the attribute is required
50
+ # @option options [Class, Module, Object, Proc] :type A type validator
51
+ #
52
+ # @return [void]
53
+ # @rbs (
54
+ # String | Symbol name,
55
+ # *(Class | Module | Object | Proc | String)? type_validator_and_description,
56
+ # ?default: untyped,
57
+ # ?default_generator: untyped,
58
+ # ?default_value: untyped,
59
+ # ?desc: String?,
60
+ # ?description: String?,
61
+ # ?required: bool,
62
+ # ?type: Class | Module | Object | Proc
63
+ # ) -> void
64
+ def attribute(...)
65
+ # @type self: Class & Behavior & ClassMethods
66
+ attribute = Attribute.new(...)
67
+ attribute_lock.synchronize { attributes.add(attribute) }
68
+ attr_reader attribute.name
69
+ end
70
+
71
+ # Returns the mutex used to synchronize attribute operations.
72
+ #
73
+ # @return [Mutex]
74
+ # @rbs () -> Mutex
75
+ def attribute_lock
76
+ @attribute_lock ||= Mutex.new
77
+ end
78
+
79
+ # Returns the set of attributes defined for this context.
80
+ #
81
+ # @return [AttributeSet]
82
+ # @rbs () -> AttributeSet
83
+ def attributes
84
+ @attributes ||= AttributeSet.new
85
+ end
86
+
87
+ # Handles inheritance of attributes to subclasses.
88
+ #
89
+ # @param subclass [Class, Module] The inheriting class
90
+ #
91
+ # @return [void]
92
+ # @rbs (Class | Module subclass) -> void
93
+ def inherited(subclass)
94
+ super
95
+ attribute_lock.synchronize do
96
+ subclass.instance_variable_set(:@attributes, attributes.dup)
97
+ end
98
+ end
99
+ end
100
+
101
+ # Initializes a new context instance with the given attributes.
102
+ #
103
+ # @param options [Hash{String, Symbol => Object}] Attribute values for initialization
104
+ #
105
+ # @raise [ArgumentError] If any attribute values are invalid
106
+ # @return [Behavior]
107
+ # @rbs (**untyped options) -> void
108
+ def initialize(**options)
109
+ options = options.transform_keys(&:to_sym)
110
+
111
+ self.class.send(:attributes).each do |attribute|
112
+ value = options.fetch(attribute.name) { attribute.default if attribute.default? }
113
+ raise ArgumentError, "Invalid value for #{attribute.name}: #{value.inspect}" unless attribute.valid?(value)
114
+
115
+ instance_variable_set(:"@#{attribute.name}", value)
116
+ end
117
+ end
118
+
119
+ # Returns a hash of all attribute names and their values.
120
+ #
121
+ # @return [Hash{Symbol => Object}] A hash of attribute values
122
+ # @rbs () -> Hash[Symbol, untyped]
123
+ def to_hash
124
+ self.class.send(:attributes).each_with_object({}) do |attribute, hash|
125
+ hash[attribute.name] = public_send(attribute.name)
126
+ end
127
+ end
128
+ alias to_h to_hash
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/command/context/behavior'
4
+
5
+ module Domainic
6
+ module Command
7
+ module Context
8
+ # A context class for managing command input arguments. This class provides a structured way to define,
9
+ # validate, and access input parameters for commands.
10
+ #
11
+ # @example
12
+ # class MyInputContext < Domainic::Command::Context::InputContext
13
+ # argument :name, String, "The name to process"
14
+ # argument :count, Integer, default: 1
15
+ # end
16
+ #
17
+ # @author {https://aaronmallen.me Aaron Allen}
18
+ # @since 0.1.0
19
+ class InputContext
20
+ # @rbs! extend Behavior::ClassMethods
21
+
22
+ include Behavior
23
+
24
+ # Defines an input argument for the command
25
+ #
26
+ # @overload argument(name, *type_validator_and_description, **options)
27
+ # @param name [String, Symbol] The name of the argument
28
+ # @param type_validator_and_description [Array<Class, Module, Object, Proc, String, nil>] Type validator or
29
+ # description arguments
30
+ # @param options [Hash] Configuration options for the argument
31
+ # @option options [Object] :default A static default value
32
+ # @option options [Proc] :default_generator A proc that generates the default value
33
+ # @option options [Object] :default_value Alias for :default
34
+ # @option options [String, nil] :desc Short description of the argument
35
+ # @option options [String, nil] :description Full description of the argument
36
+ # @option options [Boolean] :required Whether the argument is required
37
+ # @option options [Class, Module, Object, Proc] :type A type validator
38
+ #
39
+ # @return [void]
40
+ # @rbs (
41
+ # String | Symbol name,
42
+ # *(Class | Module | Object | Proc | String)? type_validator_and_description,
43
+ # ?default: untyped,
44
+ # ?default_generator: untyped,
45
+ # ?default_value: untyped,
46
+ # ?desc: String?,
47
+ # ?description: String?,
48
+ # ?required: bool,
49
+ # ?type: Class | Module | Object | Proc
50
+ # ) -> void
51
+ def self.argument(...) = attribute(...)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/command/context/behavior'
4
+
5
+ module Domainic
6
+ module Command
7
+ module Context
8
+ # A context class for managing command output values. This class provides a structured way to define,
9
+ # validate, and access the return values from commands.
10
+ #
11
+ # @example
12
+ # class MyOutputContext < Domainic::Command::Context::OutputContext
13
+ # field :processed_name, String, "The processed name"
14
+ # field :status, Symbol, default: :success
15
+ # end
16
+ #
17
+ # @author {https://aaronmallen.me Aaron Allen}
18
+ # @since 0.1.0
19
+ class OutputContext
20
+ # @rbs! extend Behavior::ClassMethods
21
+
22
+ include Behavior
23
+
24
+ # Defines a return value for the command
25
+ #
26
+ # @overload field(name, *type_validator_and_description, **options)
27
+ # @param name [String, Symbol] The name of the return value
28
+ # @param type_validator_and_description [Array<Class, Module, Object, Proc, String, nil>] Type validator or
29
+ # description arguments
30
+ # @param options [Hash] Configuration options for the return value
31
+ # @option options [Object] :default A static default value
32
+ # @option options [Proc] :default_generator A proc that generates the default value
33
+ # @option options [Object] :default_value Alias for :default
34
+ # @option options [String, nil] :desc Short description of the return value
35
+ # @option options [String, nil] :description Full description of the return value
36
+ # @option options [Boolean] :required Whether the return value is required
37
+ # @option options [Class, Module, Object, Proc] :type A type validator
38
+ #
39
+ # @return [void]
40
+ # @rbs (
41
+ # String | Symbol name,
42
+ # *(Class | Module | Object | Proc | String)? type_validator_and_description,
43
+ # ?default: untyped,
44
+ # ?default_generator: untyped,
45
+ # ?default_value: untyped,
46
+ # ?desc: String?,
47
+ # ?description: String?,
48
+ # ?required: bool,
49
+ # ?type: Class | Module | Object | Proc
50
+ # ) -> void
51
+ def self.field(...) = attribute(...)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domainic
4
+ module Command
5
+ module Context
6
+ # A flexible context class for managing command state during execution. This class provides a dynamic
7
+ # storage mechanism for command data, allowing both hash-style and method-style access to values.
8
+ #
9
+ # The RuntimeContext serves as a mutable workspace during command execution, bridging the gap between
10
+ # input parameters and output values. It automatically handles type coercion of keys to symbols and
11
+ # provides safe value duplication when converting to a hash.
12
+ #
13
+ # @example Hash-style access
14
+ # context = RuntimeContext.new(count: 1)
15
+ # context[:count] #=> 1
16
+ # context[:count] = 2
17
+ # context[:count] #=> 2
18
+ #
19
+ # @example Method-style access
20
+ # context = RuntimeContext.new(name: "test")
21
+ # context.name #=> "test"
22
+ # context.name = "new test"
23
+ # context.name #=> "new test"
24
+ #
25
+ # @author {https://aaronmallen.me Aaron Allen}
26
+ # @since 0.1.0
27
+ class RuntimeContext
28
+ # @rbs @data: Hash[Symbol, untyped]
29
+
30
+ # Creates a new RuntimeContext with the given options
31
+ #
32
+ # @param options [Hash] Initial values for the context
33
+ #
34
+ # @return [RuntimeContext]
35
+ # @rbs (**untyped options) -> void
36
+ def initialize(**options)
37
+ @data = options.transform_keys(&:to_sym)
38
+ end
39
+
40
+ # Retrieves a value by its attribute name
41
+ #
42
+ # @param attribute_name [String, Symbol] The name of the attribute to retrieve
43
+ #
44
+ # @return [Object, nil] The value associated with the attribute name
45
+ # @rbs (String | Symbol attribute_name) -> untyped
46
+ def [](attribute_name)
47
+ read_from_attribute(attribute_name)
48
+ end
49
+
50
+ # Sets a value for the given attribute name
51
+ #
52
+ # @param attribute_name [String, Symbol] The name of the attribute to set
53
+ # @param value [Object] The value to store
54
+ #
55
+ # @return [Object] The stored value
56
+ # @rbs (String | Symbol attribute_name, untyped value) -> untyped
57
+ def []=(attribute_name, value)
58
+ write_to_attribute(attribute_name, value)
59
+ end
60
+
61
+ # Converts the context to a hash, duplicating values where appropriate
62
+ #
63
+ # @note Class and Module values are not duplicated to prevent potential issues
64
+ #
65
+ # @return [Hash{Symbol => Object}] A hash containing all stored values
66
+ # @rbs () -> Hash[Symbol, untyped]
67
+ def to_hash
68
+ @data.transform_values do |value|
69
+ value.is_a?(Class) || value.is_a?(Module) ? value : value.dup
70
+ end
71
+ end
72
+ alias to_h to_hash
73
+
74
+ private
75
+
76
+ # Handles dynamic method calls for reading and writing attributes
77
+ #
78
+ # @return [Object, nil]
79
+ # @rbs override
80
+ def method_missing(method_name, ...)
81
+ return super unless respond_to_missing?(method_name)
82
+
83
+ if method_name.to_s.end_with?('=')
84
+ write_to_attribute(method_name.to_s.delete_suffix('=').to_sym, ...)
85
+ else
86
+ @data[method_name]
87
+ end
88
+ end
89
+
90
+ # Reads a value from the internal storage
91
+ #
92
+ # @param attribute_name [String, Symbol] The name of the attribute to read
93
+ #
94
+ # @return [Object, nil] The stored value
95
+ # @rbs (String | Symbol attribute_name) -> untyped
96
+ def read_from_attribute(attribute_name)
97
+ @data[attribute_name.to_sym]
98
+ end
99
+
100
+ # Determines if a method name can be handled dynamically
101
+ #
102
+ # @param method_name [Symbol] The name of the method to check
103
+ # @param _include_private [Boolean] Whether to include private methods
104
+ #
105
+ # @return [Boolean] Whether the method can be handled
106
+ # @rbs (String | Symbol method_name, ?bool _include_private) -> bool
107
+ def respond_to_missing?(method_name, _include_private = false)
108
+ return true if method_name.to_s.end_with?('=')
109
+ return true if @data.key?(method_name.to_sym)
110
+
111
+ super
112
+ end
113
+
114
+ # Writes a value to the internal storage
115
+ #
116
+ # @param attribute_name [String, Symbol] The name of the attribute to write
117
+ # @param value [Object] The value to store
118
+ # @return [Object] The stored value
119
+ # @rbs (String | Symbol attribute_name, untyped value) -> untyped
120
+ def write_to_attribute(attribute_name, value)
121
+ @data[attribute_name.to_sym] = value
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domainic
4
+ module Command
5
+ # Base error class for command-related errors. This class serves as the root of the command error
6
+ # hierarchy, allowing for specific error handling of command-related issues.
7
+ #
8
+ # @note This is an abstract class and should not be instantiated directly. Instead, use one of its
9
+ # subclasses for specific error cases.
10
+ #
11
+ # @example Rescuing command errors
12
+ # begin
13
+ # # Command execution code
14
+ # rescue Domainic::Command::Error => e
15
+ # # Handle any command-related error
16
+ # end
17
+ #
18
+ # @author {https://aaronmallen.me Aaron Allen}
19
+ # @since 0.1.0
20
+ class Error < StandardError
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/command/errors/error'
4
+
5
+ module Domainic
6
+ module Command
7
+ # Error class raised when a command encounters an execution failure. This class provides access to
8
+ # both the error message and the {Result} object containing detailed information about the failure.
9
+ #
10
+ # @example Handling execution errors
11
+ # begin
12
+ # command.call!
13
+ # rescue Domainic::Command::ExecutionError => e
14
+ # puts e.message # Access the error message
15
+ # puts e.result.errors # Access the detailed errors
16
+ # puts e.result.status_code # Access the status code
17
+ # end
18
+ #
19
+ # @author {https://aaronmallen.me Aaron Allen}
20
+ # @since 0.1.0
21
+ class ExecutionError < Error
22
+ # The {Result} object containing detailed information about the execution failure
23
+ #
24
+ # @return [Result] The result object associated with the failure
25
+ attr_reader :result #: Result
26
+
27
+ # Creates a new execution error with the given message and result
28
+ #
29
+ # @param message [String] The error message describing what went wrong
30
+ # @param result [Result] The result object containing detailed failure information
31
+ #
32
+ # @return [void]
33
+ # @rbs (String message, Result result) -> void
34
+ def initialize(message, result)
35
+ @result = result
36
+ super(message)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/command/errors/execution_error'
4
+ require 'domainic/command/result'
5
+
6
+ module Domainic
7
+ module Command
8
+ # Instance methods that are included in any class that includes {Command}. These methods provide
9
+ # the core execution logic and error handling for commands.
10
+ #
11
+ # @author {https://aaronmallen.me Aaron Allen}
12
+ # @since 0.1.0
13
+ module InstanceMethods
14
+ # Executes the command with the given context, handling any errors
15
+ #
16
+ # @param context [Hash] The input context for the command
17
+ #
18
+ # @return [Result] The result of the command execution
19
+ # @rbs (**untyped context) -> Result
20
+ def call(**context)
21
+ call!(**context)
22
+ rescue ExecutionError => e
23
+ e.result
24
+ end
25
+
26
+ # Executes the command with the given context, raising any errors
27
+ #
28
+ # @param input [Hash] The input context for the command
29
+ #
30
+ # @raise [ExecutionError] If the command execution fails
31
+ # @return [Result] The result of the command execution
32
+ # @rbs (**untyped context) -> Result
33
+ def call!(**input)
34
+ __execute_command!(input)
35
+ rescue StandardError => e
36
+ raise e if e.is_a?(ExecutionError)
37
+
38
+ raise ExecutionError.new("#{self.class} failed", Result.failure(e, context.to_h))
39
+ end
40
+
41
+ # Executes the command's business logic
42
+ #
43
+ # @abstract Subclass and override {#execute} to implement command behavior
44
+ #
45
+ # @raise [NotImplementedError] If the subclass does not implement {#execute}
46
+ # @return [void]
47
+ # @rbs () -> void
48
+ def execute
49
+ raise NotImplementedError, "#{self.class} does not implement #execute"
50
+ end
51
+
52
+ private
53
+
54
+ # The runtime context for the command execution
55
+ #
56
+ # @return [Context::RuntimeContext] The runtime context
57
+ attr_reader :context #: Context::RuntimeContext
58
+
59
+ # Execute the command with the given input context
60
+ #
61
+ # @param input [Hash] The input context for the command
62
+ #
63
+ # @return [Result] The result of the command execution
64
+ # @rbs (Hash[String | Symbol, untyped] input) -> Result
65
+ def __execute_command!(input)
66
+ input_context = __validate_context!(:input, input)
67
+ @context = self.class.send(:runtime_context_class).new(**input_context.to_h)
68
+ execute
69
+ output_context = __validate_context!(:output, context.to_h)
70
+ Result.success(output_context.to_h)
71
+ end
72
+
73
+ # Validates an input or output context
74
+ #
75
+ # @param context_type [Symbol] The type of context to validate
76
+ # @param context [Hash] The context data to validate
77
+ #
78
+ # @raise [ExecutionError] If the context is invalid
79
+ # @return [Context::InputContext, Context::OutputContext] The validated context
80
+ # @rbs (
81
+ # :input | :output context_type,
82
+ # Hash[String | Symbol, untyped] context
83
+ # ) -> (Context::InputContext | Context::OutputContext)
84
+ def __validate_context!(context_type, context)
85
+ self.class.send(:"#{context_type}_context_class").new(**context)
86
+ rescue StandardError => e
87
+ result = context_type == :input ? Result.failure_at_input(e) : Result.failure_at_output(e)
88
+ raise ExecutionError.new("#{self.class} has invalid #{context_type}", result)
89
+ end
90
+ end
91
+ end
92
+ end