cattri 0.1.0 → 0.1.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c63162e310f56cece90ee13f07b6edce76503e525ac899094a454248187a0b4
4
- data.tar.gz: b2eca347c4603946fa1b1dc62bd5d36138c4fed87e4909e9650b62005dc89e02
3
+ metadata.gz: e8689b6e0fa1e741fc271fece58183191d290875fe203af5622f069aaf3c4cf7
4
+ data.tar.gz: 2435ba9915a003f39d2d0275c725064c0c5c885940e7de109bb852a7258dfdd6
5
5
  SHA512:
6
- metadata.gz: 3c749774916b9921d089554b2e0a2c2c747205d328700f54ee41c6912a40d98656b4d861e7d86523ae3f709e612dde9115ce0357541571efe3361d1d5ef03c49
7
- data.tar.gz: 3a2748150b821d01d8d36bbf44aefbc093751f3ac18f32819c6e731581173abc9423821ad259ad1089c952860e4fd4cc8b759f6312179a62c055423fcf7e4a96
6
+ metadata.gz: 847f1396957fa8567868273f61c1906e95001e0fd8c632de72ed3f520ec4a7edb1c91c7c1f6a02101913c392d119b24a32a4d835827dab864338ab0e6b668574
7
+ data.tar.gz: f7347d6cb14bb1b0501ff45799ba513388462ede2c95a47f84efb9d4a99ce7c9a6c6a8c726e605b4d475b26664c5f8861f8fc50922da4ebfbe4b139a257af131
data/.rubocop.yml CHANGED
@@ -5,7 +5,11 @@ AllCops:
5
5
 
6
6
  Style/Documentation:
7
7
  Exclude:
8
- - spec/castkit/**/*.rb
8
+ - spec/cattri/**/*.rb
9
+
10
+ Style/DocumentationMethod:
11
+ Exclude:
12
+ - spec/cattri/**/*.rb
9
13
 
10
14
  Style/StringLiterals:
11
15
  Enabled: true
data/CHANGELOG.md CHANGED
@@ -1,4 +1,34 @@
1
- ## [Unreleased]
1
+ ## [0.1.1] - 2025-04-22
2
+
3
+ ### Added
4
+
5
+ - `Cattri::Context`: new class encapsulating all class-level and instance-level attribute metadata.
6
+ - `Cattri::Attribute`: formal representation of a defined attribute, with support for default values, coercion, and visibility.
7
+ - `Cattri::AttributeDefiner`: internal abstraction for building attributes and assigning behavior based on visibility and type.
8
+ - `Cattri::Visibility`: tracks current method visibility (`public`, `protected`, `private`) to ensure dynamically defined methods (e.g., via `cattr`, `iattr`) respect the active access scope during declaration.
9
+
10
+ ### Changed
11
+
12
+ - Internal architecture now uses `Context` to manage attribute storage and duplication logic across inheritance chains.
13
+ - Class and instance attribute definitions (`cattr`, `iattr`) now delegate to `AttributeDefiner`, improving consistency and reducing duplication.
14
+ - Visibility handling is now centralized through `Cattri::Visibility`, which intercepts `public`, `protected`, and `private` to track and apply the current access level when defining methods.
15
+ - Subclass inheritance now copies attribute metadata and current values using a consistent, visibility-aware strategy.
16
+
17
+ ### Removed
18
+
19
+ - Legacy handling of attribute hashes and manual copying in `inherited` hooks.
20
+ - Ad hoc attribute construction logic in `ClassAttributes` and `InstanceAttributes`.
21
+
22
+ ### Improved
23
+
24
+ - Clear separation of concerns between metadata (`Attribute`), context (`Context`), and definition logic (`AttributeDefiner`).
25
+ - More robust error messages and consistent failure behavior when defining attributes with invalid configuration.
26
+
27
+ ---
28
+
29
+ No breaking changes – the public DSL (cattr, iattr) remains identical to v0.1.0.
30
+
31
+ ---
2
32
 
3
33
  ## [0.1.0] - 2025-04-17
4
34
 
