cattri 0.1.0 → 0.1.2

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: 4558aae18122f29cebf5ddb34fe452ee6b00898d994b4cd8f2b0a825c5a599aa
4
+ data.tar.gz: '092f86968324144a229510426858c9a4cee0245267503a160e27b6087384e88e'
5
5
  SHA512:
6
- metadata.gz: 3c749774916b9921d089554b2e0a2c2c747205d328700f54ee41c6912a40d98656b4d861e7d86523ae3f709e612dde9115ce0357541571efe3361d1d5ef03c49
7
- data.tar.gz: 3a2748150b821d01d8d36bbf44aefbc093751f3ac18f32819c6e731581173abc9423821ad259ad1089c952860e4fd4cc8b759f6312179a62c055423fcf7e4a96
6
+ metadata.gz: 61ac08cebe59853a0a4c163052bdabef6e70db7476bf0d93ff4a1184b706e326fab8583ad6b77533f7c297e5be15cd15cd3414ef8c751443d4f1ed8a06b768a6
7
+ data.tar.gz: dc18a8a83dc56d9a4c2b122c3f824808f8037f0d1cb08a2d71e82ef6aa933d73c75efa083df12cb4b5a7dfb30a0ce22c0558871fe07365d73b601b2e32b537fd
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,57 @@
1
- ## [Unreleased]
1
+ ## [0.1.2] - 2025-04-22
2
+
3
+ ### Added
4
+
5
+ - Support for defining multiple attributes in a single call to `cattr` or `iattr`.
6
+ - Example: `cattr :foo, :bar, default: 1`
7
+ - Shared options apply to all attributes.
8
+ - Adds `cattr_setter` and `iattr_setter` for defining setters on attributes, useful when defining multiple attributes since ambiguous blocks are not allow.
9
+ ```ruby
10
+ class Config
11
+ include Cattri
12
+
13
+ cattr :a, :b # new functionality, does not allow setter blocks.
14
+ # creates writers as def a=(val); @a = val; end
15
+
16
+ cattr_setter :a do |val| # redefines a= as def a=(val); val.to_s.downcase.to_sym; end
17
+ val.to_s.downcase.to_sym
18
+ end
19
+ end
20
+ ```
21
+ - Validation to prevent use of a block when defining multiple attributes.
22
+ - Raises `Cattri::AmbiguousBlockError` if `&block` is passed with more than one attribute.
23
+
24
+ ## [0.1.1] - 2025-04-22
25
+
26
+ ### Added
27
+
28
+ - `Cattri::Context`: new class encapsulating all class-level and instance-level attribute metadata.
29
+ - `Cattri::Attribute`: formal representation of a defined attribute, with support for default values, coercion, and visibility.
30
+ - `Cattri::AttributeDefiner`: internal abstraction for building attributes and assigning behavior based on visibility and type.
31
+ - `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.
32
+
33
+ ### Changed
34
+
35
+ - Internal architecture now uses `Context` to manage attribute storage and duplication logic across inheritance chains.
36
+ - Class and instance attribute definitions (`cattr`, `iattr`) now delegate to `AttributeDefiner`, improving consistency and reducing duplication.
37
+ - 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.
38
+ - Subclass inheritance now copies attribute metadata and current values using a consistent, visibility-aware strategy.
39
+
40
+ ### Removed
41
+
42
+ - Legacy handling of attribute hashes and manual copying in `inherited` hooks.
43
+ - Ad hoc attribute construction logic in `ClassAttributes` and `InstanceAttributes`.
44
+
45
+ ### Improved
46
+
47
+ - Clear separation of concerns between metadata (`Attribute`), context (`Context`), and definition logic (`AttributeDefiner`).
48
+ - More robust error messages and consistent failure behavior when defining attributes with invalid configuration.
49
+
50
+ ---
51
+
52
+ No breaking changes – the public DSL (cattr, iattr) remains identical to v0.1.0.
53
+
54
+ ---
2
55
 
3
56
  ## [0.1.0] - 2025-04-17
4
57
 
data/README.md CHANGED
@@ -1,153 +1,253 @@
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
40
-
41
- cattr :enabled, default: true
42
- iattr :name, default: "anonymous"
42
+ class Config
43
+ include Cattri # exposes `cattr` & `iattr`
44
+
45
+ # -- class‑level ----------------------------------
46
+ cattr :flag_a, :flag_b, default: true
47
+ cattr :enabled, default: true
48
+ cattr :timeout, default: -> { 5.0 }, instance_reader: false
49
+
50
+ # -- instance‑level -------------------------------
51
+ iattr :item_a, :item_b, default: true
52
+ iattr :name, default: "anonymous"
53
+ iattr :age, default: 0 do |val| # coercion block
54
+ Integer(val)
55
+ end
43
56
  end
44
57
 
45
- MyConfig.enabled # => true
46
- MyConfig.new.name # => "anonymous"
58
+ Config.enabled # => true
59
+ Config.enabled = false
60
+ Config.new.age = "42" # => 42
47
61
  ```
