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