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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4cdb2e918654e0c6287f9aeaf7d65db72fdb12b45d9c70809c78d908069a7748
4
- data.tar.gz: 2b537214957be06288517fd475203cae6d7bf58bda96df38e0b903b3a86d644c
3
+ metadata.gz: 37af16c660cced1f71598b53c4eebd3930683e76cbc5c5c32f715071b976dd58
4
+ data.tar.gz: 89dd55523e0a75bfd2ffacc209b1b3cb968eadadd413dfd4208772794a841919
5
5
  SHA512:
6
- metadata.gz: db9e2b2098d586300d92035a695085df3744300c3a04b0584cdb35c6b86ccfa30f64c22b50a0e6fe1ceae88b1014d75ce940a08dc16691d8a656fc5f39c86c97
7
- data.tar.gz: '08186473bb926e22a1a4fea1f13d15055d87a69e20f4d74c2630d17c48f9a977aac7cd627f4fb5b21982a925251f1a0aa91d2335a02b9dc73949cc884c294d2b'
6
+ metadata.gz: 75144138e2240b1e90c5e16935747c3f8d5073d28cc957227ec661839441953357a95fdf60664ada068af81e6ade28c6dbe88fac2ff86cd0988526473d1a19bb
7
+ data.tar.gz: 7658065d96b7264d047461f69f4d3a4985988e17c663df668e3a91d98327f9460f17d5b39917c0f9abd5e9d06d30ca0d185f02185b514dd98e22c79a2e3010bb
data/.yardopts ADDED
@@ -0,0 +1,11 @@
1
+ --title Domainic::Command
2
+ --readme README.md
3
+ --no-private
4
+ --protected
5
+ --markup markdown
6
+ --markup-provider redcarpet
7
+ --embed-mixins
8
+ --tag rbs
9
+ --hide-tag rbs
10
+ --files CHANGELOG.md,LICENSE
11
+ 'lib/**/*.rb'
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog], and this project adheres to [Break Versioning].
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [v0.1.0] - 2024-12-29
10
+
11
+ * Initial release
12
+
13
+ [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
14
+ [Break Versioning]: https://www.taoensso.com/break-versioning
15
+
16
+ <!-- versions -->
17
+
18
+ [Unreleased]: https://github.com/domainic/domainic/compare/domainic-command-v0.1.0...HEAD
19
+ [v0.1.0]: https://github.com/domainic/domainic/compare/1fdeec3d5d3c6bfe61c2186ba848681c51469e90...domainic-command-v0.1.0
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2024 Aaron Allen
3
+ Copyright (c) Aaron Allen
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,3 +1,94 @@
1
- # Domainic
1
+ # Domainic::Command
2
2
 
3
- Coming Soon
3
+ [![Domainic::Command Version](https://img.shields.io/gem/v/domainic-command?style=for-the-badge&logo=rubygems&logoColor=white&logoSize=auto&label=Gem%20Version)](https://rubygems.org/gems/domainic-command)
4
+ [![Domainic::Command License](https://img.shields.io/github/license/domainic/domainic?style=for-the-badge&logo=opensourceinitiative&logoColor=white&logoSize=auto)](./LICENSE)
5
+ [![Domainic::Command Docs](https://img.shields.io/badge/rubydoc-blue?style=for-the-badge&logo=readthedocs&logoColor=white&logoSize=auto&label=docs)](https://rubydoc.info/gems/domainic-command/0.1.0)
6
+ [![Domainic::Command Open Issues](https://img.shields.io/github/issues-search/domainic/domainic?query=state%3Aopen%20label%3Adomainic-command&style=for-the-badge&logo=github&logoColor=white&logoSize=auto&label=issues&color=red)](https://github.com/domainic/domainic/issues?q=state%3Aopen%20label%3Adomainic-command%20)
7
+
8
+ A robust implementation of the Command pattern for Ruby applications, providing type-safe, self-documenting business
9
+ operations with standardized error handling and composable workflows.
10
+
11
+ Tired of scattered business logic and unclear error handling? Domainic::Command brings clarity to your domain operations
12
+ by:
13
+
14
+ * Enforcing explicit input/output contracts with type validation
15
+ * Providing consistent error handling and status reporting
16
+ * Enabling self-documenting business operations
17
+ * Supporting composable command workflows
18
+ * Maintaining thread safety for concurrent operations
19
+
20
+ ## Quick Start
21
+
22
+ ```ruby
23
+ class CreateUser
24
+ include Domainic::Command
25
+
26
+ # Define expected inputs with validation
27
+ argument :login, String, "The user's login", required: true
28
+ argument :password, String, "The user's password", required: true
29
+
30
+ # Define expected outputs
31
+ output :user, User, "The created user", required: true
32
+ output :created_at, Time, "When the user was created"
33
+
34
+ def execute
35
+ user = User.create!(login: context.login, password: context.password)
36
+ context.user = user
37
+ context.created_at = Time.current
38
+ end
39
+ end
40
+
41
+ # Success case
42
+ result = CreateUser.call(login: "user@example.com", password: "secret123")
43
+ result.successful? # => true
44
+ result.user # => #<User id: 1, login: "user@example.com">
45
+
46
+ # Failure case
47
+ result = CreateUser.call(login: "invalid")
48
+ result.failure? # => true
49
+ result.errors # => { password: ["is required"] }
50
+ ```
51
+
52
+ ## Installation
53
+
54
+ Add this line to your application's Gemfile:
55
+
56
+ ```ruby
57
+ gem 'domainic-command'
58
+ ```
59
+
60
+ Or install it yourself as:
61
+
62
+ ```bash
63
+ gem install domainic-command
64
+ ```
65
+
66
+ ## Key Features
67
+
68
+ * **Type-Safe Arguments**: Define and validate input parameters with clear type constraints
69
+ * **Explicit Outputs**: Specify expected return values and their requirements
70
+ * **Standardized Error Handling**: Consistent error reporting with detailed failure information
71
+ * **Thread Safety**: Built-in thread safety for class definition and execution
72
+ * **Command Composition**: Build complex workflows by combining simpler commands
73
+ * **Self-Documenting**: Generate clear documentation from your command definitions
74
+ * **Framework Agnostic**: Use with any Ruby application (Rails, Sinatra, pure Ruby)
75
+
76
+ ## Documentation
77
+
78
+ For detailed usage instructions and examples, see [USAGE.md](./docs/USAGE.md).
79
+
80
+ ## Contributing
81
+
82
+ We welcome contributions! Please see our
83
+ [Contributing Guidelines](https://github.com/domainic/domainic/wiki/CONTRIBUTING) for:
84
+
85
+ * Development setup and workflow
86
+ * Code style and documentation standards
87
+ * Testing requirements
88
+ * Pull request process
89
+
90
+ Before contributing, please review our [Code of Conduct](https://github.com/domainic/domainic/wiki/CODE_OF_CONDUCT).
91
+
92
+ ## License
93
+
94
+ The gem is available as open source under the terms of the [MIT License](./LICENSE).
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/command/context/input_context'
4
+ require 'domainic/command/context/output_context'
5
+ require 'domainic/command/context/runtime_context'
6
+
7
+ module Domainic
8
+ module Command
9
+ # Class methods that are extended onto any class that includes {Command}. These methods provide
10
+ # the DSL for defining command inputs and outputs, as well as class-level execution methods.
11
+ #
12
+ # @author {https://aaronmallen.me Aaron Allen}
13
+ # @since 0.1.0
14
+ module ClassMethods
15
+ # @rbs @input_context_class: singleton(Context::InputContext)
16
+ # @rbs @output_context_class: singleton(Context::OutputContext)
17
+ # @rbs @runtime_context_class: singleton(Context::RuntimeContext)
18
+
19
+ # Specifies an external input context class for the command
20
+ #
21
+ # @param input_context_class [Class] A subclass of {Context::InputContext}
22
+ #
23
+ # @raise [ArgumentError] If the provided class is not a subclass of {Context::InputContext}
24
+ # @return [void]
25
+ # @rbs (singleton(Context::InputContext) input_context_class) -> void
26
+ def accepts_arguments_matching(input_context_class)
27
+ unless input_context_class < Context::InputContext
28
+ raise ArgumentError, 'Input context class must be a subclass of Context::InputContext'
29
+ end
30
+
31
+ # @type self: Class & ClassMethods
32
+ @input_context_class = begin
33
+ const_set(:InputContext, Class.new(input_context_class))
34
+ const_get(:InputContext)
35
+ end
36
+ end
37
+
38
+ # Defines an input argument for the command
39
+ #
40
+ # @overload argument(name, *type_validator_and_description, **options)
41
+ # @param name [String, Symbol] The name of the argument
42
+ # @param type_validator_and_description [Array<Class, Module, Object, Proc, String, nil>] Type validator or
43
+ # description arguments
44
+ # @param options [Hash] Configuration options for the argument
45
+ # @option options [Object] :default A static default value
46
+ # @option options [Proc] :default_generator A proc that generates the default value
47
+ # @option options [Object] :default_value Alias for :default
48
+ # @option options [String, nil] :desc Short description of the argument
49
+ # @option options [String, nil] :description Full description of the argument
50
+ # @option options [Boolean] :required Whether the argument is required
51
+ # @option options [Class, Module, Object, Proc] :type A type validator
52
+ #
53
+ # @return [void]
54
+ # @rbs (
55
+ # String | Symbol name,
56
+ # *(Class | Module | Object | Proc | String)? type_validator_and_description,
57
+ # ?default: untyped,
58
+ # ?default_generator: untyped,
59
+ # ?default_value: untyped,
60
+ # ?desc: String?,
61
+ # ?description: String?,
62
+ # ?required: bool,
63
+ # ?type: Class | Module | Object | Proc
64
+ # ) -> void
65
+ def argument(...)
66
+ input_context_class.argument(...)
67
+ end
68
+
69
+ # Executes the command with the given context, handling any errors
70
+ #
71
+ # @param context [Hash] The input context for the command
72
+ #
73
+ # @return [Result] The result of the command execution
74
+ # @rbs (**untyped context) -> Result
75
+ def call(**context)
76
+ # @type self: Class & ClassMethods & InstanceMethods
77
+ new.call(**context)
78
+ end
79
+
80
+ # Executes the command with the given context, raising any errors
81
+ #
82
+ # @param context [Hash] The input context for the command
83
+ #
84
+ # @raise [ExecutionError] If the command execution fails
85
+ # @return [Result] The result of the command execution
86
+ # @rbs (**untyped context) -> Result
87
+ def call!(**context)
88
+ # @type self: Class & ClassMethods & InstanceMethods
89
+ new.call!(**context)
90
+ end
91
+
92
+ # Defines an output field for the command
93
+ #
94
+ # @overload output(name, *type_validator_and_description, **options)
95
+ # @param name [String, Symbol] The name of the output field
96
+ # @param type_validator_and_description [Array<Class, Module, Object, Proc, String, nil>] Type validator or
97
+ # description arguments
98
+ # @param options [Hash] Configuration options for the output
99
+ # @option options [Object] :default A static default value
100
+ # @option options [Proc] :default_generator A proc that generates the default value
101
+ # @option options [Object] :default_value Alias for :default
102
+ # @option options [String, nil] :desc Short description of the output
103
+ # @option options [String, nil] :description Full description of the output
104
+ # @option options [Boolean] :required Whether the output is required
105
+ # @option options [Class, Module, Object, Proc] :type A type validator
106
+ #
107
+ # @return [void]
108
+ # @rbs (
109
+ # String | Symbol name,
110
+ # *(Class | Module | Object | Proc | String)? type_validator_and_description,
111
+ # ?default: untyped,
112
+ # ?default_generator: untyped,
113
+ # ?default_value: untyped,
114
+ # ?desc: String?,
115
+ # ?description: String?,
116
+ # ?required: bool,
117
+ # ?type: Class | Module | Object | Proc
118
+ # ) -> void
119
+ def output(...)
120
+ output_context_class.field(...)
121
+ end
122
+
123
+ # Specifies an external output context class for the command
124
+ #
125
+ # @param output_context_class [Class] A subclass of {Context::OutputContext}
126
+ #
127
+ # @raise [ArgumentError] If the provided class is not a subclass of {Context::OutputContext}
128
+ # @return [void]
129
+ # @rbs (singleton(Context::OutputContext) output_context_class) -> void
130
+ def returns_data_matching(output_context_class)
131
+ unless output_context_class < Context::OutputContext
132
+ raise ArgumentError, 'Output context class must be a subclass of Context::OutputContext'
133
+ end
134
+
135
+ # @type self: Class & ClassMethods
136
+ @output_context_class = begin
137
+ const_set(:OutputContext, Class.new(output_context_class))
138
+ const_get(:OutputContext)
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ # Returns the input context class for the command
145
+ #
146
+ # @return [Class] A subclass of {Context::InputContext}
147
+ # @rbs () -> singleton(Context::InputContext)
148
+ def input_context_class
149
+ # @type self: Class & ClassMethods
150
+ @input_context_class ||= begin
151
+ const_set(:InputContext, Class.new(Context::InputContext)) unless const_defined?(:InputContext)
152
+ const_get(:InputContext)
153
+ end
154
+ end
155
+
156
+ # Returns the output context class for the command
157
+ #
158
+ # @return [Class] A subclass of {Context::OutputContext}
159
+ # @rbs () -> singleton(Context::OutputContext)
160
+ def output_context_class
161
+ # @type self: Class & ClassMethods
162
+ @output_context_class ||= begin
163
+ const_set(:OutputContext, Class.new(Context::OutputContext)) unless const_defined?(:OutputContext)
164
+ const_get(:OutputContext)
165
+ end
166
+ end
167
+
168
+ # Returns the runtime context class for the command
169
+ #
170
+ # @return [Class] A subclass of {Context::RuntimeContext}
171
+ # @rbs () -> singleton(Context::RuntimeContext)
172
+ def runtime_context_class
173
+ # @type self: Class & ClassMethods
174
+ @runtime_context_class ||= begin
175
+ const_set(:RuntimeContext, Class.new(Context::RuntimeContext)) unless const_defined?(:RuntimeContext)
176
+ const_get(:RuntimeContext)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domainic
4
+ module Command
5
+ module Context
6
+ # Represents an attribute within a command context. This class manages the lifecycle of an attribute, including
7
+ # its validation, default values, and metadata such as descriptions
8
+ #
9
+ # The `Attribute` class supports a variety of configuration options, such as marking an attribute as required,
10
+ # defining static or dynamic default values, and specifying custom validators. These features ensure that
11
+ # attributes conform to expected rules and provide useful metadata for documentation or runtime behavior
12
+ #
13
+ # @author {https://aaronmallen.me Aaron Allen}
14
+ # @since 0.1.0
15
+ class Attribute
16
+ # Represents an undefined default value for an attribute. This is used internally to distinguish between
17
+ # an attribute with no default and one with a defined default
18
+ #
19
+ # @return [Object] A frozen object representing an undefined default value
20
+ UNDEFINED_DEFAULT = Object.new.freeze #: Object
21
+ private_constant :UNDEFINED_DEFAULT
22
+
23
+ # @rbs @default: untyped
24
+ # @rbs @description: String?
25
+ # @rbs @name: Symbol
26
+ # @rbs @required: bool
27
+ # @rbs @type_validator: Class | Module | Object | Proc
28
+
29
+ # The textual description of the attribute, providing metadata about its purpose or usage
30
+ #
31
+ # @return [String, nil] A description of the attribute
32
+ attr_reader :description #: String?
33
+
34
+ # The name of the attribute, uniquely identifying it within a command context
35
+ #
36
+ # @return [Symbol] The name of the attribute
37
+ attr_reader :name #: Symbol
38
+
39
+ # Create a new attribute instance
40
+ #
41
+ # @overload initialize(
42
+ # name, type_validator_or_description = nil, description_or_type_validator = nil, **options
43
+ # )
44
+ # @param name [String, Symbol] The {#name} of the attribute
45
+ # @param type_validator_or_description [Class, Module, Object, Proc, String, nil] A type validator or the
46
+ # {#description} of the attribute
47
+ # @param description_or_type_validator [Class, Module, Object, Proc, String, nil] The {#description} or a
48
+ # type_validator of the attribute
49
+ # @param options [Hash] Configuration options for the attribute
50
+ # @option options [Object] :default The {#default} of the attribute
51
+ # @option options [Proc] :default_generator An alias for :default
52
+ # @option options [Object] :default_value An alias for :default
53
+ # @option options [String, nil] :desc An alias for :description
54
+ # @option options [String, nil] :description The {#description} of the attribute
55
+ # @option options [Boolean] :required Whether the attribute is {#required?}
56
+ # @option options [Class, Module, Object, Proc] :type A type validator for the attribute value
57
+ #
58
+ # @return [Attribute] the new Attribute instance
59
+ # @rbs (
60
+ # String | Symbol name,
61
+ # *(Class | Module | Object | Proc | String)? type_validator_and_description,
62
+ # ?default: untyped,
63
+ # ?default_generator: untyped,
64
+ # ?default_value: untyped,
65
+ # ?desc: String?,
66
+ # ?description: String?,
67
+ # ?required: bool,
68
+ # ?type: Class | Module | Object | Proc
69
+ # ) -> void
70
+ def initialize(name, *type_validator_and_description, **options)
71
+ symbolized_options = options.transform_keys(&:to_sym)
72
+
73
+ @name = name.to_sym
74
+ @required = symbolized_options[:required] == true
75
+
76
+ initialize_default(symbolized_options)
77
+ initialize_description_and_type_validator(type_validator_and_description, symbolized_options)
78
+ end
79
+
80
+ # Retrieves the default value of the attribute. If a default generator is specified, it evaluates the generator
81
+ # and returns the result
82
+ #
83
+ # @return [Object, nil] The default value or the result of the generator
84
+ # @rbs () -> untyped
85
+ def default
86
+ return unless default?
87
+
88
+ @default.is_a?(Proc) ? @default.call : @default
89
+ end
90
+
91
+ # Determines whether the attribute has a default value defined
92
+ #
93
+ # @return [Boolean] `true` if a default is set; otherwise, `false`
94
+ # @rbs () -> bool
95
+ def default?
96
+ @default != UNDEFINED_DEFAULT
97
+ end
98
+
99
+ # Determines whether the attribute is marked as required
100
+ #
101
+ # @return [Boolean] `true` if the attribute is required; otherwise, `false`
102
+ # @rbs () -> bool
103
+ def required?
104
+ @required
105
+ end
106
+
107
+ # Validates the given value against the attribute's type validator
108
+ #
109
+ # @param value [Object] The value to validate
110
+ #
111
+ # @return [Boolean] `true` if the value is valid; otherwise, `false`
112
+ # @rbs (untyped value) -> bool
113
+ def valid?(value)
114
+ return false if value.nil? && required?
115
+ return true if @type_validator.nil?
116
+
117
+ validator = @type_validator
118
+ return validator.call(value) if validator.is_a?(Proc)
119
+
120
+ validator === value || value.is_a?(validator) # rubocop:disable Style/CaseEquality
121
+ end
122
+
123
+ private
124
+
125
+ # Initializes the {#default} value for the attribute, using the provided options
126
+ #
127
+ # @param options [Hash] Configuration options containing default-related keys
128
+ #
129
+ # @return [void]
130
+ # @rbs (Hash[Symbol, untyped] options) -> void
131
+ def initialize_default(options)
132
+ @default = if %i[default default_generator default_value].any? { |key| options.key?(key) }
133
+ options.values_at(:default, :default_generator, :default_value).compact.first
134
+ else
135
+ UNDEFINED_DEFAULT
136
+ end
137
+ end
138
+
139
+ # Initializes the description and type validator for the attribute based on the given arguments and options
140
+ #
141
+ # @param arguments [Array<Class, Module, Object, Proc, String, nil>] Arguments for validators or description
142
+ # @param options [Hash] Configuration options
143
+ #
144
+ # @return [void]
145
+ # @rbs (Array[(Class | Module | Object | Proc | String)?] arguments, Hash[Symbol, untyped] options) -> void
146
+ def initialize_description_and_type_validator(arguments, options)
147
+ @description = arguments.compact.find { |argument| argument.is_a?(String) } ||
148
+ options[:description] ||
149
+ options[:desc]
150
+
151
+ @type_validator = arguments.compact.find { |argument| !argument.is_a?(String) } ||
152
+ options[:type]
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domainic
4
+ module Command
5
+ module Context
6
+ # A collection class for managing a set of command context attributes. This class provides a simple interface
7
+ # for storing, accessing, and iterating over {Attribute} instances.
8
+ #
9
+ # @example
10
+ # set = AttributeSet.new
11
+ # set.add(Attribute.new(:name))
12
+ # set[:name] # => #<Attribute name=:name>
13
+ #
14
+ # @author {https://aaronmallen.me Aaron Allen}
15
+ # @since 0.1.0
16
+ class AttributeSet
17
+ # @rbs @lookup: Hash[Symbol, Attribute]
18
+
19
+ # Creates a new AttributeSet instance
20
+ #
21
+ # @return [AttributeSet]
22
+ # @rbs () -> void
23
+ def initialize
24
+ @lookup = {}
25
+ end
26
+
27
+ # Retrieves an attribute by name
28
+ #
29
+ # @param attribute_name [String, Symbol] The name of the attribute to retrieve
30
+ #
31
+ # @return [Attribute, nil] The attribute with the given name, or nil if not found
32
+ # @rbs (String | Symbol attribute_name) -> Attribute?
33
+ def [](attribute_name)
34
+ @lookup[attribute_name.to_sym]
35
+ end
36
+
37
+ # Adds an attribute to the set
38
+ #
39
+ # @param attribute [Attribute] The attribute to add
40
+ #
41
+ # @raise [ArgumentError] If the provided attribute is not an {Attribute} instance
42
+ # @return [void]
43
+ # @rbs (Attribute attribute) -> void
44
+ def add(attribute)
45
+ unless attribute.is_a?(Attribute)
46
+ raise ArgumentError, 'Attribute must be an instance of Domainic::Command::Context::Attribute'
47
+ end
48
+
49
+ @lookup[attribute.name] = attribute
50
+ end
51
+
52
+ # Returns all attributes in the set
53
+ #
54
+ # @return [Array<Attribute>] An array of all attributes
55
+ # @rbs () -> Array[Attribute]
56
+ def all
57
+ @lookup.values
58
+ end
59
+
60
+ # Iterates over each attribute in the set
61
+ #
62
+ # @yield [Attribute] Each attribute in the set
63
+ #
64
+ # @return [void]
65
+ # @rbs () { (Attribute) -> untyped } -> void
66
+ def each(...)
67
+ all.each(...)
68
+ end
69
+
70
+ # Iterates over each attribute in the set with an object
71
+ #
72
+ # @overload each_with_object(object)
73
+ # @param object [Object] The object to pass to the block
74
+ # @yield [Attribute, Object] Each attribute and the object
75
+ #
76
+ # @return [Object] The final state of the object
77
+ # @rbs [U] (U object) { (Attribute, U) -> untyped } -> U
78
+ def each_with_object(...)
79
+ all.each_with_object(...) # steep:ignore UnresolvedOverloading
80
+ end
81
+
82
+ private
83
+
84
+ # Ensure that Attributes are duplicated when the AttributeSet is duplicated
85
+ #
86
+ # @param source [AttributeSet] The source AttributeSet to copy
87
+ #
88
+ # @return [AttributeSet]
89
+ def initialize_copy(source)
90
+ @lookup = source.instance_variable_get(:@lookup).transform_values(&:dup)
91
+ super
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end