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,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