data/README.md CHANGED
@@ -1,153 +1,209 @@
1
1
  # Cattri
2
2
 
3
- Cattri is a lightweight Ruby DSL for defining class-level and instance-level attributes with optional defaults, coercion, and reset capabilities.
3
+ A **minimal‑footprint** DSL for defining **classlevel** and **instancelevel** attributes in Ruby, with first‑class support for custom defaults, coercion, visibility tracking, and safety‑first error handling.
4
4
 
5
- It provides fine-grained control over attribute behavior, including:
5
+ ---
6
+
7
+ ## Why another attribute DSL?
6
8
 
7
- - Class-level attributes (`cattr`) with optional instance accessors
8
- - Instance-level attributes (`iattr`) with coercion and lazy defaults
9
- - Optional locking of class attribute definitions to prevent subclass redefinition
10
- - Simple, expressive DSL for reusable metaprogramming
9
+ | Capability | Cattri | `attr_*` / `cattr_*` (Ruby / ActiveSupport) | `dry-configurable` |
10
+ |------------|:------:|:-------------------------------------------:|:------------------:|
11
+ | **Single DSL for class *and* instance attributes** | | ❌ (separate APIs) | ⚠️ (config only) |
12
+ | **Per‑subclass deep copy of attribute metadata & values** | ✅ | ❌ | ❌ |
13
+ | **Built‑in visibility tracking (`public` / `protected` / `private`)** | ✅ | ❌ | ❌ |
14
+ | **Lazy or static *default* values** | ✅ | ⚠️ (writer‑based) | ✅ |
15
+ | **Optional *coercion* via custom setter block** | ✅ | ❌ | ✅ |
16
+ | **Read‑only / write‑only flags** | ✅ | ⚠️ (reader / writer macros) | ❌ |
17
+ | **Introspection helpers (`snapshot_*`)** | ✅ | ❌ | ⚠️ via internals |
18
+ | **Clear, granular error hierarchy** | ✅ | ❌ | ✅ |
19
+ | **Zero runtime dependencies** | ✅ | ⚠️ (ActiveSupport) | ✅ |
11
20
 
12
- ## Features
21
+ > **TL;DR** – If you need lightweight, _Rails‑agnostic_ attribute helpers that play nicely with inheritance and don’t leak state between subclasses, Cattri is for you.
13
22
 
14
- - ✅ Define readable/writable class and instance attributes
15
- - 🧱 Static or callable default values
16
- - 🌀 Optional coercion logic via blocks
17
- - 🧼 Reset attributes to default
18
- - 🔒 Lock class attribute definitions in base class
19
- - 🔍 Introspect attribute definitions and values (optional)
23
+ ---
20
24
 
21
- ## 📦 Installation
25
+ ## Installation
22
26
 
23
27
  ```bash
24
- bundle add cattri
28
+ bundle add cattri # Ruby ≥ 2.7
25
29
  ```
26
30
 
27
- Or add to your Gemfile:
31
+ Or in your Gemfile:
28
32
 
29
33
  ```ruby
30
34
  gem "cattri"
31
35
  ```
32
36
 
33
- ## 🚀 Usage
37
+ ---
34
38
 
35
- ### Class & Instance Attributes
39
+ ## Quick start
36
40
 
37
41
  ```ruby
38
- class MyConfig
39
- include Cattri
42
+ class Config
43
+ include Cattri # exposes `cattr` & `iattr`
44
+
45
+ # -- class‑level ----------------------------------
46
+ cattr :enabled, default: true
47
+ cattr :timeout, default: -> { 5.0 }, instance_reader: false
40
48
 
41
- cattr :enabled, default: true
42
- iattr :name, default: "anonymous"
49
+ # -- instance‑level -------------------------------
50
+ iattr :name, default: "anonymous"
51
+ iattr :age, default: 0 do |val| # coercion block
52
+ Integer(val)
53
+ end
43
54
  end
44
55
 
45
- MyConfig.enabled # => true
46
- MyConfig.new.name # => "anonymous"
56
+ Config.enabled # => true
57
+ Config.enabled = false
58
+ Config.new.age = "42" # => 42
47
59
  ```
