domainic-attributer 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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE +21 -0
  4. data/README.md +396 -0
  5. data/lib/domainic/attributer/attribute/callback.rb +68 -0
  6. data/lib/domainic/attributer/attribute/coercer.rb +93 -0
  7. data/lib/domainic/attributer/attribute/mixin/belongs_to_attribute.rb +68 -0
  8. data/lib/domainic/attributer/attribute/signature.rb +338 -0
  9. data/lib/domainic/attributer/attribute/validator.rb +128 -0
  10. data/lib/domainic/attributer/attribute.rb +256 -0
  11. data/lib/domainic/attributer/attribute_set.rb +208 -0
  12. data/lib/domainic/attributer/class_methods.rb +247 -0
  13. data/lib/domainic/attributer/dsl/attribute_builder/option_parser.rb +247 -0
  14. data/lib/domainic/attributer/dsl/attribute_builder.rb +233 -0
  15. data/lib/domainic/attributer/dsl/initializer.rb +130 -0
  16. data/lib/domainic/attributer/dsl/method_injector.rb +97 -0
  17. data/lib/domainic/attributer/dsl.rb +5 -0
  18. data/lib/domainic/attributer/instance_methods.rb +65 -0
  19. data/lib/domainic/attributer/undefined.rb +44 -0
  20. data/lib/domainic/attributer.rb +114 -0
  21. data/lib/domainic-attributer.rb +3 -0
  22. data/sig/domainic/attributer/attribute/callback.rbs +48 -0
  23. data/sig/domainic/attributer/attribute/coercer.rbs +59 -0
  24. data/sig/domainic/attributer/attribute/mixin/belongs_to_attribute.rbs +46 -0
  25. data/sig/domainic/attributer/attribute/signature.rbs +223 -0
  26. data/sig/domainic/attributer/attribute/validator.rbs +83 -0
  27. data/sig/domainic/attributer/attribute.rbs +150 -0
  28. data/sig/domainic/attributer/attribute_set.rbs +134 -0
  29. data/sig/domainic/attributer/class_methods.rbs +151 -0
  30. data/sig/domainic/attributer/dsl/attribute_builder/option_parser.rbs +130 -0
  31. data/sig/domainic/attributer/dsl/attribute_builder.rbs +156 -0
  32. data/sig/domainic/attributer/dsl/initializer.rbs +91 -0
  33. data/sig/domainic/attributer/dsl/method_injector.rbs +66 -0
  34. data/sig/domainic/attributer/dsl.rbs +1 -0
  35. data/sig/domainic/attributer/instance_methods.rbs +53 -0
  36. data/sig/domainic/attributer/undefined.rbs +14 -0
  37. data/sig/domainic/attributer.rbs +69 -0
  38. data/sig/domainic-attributer.rbs +1 -0
  39. data/sig/manifest.yaml +2 -0
  40. metadata +89 -0
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/attributer/attribute/callback'
4
+ require 'domainic/attributer/attribute/coercer'
5
+ require 'domainic/attributer/attribute/signature'
6
+ require 'domainic/attributer/attribute/validator'
7
+ require 'domainic/attributer/undefined'
8
+
9
+ module Domainic
10
+ module Attributer
11
+ # A class representing a managed attribute in the Domainic::Attributer system.
12
+ #
13
+ # This class serves as the core component of the attribute management system.
14
+ # It coordinates type information, visibility settings, value coercion,
15
+ # validation, and change notifications for an attribute. Each instance
16
+ # represents a single attribute definition within a class.
17
+ #
18
+ # @author {https://aaronmallen.me Aaron Allen}
19
+ # @since 0.1.0
20
+ class Attribute
21
+ # @rbs!
22
+ # type initialize_options = {
23
+ # ?callbacks: Array[Callback::handler] | Callback::handler,
24
+ # ?coercers: Array[Coercer::handler] | Coercer::handler,
25
+ # ?default: untyped,
26
+ # ?description: String?,
27
+ # name: String | Symbol,
28
+ # ?nilable: bool,
29
+ # ?position: Integer?,
30
+ # ?read: Signature::visibility_symbol,
31
+ # ?required: bool,
32
+ # type: Signature::type_symbol,
33
+ # ?validators: Array[Validator::handler] | Validator::handler,
34
+ # ?write: Signature::visibility_symbol
35
+ # }
36
+
37
+ # @rbs @base: __todo__
38
+ # @rbs @callback: Callback
39
+ # @rbs @coercer: Coercer
40
+ # @rbs @default: untyped
41
+ # @rbs @description: String?
42
+ # @rbs @name: Symbol
43
+ # @rbs @signature: Signature
44
+ # @rbs @validator: Validator
45
+
46
+ # @return [Class, Module] the class or module this attribute belongs to
47
+ attr_reader :base #: __todo__
48
+
49
+ # @return [String, nil] the description of the attribute
50
+ attr_reader :description #: String?
51
+
52
+ # @return [Symbol] the name of the attribute
53
+ attr_reader :name #: Symbol
54
+
55
+ # @return [Signature] the signature configuration for this attribute
56
+ attr_reader :signature #: Signature
57
+
58
+ # Initialize a new Attribute instance.
59
+ #
60
+ # @param base [Class, Module] the class or module this attribute belongs to
61
+ # @param options [Hash] the options to create the attribute with
62
+ # @option options [Array<Proc>, Proc] :callbacks callbacks to trigger on value changes
63
+ # @option options [Array<Proc, Symbol>, Proc, Symbol] :coercers handlers for value coercion
64
+ # @option options [Object] :default the default value or generator
65
+ # @option options [String] :description a description of the attribute
66
+ # @option options [String, Symbol] :name the name of the attribute
67
+ # @option options [Boolean] :nilable (true) whether the attribute can be nil
68
+ # @option options [Integer] :position the position for ordered attributes
69
+ # @option options [Symbol] :read the read visibility
70
+ # @option options [Boolean] :required (false) whether the attribute is required
71
+ # @option options [Symbol] :type the type of attribute
72
+ # @option options [Array<Proc, Object>, Proc, Object] :validators handlers for value validation
73
+ # @option options [Symbol] :write the write visibility
74
+ #
75
+ # @raise [ArgumentError] if the configuration is invalid
76
+ # @return [void]
77
+ #
78
+ # @rbs (
79
+ # __todo__ base,
80
+ # ?callbacks: Array[Callback::handler] | Callback::handler,
81
+ # ?coercers: Array[Coercer::handler] | Coercer::handler,
82
+ # ?default: untyped,
83
+ # ?description: String?,
84
+ # name: String | Symbol,
85
+ # ?nilable: bool,
86
+ # ?position: Integer?,
87
+ # ?read: Signature::visibility_symbol,
88
+ # ?required: bool,
89
+ # type: Signature::type_symbol,
90
+ # ?validators: Array[Validator::handler] | Validator::handler,
91
+ # ?write: Signature::visibility_symbol
92
+ # ) -> void
93
+ def initialize(base, **options)
94
+ options = options.transform_keys(&:to_sym)
95
+ # @type var options: initialize_options
96
+ validate_and_apply_initialize_options!(base, options)
97
+ rescue StandardError => e
98
+ raise ArgumentError, e.message
99
+ end
100
+
101
+ # Apply a value to the attribute on an instance.
102
+ #
103
+ # This method applies all attribute constraints (coercion, validation) to a value
104
+ # and sets it on the given instance. It manages the complete lifecycle of setting
105
+ # an attribute value including:
106
+ # 1. Handling default values
107
+ # 2. Coercing the value
108
+ # 3. Validating the result
109
+ # 4. Setting the value
110
+ # 5. Triggering callbacks
111
+ #
112
+ # @param instance [Object] the instance to set the value on
113
+ # @param value [Object] the value to set
114
+ #
115
+ # @raise [ArgumentError] if the value is invalid
116
+ # @return [void]
117
+ # @rbs (untyped instance, untyped value) -> void
118
+ def apply!(instance, value = Undefined)
119
+ old_value = instance.instance_variable_get(:"@#{name}")
120
+
121
+ coerced_value = value == Undefined ? generate_default(instance) : value
122
+ coerced_value = @coercer.call(instance, coerced_value) unless coerced_value == Undefined
123
+
124
+ @validator.call(instance, coerced_value)
125
+
126
+ instance.instance_variable_set(:"@#{name}", coerced_value == Undefined ? nil : coerced_value)
127
+
128
+ @callback.call(instance, old_value, coerced_value)
129
+ end
130
+
131
+ # Check if this attribute has a default value.
132
+ #
133
+ # @return [Boolean] true if a default value is set
134
+ # @rbs () -> bool
135
+ def default?
136
+ @default != Undefined
137
+ end
138
+
139
+ # Create a duplicate instance for a new base class.
140
+ #
141
+ # @param new_base [Class, Module] the new base class
142
+ #
143
+ # @return [Attribute] the duplicated instance
144
+ # @rbs (__todo__ new_base) -> Attribute
145
+ def dup_with_base(new_base)
146
+ raise ArgumentError, "invalid base: #{new_base}" unless new_base.is_a?(Class) || new_base.is_a?(Module)
147
+
148
+ dup.tap { |duped| duped.instance_variable_set(:@base, new_base) }
149
+ end
150
+
151
+ # Generate the default value for this attribute.
152
+ #
153
+ # @param instance [Object] the instance to generate the default for
154
+ #
155
+ # @return [Object] the generated default value
156
+ # @rbs (untyped instance) -> untyped
157
+ def generate_default(instance)
158
+ @default.is_a?(Proc) ? instance.instance_exec(&@default) : @default
159
+ end
160
+
161
+ # Merge this attribute's configuration with another.
162
+ #
163
+ # @param other [Attribute] the attribute to merge with
164
+ #
165
+ # @raise [ArgumentError] if other is not an Attribute
166
+ # @return [Attribute] a new attribute with merged configuration
167
+ # @rbs (Attribute other) -> Attribute
168
+ def merge(other)
169
+ raise ArgumentError, 'other must be an instance of Attribute' unless other.is_a?(self.class)
170
+
171
+ self.class.new(other.base, **to_options, **other.send(:to_options)) # steep:ignore InsufficientKeywordArguments
172
+ end
173
+
174
+ private
175
+
176
+ # Apply initialization options to create attribute components.
177
+ #
178
+ # @param base [Class, Module] the base class
179
+ # @param options [Hash] the initialization options
180
+ #
181
+ # @return [void]
182
+ # @rbs (__todo__ base, initialize_options options) -> void
183
+ def apply_initialize_options!(base, options)
184
+ @base = base
185
+ @callback = Callback.new(self, options.fetch(:callbacks, []))
186
+ @coercer = Coercer.new(self, options.fetch(:coercers, []))
187
+ @default = options.fetch(:default, Undefined)
188
+ @description = options.fetch(:description, nil)
189
+ @name = options.fetch(:name).to_sym
190
+ @signature = Signature.new(
191
+ self, type: options.fetch(:type), **options.slice(:nilable, :position, :read, :required, :write)
192
+ )
193
+ @validator = Validator.new(self, options.fetch(:validators, []))
194
+ end
195
+
196
+ # Initialize a copy of this attribute.
197
+ #
198
+ # @param source [Attribute] the source attribute
199
+ #
200
+ # @return [Attribute] the initialized copy
201
+ # @rbs override
202
+ def initialize_copy(source)
203
+ @base = source.base
204
+ @callback = source.instance_variable_get(:@callback).dup_with_attribute(self)
205
+ @coercer = source.instance_variable_get(:@coercer).dup_with_attribute(self)
206
+ @default = source.instance_variable_get(:@default)
207
+ @description = source.description
208
+ @name = source.name
209
+ @signature = source.signature.dup_with_attribute(self)
210
+ @validator = source.instance_variable_get(:@validator).dup_with_attribute(self)
211
+ super
212
+ end
213
+
214
+ # Get this attribute's configuration as options.
215
+ #
216
+ # @return [Hash] the configuration options
217
+ # @rbs () -> initialize_options
218
+ def to_options
219
+ {
220
+ callbacks: @callback.instance_variable_get(:@handlers),
221
+ coercers: @coercer.instance_variable_get(:@handlers),
222
+ default: @default,
223
+ description: @description,
224
+ name: @name,
225
+ validators: @validator.instance_variable_get(:@handlers)
226
+ }.merge(signature.send(:to_options)) #: initialize_options
227
+ end
228
+
229
+ # Validate and apply initialization options.
230
+ #
231
+ # @param base [Class, Module] the base class
232
+ # @param options [Hash] the initialization options
233
+ #
234
+ # @return [void]
235
+ # @rbs (__todo__ base, initialize_options options) -> void
236
+ def validate_and_apply_initialize_options!(base, options)
237
+ validate_initialize_options!(base, options)
238
+ apply_initialize_options!(base, options)
239
+ end
240
+
241
+ # Validate initialization options.
242
+ #
243
+ # @param base [Class, Module] the base class
244
+ # @param options [Hash] the initialization options
245
+ #
246
+ # @raise [ArgumentError] if any options are invalid
247
+ # @return [void]
248
+ # @rbs (__todo__ base, initialize_options options) -> void
249
+ def validate_initialize_options!(base, options)
250
+ raise ArgumentError, "invalid base: #{base}" unless base.is_a?(Class) || base.is_a?(Module)
251
+ raise ArgumentError, 'missing keyword :name' unless options.key?(:name)
252
+ raise ArgumentError, 'missing keyword :type' unless options.key?(:type)
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'domainic/attributer/attribute'
4
+ require 'forwardable'
5
+
6
+ module Domainic
7
+ module Attributer
8
+ # A class representing an ordered collection of attributes.
9
+ #
10
+ # This class manages a set of attributes for a given class or module. It maintains
11
+ # attributes in a specific order determined by their type (argument vs option),
12
+ # default values, and position. The collection supports standard operations like
13
+ # adding, selecting, and merging attributes while maintaining proper ownership
14
+ # relationships with their base class.
15
+ #
16
+ # @author {https://aaronmallen.me Aaron Allen}
17
+ # @since 0.1.0
18
+ class AttributeSet
19
+ extend Forwardable
20
+
21
+ # @rbs @base: __todo__
22
+ # @rbs @lookup: Hash[Symbol, Attribute]
23
+
24
+ # Initialize a new AttributeSet.
25
+ #
26
+ # @param base [Class, Module] the class or module this set belongs to
27
+ # @param attributes [Array<Attribute>] initial attributes to add
28
+ #
29
+ # @return [void]
30
+ # @rbs (__todo__ base, ?Array[Attribute] attributes) -> void
31
+ def initialize(base, attributes = [])
32
+ @base = base
33
+ @lookup = {}
34
+ attributes.each { |attribute| add(attribute) }
35
+ end
36
+
37
+ # Get an attribute by name.
38
+ #
39
+ # @param attribute_name [String, Symbol] the name of the attribute
40
+ #
41
+ # @return [Attribute, nil] the attribute if found
42
+ # @rbs (String | Symbol attribute_name) -> Attribute?
43
+ def [](attribute_name)
44
+ @lookup[attribute_name.to_sym]
45
+ end
46
+
47
+ # Add an attribute to the set.
48
+ #
49
+ # If an attribute with the same name exists, the attributes are merged.
50
+ # If the attribute belongs to a different base class, it is duplicated
51
+ # with the correct base. After adding, attributes are sorted by type
52
+ # and position.
53
+ #
54
+ # @param attribute [Attribute] the attribute to add
55
+ #
56
+ # @raise [ArgumentError] if attribute is invalid
57
+ # @return [void]
58
+ # @rbs (Attribute attribute) -> void
59
+ def add(attribute)
60
+ raise ArgumentError, "Invalid attribute: #{attribute.inspect}" unless attribute.is_a?(Attribute)
61
+
62
+ @lookup[attribute.name] = if @lookup.key?(attribute.name)
63
+ @lookup[attribute.name].merge(attribute).dup_with_base(@base)
64
+ elsif attribute.base != @base
65
+ attribute.dup_with_base(@base)
66
+ else
67
+ attribute
68
+ end
69
+
70
+ sort_lookup
71
+ nil
72
+ end
73
+
74
+ # Check if an attribute exists in the set.
75
+ #
76
+ # @param attribute_name [String, Symbol] the name to check
77
+ #
78
+ # @return [Boolean] true if the attribute exists
79
+ def attribute?(attribute_name)
80
+ @lookup.key?(attribute_name.to_sym)
81
+ end
82
+
83
+ # Get all attribute names.
84
+ #
85
+ # @return [Array<Symbol>] the attribute names
86
+ # @rbs () -> Array[Symbol]
87
+ def attribute_names
88
+ @lookup.keys
89
+ end
90
+
91
+ # Get all attributes.
92
+ #
93
+ # @return [Array<Attribute>] the attributes
94
+ # @rbs () -> Array[Attribute]
95
+ def attributes
96
+ @lookup.values
97
+ end
98
+
99
+ # @rbs! def count: () ?{ (Symbol, Attribute) -> boolish } -> Integer
100
+ def_delegators :@lookup, :count
101
+
102
+ # Create a duplicate set for a new base class.
103
+ #
104
+ # @param new_base [Class, Module] the new base class
105
+ #
106
+ # @return [AttributeSet] the duplicated set
107
+ # @rbs (__todo__ base) -> AttributeSet
108
+ def dup_with_base(new_base)
109
+ dup.tap do |duped|
110
+ duped.instance_variable_set(:@base, new_base)
111
+ duped.instance_variable_set(
112
+ :@lookup,
113
+ @lookup.transform_values { |attribute| attribute.dup_with_base(new_base) }
114
+ )
115
+ end
116
+ end
117
+
118
+ # Iterate over attribute name/value pairs.
119
+ #
120
+ # @yield [name, attribute] each name/attribute pair
121
+ # @yieldparam name [Symbol] the attribute name
122
+ # @yieldparam attribute [Attribute] the attribute
123
+ #
124
+ # @return [self]
125
+ # @rbs () { ([Symbol, Attribute]) -> untyped } -> self
126
+ def each(...)
127
+ @lookup.each(...)
128
+ self
129
+ end
130
+ alias each_pair each
131
+
132
+ # @rbs! def empty?: () -> bool
133
+ def_delegators :@lookup, :empty?
134
+
135
+ # Create a new set excluding specified attributes.
136
+ #
137
+ # @param attribute_names [Array<String, Symbol>] names to exclude
138
+ #
139
+ # @return [AttributeSet] new set without specified attributes
140
+ # @rbs (*String | Symbol attribute_names) -> AttributeSet
141
+ def except(*attribute_names)
142
+ self.class.new(@base, @lookup.except(*attribute_names.map(&:to_sym)).values)
143
+ end
144
+
145
+ # @rbs! def length: () -> Integer
146
+ def_delegators :@lookup, :length
147
+
148
+ # Merge another set into this one.
149
+ #
150
+ # @param other [AttributeSet] the set to merge
151
+ #
152
+ # @return [AttributeSet] new set with merged attributes
153
+ # @rbs (AttributeSet other) -> AttributeSet
154
+ def merge(other)
155
+ self.class.new(other.instance_variable_get(:@base), attributes + other.attributes)
156
+ end
157
+
158
+ # Create a new set with rejected attributes.
159
+ #
160
+ # @yield [name, attribute] each name/attribute pair
161
+ # @yieldparam name [Symbol] the attribute name
162
+ # @yieldparam attribute [Attribute] the attribute
163
+ #
164
+ # @return [AttributeSet] new set without rejected attributes
165
+ # @rbs () { (Symbol, Attribute) -> boolish } -> AttributeSet
166
+ def reject(...)
167
+ self.class.new(@base, @lookup.reject(...).values)
168
+ end
169
+
170
+ # Create a new set with selected attributes.
171
+ #
172
+ # @yield [name, attribute] each name/attribute pair
173
+ # @yieldparam name [Symbol] the attribute name
174
+ # @yieldparam attribute [Attribute] the attribute
175
+ #
176
+ # @return [AttributeSet] new set with selected attributes
177
+ # @rbs () { (Symbol, Attribute) -> boolish } -> AttributeSet
178
+ def select(...)
179
+ self.class.new(@base, @lookup.select(...).values)
180
+ end
181
+
182
+ # @rbs! def size: () -> Integer
183
+ def_delegators :@lookup, :size
184
+
185
+ private
186
+
187
+ # Sort attributes by type and position.
188
+ #
189
+ # Attributes are sorted first by type (required arguments, defaulted arguments,
190
+ # then options), and then by their position within those groups.
191
+ #
192
+ # @return [void]
193
+ # @rbs () -> void
194
+ def sort_lookup
195
+ @lookup = @lookup.sort_by do |_, attribute|
196
+ [
197
+ if attribute.signature.option?
198
+ 2
199
+ else
200
+ (attribute.default? ? 1 : 0)
201
+ end,
202
+ attribute.signature.position
203
+ ]
204
+ end.to_h
205
+ end
206
+ end
207
+ end
208
+ end