48
62
 
49
- ### Class Attributes
50
-
51
- ```ruby
52
- class MyConfig
53
- extend Cattri::ClassAttributes
63
+ ---
54
64
 
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
65
+ ## Defining attributes
61
66
 
62
- MyConfig.format # => :json
63
- MyConfig.format :xml
64
- MyConfig.format # => :xml
67
+ ### Class attributes (`cattr`)
65
68
 
66
- MyConfig.version # => "1.0.0"
69
+ ```ruby
70
+ cattr :log_level, default: :info,
71
+ access: :protected, # respects current visibility by default
72
+ readonly: false,
73
+ instance_reader: true do |value|
74
+ value.to_sym
75
+ end
67
76
  ```
68
77
 
69
- #### Instance Access
78
+ ### Instance attributes (`iattr`)
70
79
 
71
80
  ```ruby
72
- MyConfig.new.format # => :xml
81
+ iattr :token, default: -> { SecureRandom.hex(8) },
82
+ reader: true,
83
+ writer: false # read‑only
73
84
  ```
74
85
 
75
- #### Locking Class Attribute Definitions
86
+ Both forms accept:
87
+
88
+ | Option | Purpose |
89
+ | ------ | ------- |
90
+ | `default:` | Static value or callable (`Proc`) evaluated lazily. |
91
+ | `access:` | Override inferred visibility (`:public`, `:protected`, `:private`). |
92
+ | `reader:` / `writer:` | Disable reader or writer for instance attributes. |
93
+ | `readonly:` | Shorthand for class attributes (`writer` is always present). |
94
+ | `instance_reader:` | Expose class attribute as instance reader (default: **true**). |
95
+
96
+ If you pass a block, it’s treated as a **coercion setter** and receives the incoming value.
97
+
98
+ ---
99
+
100
+ ## Post-definition coercion with `*_setter`
101
+
102
+ If you define multiple attributes at once, you can't provide a coercion block inline:
76
103
 
77
104
  ```ruby
78
- MyConfig.lock_cattrs!
105
+ cattr :foo, :bar, default: nil # ❌ cannot use block here
79
106
  ```
80
107
 
81
- This prevents redefinition of existing class attributes in subclasses.
108
+ Instead, define them first, then apply a coercion later using:
82
109
 
83
- ### Instance Attributes
110
+ - `cattr_setter` for class attributes
111
+ - `iattr_setter` for instance attributes
112
+
113
+ These allow you to attach or override the setter logic after the fact:
84
114
 
85
115
  ```ruby
86
- class Request
87
- include Cattri::InstanceAttributes
116
+ class Config
117
+ include Cattri
88
118
 
89
- iattr :headers, default: -> { {} }
90
- iattr_writer :raw_body do |val|
91
- val.to_s.strip
119
+ cattr :log_level
120
+ cattr_setter :log_level do |val|
121
+ val.to_s.downcase.to_sym
92
122
  end
93
- end
94
123
 
95
- req = Request.new
96
- req.headers["Content-Type"] = "application/json"
97
- req.raw_body = " data "
124
+ iattr_writer :token
125
+ iattr_setter :token do |val|
126
+ val.strip
127
+ end
128
+ end
98
129
  ```
99
130
 
100
- ### Resetting Attributes
131
+ Coercion is only applied when the attribute is written (via `=` or callable form), not when read.
132
+
133
+ Attempting to use `*_setter` on an undefined attribute or one without a writer will raise:
134
+
135
+ - `Cattri::AttributeNotDefinedError` – the attribute doesn't exist or wasn't fully defined
136
+ - `Cattri::AttributeDefinitionError` – the attribute is marked as readonly
137
+
138
+ These APIs ensure your DSL stays consistent and extensible, even when bulk-declaring attributes up front.
139
+
140
+ ---
141
+
142
+ ## Visibility tracking
143
+
144
+ Cattri watches calls to `public`, `protected`, and `private` while you define methods:
101
145
 
102
146
  ```ruby
103
- MyConfig.reset_cattrs! # Reset all class attributes
104
- MyConfig.reset_cattr!(:format)
147
+ class Secrets
148
+ include Cattri
105
149
 