48
60
 
49
- ### Class Attributes
50
-
51
- ```ruby
52
- class MyConfig
53
- extend Cattri::ClassAttributes
61
+ ---
54
62
 
55
- cattr :format, default: :json
56
- cattr_reader :version, default: "1.0.0"
57
- cattr :enabled, default: true do |value|
58
- !!value
59
- end
60
- end
63
+ ## Defining attributes
61
64
 
62
- MyConfig.format # => :json
63
- MyConfig.format :xml
64
- MyConfig.format # => :xml
65
+ ### Class attributes (`cattr`)
65
66
 
66
- MyConfig.version # => "1.0.0"
67
+ ```ruby
68
+ cattr :log_level, default: :info,
69
+ access: :protected, # respects current visibility by default
70
+ readonly: false,
71
+ instance_reader: true do |value|
72
+ value.to_sym
73
+ end
67
74
  ```
68
75
 
69
- #### Instance Access
76
+ ### Instance attributes (`iattr`)
70
77
 
71
78
  ```ruby
72
- MyConfig.new.format # => :xml
79
+ iattr :token, default: -> { SecureRandom.hex(8) },
80
+ reader: true,
81
+ writer: false # read‑only
73
82
  ```
74
83
 
75
- #### Locking Class Attribute Definitions
84
+ Both forms accept:
76
85
 
77
- ```ruby
78
- MyConfig.lock_cattrs!
79
- ```
86
+ | Option | Purpose |
87
+ | ------ | ------- |
88
+ | `default:` | Static value or callable (`Proc`) evaluated lazily. |
89
+ | `access:` | Override inferred visibility (`:public`, `:protected`, `:private`). |
90
+ | `reader:` / `writer:` | Disable reader or writer for instance attributes. |
91
+ | `readonly:` | Shorthand for class attributes (`writer` is always present). |
92
+ | `instance_reader:` | Expose class attribute as instance reader (default: **true**). |
93
+
94
+ If you pass a block, it’s treated as a **coercion setter** and receives the incoming value.
80
95
 
81
- This prevents redefinition of existing class attributes in subclasses.
96
+ ---
97
+
98
+ ## Visibility tracking
82
99
 
83
- ### Instance Attributes
100
+ Cattri watches calls to `public`, `protected`, and `private` while you define methods:
84
101
 
85
102
  ```ruby
86
- class Request
87
- include Cattri::InstanceAttributes
103
+ class Secrets
104
+ include Cattri
88
105
 
89
- iattr :headers, default: -> { {} }
90
- iattr_writer :raw_body do |val|
91
- val.to_s.strip
92
- end
106
+ private
107
+ cattr :api_key
93
108
  end
94
109
 
95
- req = Request.new
96
- req.headers["Content-Type"] = "application/json"
97
- req.raw_body = " data "
110
+ Secrets.private_methods.include?(:api_key) # => true
98
111
  ```
99
112
 
100
- ### Resetting Attributes
113
+ No boilerplate—attributes inherit the visibility that was in effect at the call site.
114
+
115
+ ---
116
+
117
+ ## Safe inheritance
118
+
119
+ Subclassing copies both **metadata** and **current values**, using defensive `#dup` where possible and falling back safely when objects are frozen or not duplicable:
101
120
 
102
121
  ```ruby
103
- MyConfig.reset_cattrs! # Reset all class attributes
104
- MyConfig.reset_cattr!(:format)
122
+ class Base
123
+ include Cattri
124
+ cattr :settings, default: {}
125
+ end
126
+
127
+ class Child < Base; end
105
128
 
106
- req.reset_iattr!(:headers) # Reset a specific instance attribute
129
+ Base.settings[:foo] = 1
130
+ Child.settings # => {} (isolated copy)
107
131
  ```
108
132
 
109
- ## 🔍 Introspection
133
+ ---
134
+
135
+ ## Introspection helpers
110
136
 
111
- If you include the `Cattri::Introspection` module:
137
+ Add `include Cattri::Introspection` (or `extend` for class‑only use) to snapshot live values:
112
138
 
113
139
  ```ruby
114
- class MyConfig
115
- include Cattri
116
- include Cattri::Introspection
140
+ Config.snapshot_cattrs # => { enabled: false, timeout: 5.0 }
141
+ instance.snapshot_iattrs # => { name: "bob", age: 42 }
142
+ ```
117
143
 
118
- cattr :items, default: []
119
- end
144
+ Great for debugging or test assertions.
120
145
 
121
- MyConfig.items << :a
122
- MyConfig.snapshot_class_attributes # => { items: [:a] }
123
- ```
146
+ ---
124
147
 
125
- ## 📚 API Overview
148
+ ## Error handling
126
149
 
127
- | Method | Description |
128
- |----------------------------------|--------------------------------------------|
129
- | `cattr`, `cattr_reader` | Define class-level attributes |
130
- | `iattr`, `iattr_reader`, `iattr_writer` | Define instance-level attributes |
131
- | `reset_cattr!`, `reset_iattr!` | Reset specific attributes |
132
- | `cattr_definition(:name)` | Get attribute metadata |
133
- | `lock_cattrs!` | Prevent redefinition in subclasses |
150
+ All errors inherit from `Cattri::Error`, allowing a single rescue for any gem‑specific issue.
134
151
 
135
- ## 🧪 Testing
152
+ | Error class | Raised when… |
153
+ |-------------|--------------|
154
+ | `Cattri::AttributeDefinedError` | an attribute is declared twice on the same level |
155
+ | `Cattri::AttributeDefinitionError` | method generation (`define_method`) fails |
156
+ | `Cattri::UnsupportedTypeError` | an internal API receives an unknown type |
157
+ | `Cattri::AttributeError` | generic superclass for attribute‑related issues |
136
158
 
137
- ```bash
138
- bundle exec rspec
159
+ Example:
160
+
161
+ ```ruby
162
+ begin
163
+ class Foo
164
+ include Cattri
165
+ cattr :foo
166
+ cattr :foo # duplicate
167
+ end
168
+ rescue Cattri::AttributeDefinedError => e
169
+ warn e.message # => "Class attribute :foo has already been defined"
170
+ rescue Cattri::AttributeError => e
171
+ warn e.message # => Catch-all for any error raised within attributes
172
+ end
139
173
  ```
140
174
 
141
- ## 💡 Why Cattri?
175
+ ---
176
+
177
+ ## Comparison with standard patterns
178
+
179
+ * **Core Ruby macros** (`attr_accessor`, `cattr_accessor`) are simple but global—attributes bleed into subclasses and lack defaults or coercion.
180
+ * **ActiveSupport** extends the API but still relies on mutable class variables and offers no visibility control.
181
+ * **Dry‑configurable** is robust yet heavyweight when you only need a handful of attributes outside a full config object.
182
+
183
+ Cattri sits in the sweet spot: **micro‑sized (~200 LOC)**, dependency‑free, and purpose‑built for attribute declaration.
184
+
185
+ ---
186
+
187
+ ## Testing tips
188
+
189
+ * Use `include Cattri::Introspection` in spec helper files to capture snapshots before/after mutations.
190
+ * Rescue `Cattri::Error` in high‑level test helpers to assert failures without coupling to sub‑class names.
142
191
 
143
- Cattri provides a cleaner alternative to `class_attribute`, `attr_accessor`, and configuration gems like `Dry::Configurable` or `ActiveSupport::Configurable`, without monkey-patching or runtime surprises.
192
+ ---
144
193
 
145
- ## 📝 License
194
+ ## Contributing
146
195
 