106
- req.reset_iattr!(:headers) # Reset a specific instance attribute
150
+ private
151
+ cattr :api_key
152
+ end
153
+
154
+ Secrets.private_methods.include?(:api_key) # => true
107
155
  ```
108
156
 
109
- ## 🔍 Introspection
157
+ No boilerplate—attributes inherit the visibility that was in effect at the call site.
158
+
159
+ ---
160
+
161
+ ## Safe inheritance
110
162
 
111
- If you include the `Cattri::Introspection` module:
163
+ Subclassing copies both **metadata** and **current values**, using defensive `#dup` where possible and falling back safely when objects are frozen or not duplicable:
112
164
 
113
165
  ```ruby
114
- class MyConfig
166
+ class Base
115
167
  include Cattri
116
- include Cattri::Introspection
117
-
118
- cattr :items, default: []
168
+ cattr :settings, default: {}
119
169
  end
120
170
 
121
- MyConfig.items << :a
122
- MyConfig.snapshot_class_attributes # => { items: [:a] }
171
+ class Child < Base; end
172
+
173
+ Base.settings[:foo] = 1
174
+ Child.settings # => {} (isolated copy)
123
175
  ```
124
176
 
125
- ## 📚 API Overview
177
+ ---
126
178
 
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 |
179
+ ## Introspection helpers
134
180
 
135
- ## 🧪 Testing
181
+ Add `include Cattri::Introspection` (or `extend` for class‑only use) to snapshot live values:
136
182
 
137
- ```bash
138
- bundle exec rspec
183
+ ```ruby
184
+ Config.snapshot_cattrs # => { enabled: false, timeout: 5.0 }
185
+ instance.snapshot_iattrs # => { name: "bob", age: 42 }
186
+ ```
187
+
188
+ Great for debugging or test assertions.
189
+
190
+ ---
191
+
192
+ ## Error handling
193
+
194
+ All errors inherit from `Cattri::Error`, allowing a single rescue for any gem‑specific issue.
195
+
196
+ | Error class | Raised when… |
197
+ |-------------|--------------|
198
+ | `Cattri::AttributeDefinedError` | an attribute is declared twice on the same level |
199
+ | `Cattri::AttributeDefinitionError` | method generation (`define_method`) fails |
200
+ | `Cattri::UnsupportedTypeError` | an internal API receives an unknown type |
201
+ | `Cattri::AttributeError` | generic superclass for attribute‑related issues |
202
+
203
+ Example:
204
+
205
+ ```ruby
206
+ begin
207
+ class Foo
208
+ include Cattri
209
+ cattr :foo
210
+ cattr :foo # duplicate
211
+ end
212
+ rescue Cattri::AttributeDefinedError => e
213
+ warn e.message # => "Class attribute :foo has already been defined"
214
+ rescue Cattri::AttributeError => e
215
+ warn e.message # => Catch-all for any error raised within attributes
216
+ end
139
217
  ```
140
218
 
141
- ## 💡 Why Cattri?
219
+ ---
220
+
221
+ ## Comparison with standard patterns
222
+
223
+ * **Core Ruby macros** (`attr_accessor`, `cattr_accessor`) are simple but global—attributes bleed into subclasses and lack defaults or coercion.
224
+ * **ActiveSupport** extends the API but still relies on mutable class variables and offers no visibility control.
225
+ * **Dry‑configurable** is robust yet heavyweight when you only need a handful of attributes outside a full config object.
142
226
 
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.
227
+ Cattri sits in the sweet spot: **micro‑sized (~300 LOC)**, dependency‑free, and purpose‑built for attribute declaration.
144
228
 
145
- ## 📝 License
229
+ ---
230
+
231
+ ## Testing tips
232
+
233
+ * Use `include Cattri::Introspection` in spec helper files to capture snapshots before/after mutations.
234
+ * Rescue `Cattri::Error` in high‑level test helpers to assert failures without coupling to sub‑class names.
235
+
236
+ ---
237
+
238
+ ## Contributing
146
239
 
147
- MIT © [Nathan Lucas](https://github.com/bnlucas). See [LICENSE](LICENSE).
240
+ 1. Fork the repo
241
+ 2. `bundle install`
242
+ 3. Run the test suite with `bundle exec rake`
243
+ 4. Submit a pull request – ensure new code is covered and **rubocop** passes.
148
244
 
149
245
  ---
150
246
 
247
+ ## License
248
+
249
+ This gem is released under the MIT License – see See [LICENSE](LICENSE) for details.
250
+
151
251
  ## 🙏 Credits
152
252
 
153
253
  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