147
- MIT © [Nathan Lucas](https://github.com/bnlucas). See [LICENSE](LICENSE).
196
+ 1. Fork the repo
197
+ 2. `bundle install`
198
+ 3. Run the test suite with `bundle exec rake`
199
+ 4. Submit a pull request – ensure new code is covered and **rubocop** passes.
148
200
 
149
201
  ---
150
202
 
203
+ ## License
204
+
205
+ This gem is released under the MIT License – see See [LICENSE](LICENSE) for details.
206
+
151
207
  ## 🙏 Credits
152
208
 
153
209
  Created with ❤️ by [Nathan Lucas](https://github.com/bnlucas)
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module Cattri
6
+ # Represents a single attribute definition in Cattri.
7
+ #
8
+ # This class encapsulates metadata and behavior for a declared attribute,
9
+ # including name, visibility, default value, and setter coercion logic.
10
+ #
11
+ # It is used internally by the DSL to configure how accessors are defined,
12
+ # memoized, and resolved at runtime.
13
+ class Attribute
14
+ # Supported attribute scopes within Cattri.
15
+ ATTRIBUTE_TYPES = %i[class instance].freeze
16
+
17
+ # Supported Ruby method visibility levels.
18
+ ACCESS_LEVELS = %i[public protected private].freeze
19
+
20
+ # Ruby value types considered safe to reuse as-is (no `#dup` needed).
21
+ SAFE_VALUE_TYPES = [Numeric, Symbol, TrueClass, FalseClass, NilClass].freeze
22
+
23
+ # Default options for class-level attributes.
24
+ DEFAULT_CLASS_ATTRIBUTE_OPTIONS = {
25
+ readonly: false,
26
+ instance_reader: true
27
+ }.freeze
28
+
29
+ # Default options for instance-level attributes.
30
+ DEFAULT_INSTANCE_ATTRIBUTE_OPTIONS = {
31
+ reader: true,
32
+ writer: true
33
+ }.freeze
34
+
35
+ # @return [Symbol] the attribute name
36
+ attr_reader :name
37
+
38
+ # @return [Symbol] the attribute type (:class or :instance)
39
+ attr_reader :type
40
+
41
+ # @return [Symbol] the associated instance variable (e.g., :@items)
42
+ attr_reader :ivar
43
+
44
+ # @return [Symbol] the access level (:public, :protected, :private)
45
+ attr_reader :access
46
+
47
+ # @return [Proc] the normalized default value block
48
+ attr_reader :default
49
+
50
+ # @return [Proc] the setter function used to assign values
51
+ attr_reader :setter
52
+
53
+ # Initializes a new attribute definition.
54
+ #
55
+ # @param name [String, Symbol] the name of the attribute
56
+ # @param type [Symbol] either :class or :instance
57
+ # @param options [Hash] additional attribute configuration
58
+ # @param block [Proc, nil] optional block for setter coercion
59
+ #
60
+ # @raise [Cattri::UnsupportedTypeError] if an invalid type is provided
61
+ def initialize(name, type, options, block)
62
+ @type = type.to_sym
63
+ raise Cattri::UnsupportedTypeError, type unless ATTRIBUTE_TYPES.include?(@type)
64
+
65
+ @name = name.to_sym
66
+ @ivar = normalize_ivar(options[:ivar])
67
+ @access = options[:access] || :public
68
+ @default = normalize_default(options[:default])
69
+ @setter = normalize_setter(block)
70
+ @options = typed_options(options)
71
+ end
72
+
73
+ # Hash-like access to option values or metadata.
74
+ #
75
+ # @param key [Symbol, String]
76
+ # @return [Object]
77
+ def [](key)
78
+ to_hash[key.to_sym]
79
+ end
80
+
81
+ # Serializes this attribute to a hash, including core properties and type-specific flags.
82
+ #
83
+ # @return [Hash]
84
+ def to_hash
85
+ @to_hash ||= {
86
+ name: @name,
87
+ ivar: @ivar,
88
+ type: @type,
89
+ access: @access,
90
+ default: @default,
91
+ setter: @setter
92
+ }.merge(@options)
93
+ end
94
+
95
+ alias to_h to_hash
96
+
97
+ # @return [Boolean] true if the attribute is class-scoped
98
+ def class_level?
99
+ type == :class
100
+ end
101
+
102
+ # @return [Boolean] true if the attribute is instance-scoped
103
+ def instance_level?
104
+ type == :instance
105
+ end
106
+
107
+ # @return [Boolean] whether the attribute is public
108
+ def public?
109
+ access == :public
110
+ end
111
+
112
+ # @return [Boolean] whether the attribute is protected
113
+ def protected?
114
+ access == :protected
115
+ end
116
+
117
+ # @return [Boolean] whether the attribute is private
118
+ def private?
119
+ access == :private
120
+ end
121
+
122
+ # Invokes the default value logic for the attribute.
123
+ #
124
+ # @return [Object] the default value for the attribute
125
+ # @raise [Cattri::AttributeError] if the default value logic raises an error
126
+ def invoke_default
127
+ default.call
128
+ rescue StandardError => e
129
+ raise Cattri::AttributeError, "Failed to evaluate the default value for :#{name}. Error: #{e.message}"
130
+ end
131
+
132
+ # Invokes the setter function with error handling
133
+ #
134
+ # @param args [Array] the positional arguments
135
+ # @param kwargs [Hash] the keyword arguments
136
+ # @raise [Cattri::AttributeError] if setter raises an error
137
+ # @return [Object] the value returned by the setter
138
+ def invoke_setter(*args, **kwargs)
139
+ setter.call(*args, **kwargs)
140
+ rescue StandardError => e
141
+ raise Cattri::AttributeError, "Failed to evaluate the setter for :#{name}. Error: #{e.message}"
142
+ end
143
+
144
+ private
145
+
146
+ # Applies class- or instance-level defaults and filters valid option keys.
147
+ #
148
+ # @param options [Hash]
149
+ # @return [Hash]
150
+ def typed_options(options)
151
+ defaults = type == :class ? DEFAULT_CLASS_ATTRIBUTE_OPTIONS : DEFAULT_INSTANCE_ATTRIBUTE_OPTIONS
152
+ defaults.merge(options.slice(*defaults.keys))
153
+ end
154
+
155
+ # Normalizes the instance variable name for the attribute.
156
+ #
157
+ # @param ivar [String, Symbol, nil]
158
+ # @return [Symbol]
159
+ def normalize_ivar(ivar)
160
+ ivar ||= name
161
+ :"@#{ivar.to_s.delete_prefix("@")}"
162
+ end
163
+
164
+ # Returns the setter proc. If no block is provided, uses default logic:
165
+ # - Returns kwargs if given
166
+ # - Returns the single positional argument if one
167
+ # - Returns all args as an array otherwise
168
+ #
169
+ # @param block [Proc, nil]
170
+ # @return [Proc]
171
+ def normalize_setter(block)
172
+ block || lambda { |*args, **kwargs|
173
+ return kwargs unless kwargs.empty?
174
+ return args.first if args.length == 1
175
+
176
+ args
177
+ }
178
+ end
179
+
180
+ # Wraps the default value in a memoized lambda.
181
+ #
182
+ # If value is already callable, returns it.
183
+ # If immutable, wraps it in a lambda.
184
+ # If mutable, wraps it in a lambda that calls `#dup`.
185
+ #
186
+ # @param default [Object, Proc, nil]
187
+ # @return [Proc]
188
+ def normalize_default(default)
189
+ return default if default.respond_to?(:call)
190
+ return -> { default } if default.frozen? || SAFE_VALUE_TYPES.any? { |type| default.is_a?(type) }
191
+
192
+ lambda {
193
+ begin
194
+ default.dup
195
+ rescue StandardError => e
196
+ raise Cattri::AttributeError,
197
+ "Failed to duplicate default value for :#{name}. Error: #{e.message}"
198
+ end
199
+ }
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cattri
4
+ # Defines attribute accessors on a given target class using Cattri's Context.
5
+ #
6
+ # This class provides a set of utility methods to generate reader and writer
7
+ # methods dynamically, with support for default values, coercion, and memoization.
8
+ #
9
+ # All accessors are defined through the Cattri::Context abstraction to ensure
10
+ # consistent scoping, visibility, and method tracking.
11
+ class AttributeDefiner
12
+ class << self
13
+ # Defines a callable accessor for class-level attributes.
14
+ #
15
+ # The generated method:
16
+ # - Returns the memoized default value when called with no args or if readonly
17
+ # - Otherwise, calls the attribute’s setter and memoizes the result
18
+ #
19
+ # If the attribute is not readonly, a writer (`foo=`) is also defined.
20
+ #
21
+ # @param attribute [Cattri::Attribute]
22
+ # @param context [Cattri::Context]
23
+ # @return [void]
24
+ # @raise [Cattri::AttributeError] if the setter raises an error
25
+ def define_callable_accessor(attribute, context)
26
+ return unless attribute.class_level?
27
+
28
+ context.define_method(attribute) do |*args, **kwargs|
29
+ readonly = (args.empty? && kwargs.empty?) || attribute[:readonly]
30
+ return AttributeDefiner.send(:memoize_default_value, self, attribute) if readonly
31
+
32
+ value = attribute.invoke_setter(*args, **kwargs)
33
+ instance_variable_set(attribute.ivar, value)
34
+ end
35
+
36
+ define_writer(attribute, context) unless attribute[:readonly]
37
+ end
38
+
39
+ # Defines an instance-level reader for class-level attributes.
40
+ #
41
+ # This method delegates the instance-level call to the class method.
42
+ # It is used when `instance_reader: true` is specified.
43
+ #
44
+ # @param attribute [Cattri::Attribute]
45
+ # @param context [Cattri::Context]
46
+ # @return [void]
47
+ def define_instance_level_reader(attribute, context)
48
+ return unless attribute.class_level?
49
+
50
+ context.target.define_method(attribute.name) do
51
+ self.class.__send__(attribute.name)
52
+ end
53
+
54
+ context.send(:apply_access, attribute.name, attribute)
55
+ end
56
+
57
+ # Defines standard reader and writer methods for instance-level attributes.
58
+ #
59
+ # Skips definition if `reader: false` or `writer: false` is specified.
60
+ #
61
+ # @param attribute [Cattri::Attribute]
62
+ # @param context [Cattri::Context]
63
+ # @return [void]
64
+ def define_accessor(attribute, context)
65
+ define_reader(attribute, context) if attribute[:reader]
66
+ define_writer(attribute, context) if attribute[:writer]
67
+ end
68
+
69
+ # Defines a memoizing reader for the given attribute.
70
+ #
71
+ # This is used for both class and instance attributes, and ensures that
72
+ # the default value is computed only once and stored in the ivar.
73
+ #
74
+ # @param attribute [Cattri::Attribute]
75
+ # @param context [Cattri::Context]
76
+ # @return [void]
77
+ def define_reader(attribute, context)
78
+ context.define_method(attribute) do
79
+ AttributeDefiner.send(:memoize_default_value, self, attribute)
80
+ end
81
+ end
82
+
83
+ # Defines a writer method (`foo=`) that sets and coerces a value via the attribute setter.
84
+ #
85
+ # @param attribute [Cattri::Attribute]
86
+ # @param context [Cattri::Context]
87
+ # @return [void]
88
+ def define_writer(attribute, context)
89
+ context.define_method(attribute, name: :"#{attribute.name}=") do |value|
90
+ coerced_value = attribute.setter.call(value)
91
+ instance_variable_set(attribute.ivar, coerced_value)
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ # Returns the memoized value for an attribute or computes it from the default.
98
+ #
99
+ # This helper ensures lazy initialization while guarding against errors in the default proc.
100
+ #
101
+ # @param receiver [Object]
102
+ # @param attribute [Cattri::Attribute]
103
+ # @return [Object]
104
+ # @raise [Cattri::AttributeError] if the default block raises an error
105
+ def memoize_default_value(receiver, attribute)
106
+ return receiver.instance_variable_get(attribute.ivar) if receiver.instance_variable_defined?(attribute.ivar)
107
+
108
+ receiver.instance_variable_set(attribute.ivar, attribute.invoke_default)
109
+ end
110
+ end
111
+ end
112
+ end