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 +4 -4
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +31 -1
- data/README.md +141 -85
- data/lib/cattri/attribute.rb +202 -0
- data/lib/cattri/attribute_definer.rb +112 -0
- data/lib/cattri/class_attributes.rb +60 -171
- data/lib/cattri/context.rb +155 -0
- data/lib/cattri/error.rb +67 -0
- data/lib/cattri/instance_attributes.rb +62 -110
- data/lib/cattri/introspection.rb +1 -1
- data/lib/cattri/version.rb +1 -1
- data/lib/cattri/visibility.rb +66 -0
- data/lib/cattri.rb +91 -8
- metadata +6 -5
- data/.idea/workspace.xml +0 -350
- data/.rspec_status +0 -75
- data/lib/cattri/helpers.rb +0 -75
@@ -1,64 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "attribute_definer"
|
4
|
+
|
3
5
|
module Cattri
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# - Default values (static or callable)
|
7
|
-
# - Optional reader/writer method generation
|
8
|
-
# - Custom coercion logic for writers
|
9
|
-
# - Full attribute metadata access and reset capabilities
|
10
|
-
#
|
11
|
-
# This module is designed for mixin into classes or modules that want to offer
|
12
|
-
# configurable instance attributes (e.g., plugin systems, DTOs, DSLs).
|
6
|
+
# Mixin that provides support for defining instance-level attributes.
|
13
7
|
#
|
14
|
-
#
|
8
|
+
# This module is included into a class (via `include Cattri`) and exposes
|
9
|
+
# a DSL similar to `attr_accessor`, with enhancements:
|
15
10
|
#
|
16
|
-
#
|
17
|
-
#
|
11
|
+
# - Lazy or static default values
|
12
|
+
# - Coercion via custom setter blocks
|
13
|
+
# - Visibility control (`:public`, `:protected`, `:private`)
|
14
|
+
# - Read-only or write-only support
|
18
15
|
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# Integer(v)
|
22
|
-
# end
|
23
|
-
# end
|
24
|
-
#
|
25
|
-
# obj = MyObject.new
|
26
|
-
# obj.name # => "anonymous"
|
27
|
-
# obj.age = "42"
|
28
|
-
# obj.instance_variable_get(:@age) # => 42
|
16
|
+
# Each defined attribute is stored as metadata and linked to a reader and/or writer.
|
17
|
+
# Values are accessed and stored via standard instance variables.
|
29
18
|
module InstanceAttributes
|
30
|
-
|
31
|
-
|
19
|
+
# Hook called when this module is included into a class.
|
20
|
+
#
|
21
|
+
# @param base [Class]
|
22
|
+
# @return [void]
|
32
23
|
def self.included(base)
|
33
24
|
base.extend(ClassMethods)
|
34
25
|
end
|
35
26
|
|
36
|
-
#
|
27
|
+
# Defines instance-level attribute DSL methods.
|
37
28
|
module ClassMethods
|
38
|
-
|
39
|
-
|
40
|
-
# Default options for all instance attributes
|
41
|
-
DEFAULT_OPTIONS = { default: nil, reader: true, writer: true }.freeze
|
42
|
-
|
43
|
-
# Defines a new instance-level attribute with optional default and coercion.
|
29
|
+
# Defines an instance-level attribute with optional default and coercion.
|
44
30
|
#
|
45
31
|
# @param name [Symbol, String] the name of the attribute
|
46
32
|
# @param options [Hash] additional options like `:default`, `:reader`, `:writer`
|
47
|
-
# @option options [Object, Proc] :default the default value or
|
33
|
+
# @option options [Object, Proc] :default the default value or lambda
|
48
34
|
# @option options [Boolean] :reader whether to define a reader method (default: true)
|
49
35
|
# @option options [Boolean] :writer whether to define a writer method (default: true)
|
50
|
-
# @
|
51
|
-
# @
|
36
|
+
# @option options [Symbol] :access method visibility (:public, :protected, :private)
|
37
|
+
# @yieldparam value [Object] optional custom coercion logic for the setter
|
38
|
+
# @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
|
39
|
+
# `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
|
40
|
+
# already defined or an error occurs while defining methods)
|
52
41
|
def instance_attribute(name, **options, &block)
|
53
|
-
|
54
|
-
|
42
|
+
options[:access] ||= __cattri_visibility
|
43
|
+
attribute = Cattri::Attribute.new(name, :instance, options, block)
|
44
|
+
|
45
|
+
raise Cattri::AttributeDefinedError, attribute if instance_attribute_defined?(attribute.name)
|
55
46
|
|
56
|
-
|
57
|
-
|
47
|
+
begin
|
48
|
+
__cattri_instance_attributes[name.to_sym] = attribute
|
49
|
+
Cattri::AttributeDefiner.define_accessor(attribute, context)
|
50
|
+
rescue StandardError => e
|
51
|
+
raise Cattri::AttributeDefinitionError.new(self, attribute, e)
|
52
|
+
end
|
58
53
|
end
|
59
54
|
|
60
55
|
# Defines a read-only instance-level attribute.
|
61
56
|
#
|
57
|
+
# Equivalent to `instance_attribute(..., writer: false)`
|
58
|
+
#
|
62
59
|
# @param name [Symbol, String]
|
63
60
|
# @param options [Hash]
|
64
61
|
# @return [void]
|
@@ -68,26 +65,24 @@ module Cattri
|
|
68
65
|
|
69
66
|
# Defines a write-only instance-level attribute.
|
70
67
|
#
|
68
|
+
# Equivalent to `instance_attribute(..., reader: false)`
|
69
|
+
#
|
71
70
|
# @param name [Symbol, String]
|
72
71
|
# @param options [Hash]
|
73
|
-
# @
|
72
|
+
# @yieldparam value [Object] optional coercion logic
|
74
73
|
# @return [void]
|
75
74
|
def instance_attribute_writer(name, **options, &block)
|
76
75
|
instance_attribute(name, reader: false, **options, &block)
|
77
76
|
end
|
78
77
|
|
79
|
-
|
80
|
-
@__cattri_instance_attributes ||= {}
|
81
|
-
end
|
82
|
-
|
83
|
-
# Returns all defined instance-level attribute names.
|
78
|
+
# Returns a list of defined instance-level attribute names.
|
84
79
|
#
|
85
80
|
# @return [Array<Symbol>]
|
86
81
|
def instance_attributes
|
87
82
|
__cattri_instance_attributes.keys
|
88
83
|
end
|
89
84
|
|
90
|
-
# Checks
|
85
|
+
# Checks if an instance-level attribute has been defined.
|
91
86
|
#
|
92
87
|
# @param name [Symbol, String]
|
93
88
|
# @return [Boolean]
|
@@ -95,104 +90,61 @@ module Cattri
|
|
95
90
|
__cattri_instance_attributes.key?(name.to_sym)
|
96
91
|
end
|
97
92
|
|
98
|
-
#
|
93
|
+
# Returns the full attribute definition for a given name.
|
99
94
|
#
|
100
95
|
# @param name [Symbol, String]
|
101
|
-
# @return [
|
96
|
+
# @return [Cattri::Attribute, nil]
|
102
97
|
def instance_attribute_definition(name)
|
103
98
|
__cattri_instance_attributes[name.to_sym]
|
104
99
|
end
|
105
100
|
|
106
101
|
# @!method iattr(name, **options, &block)
|
107
|
-
# Alias for {
|
108
|
-
# @see .instance_attribute
|
102
|
+
# Alias for {#instance_attribute}
|
109
103
|
alias iattr instance_attribute
|
110
104
|
|
111
105
|
# @!method iattr_accessor(name, **options, &block)
|
112
|
-
# Alias for {
|
113
|
-
# @see .instance_attribute
|
106
|
+
# Alias for {#instance_attribute}
|
114
107
|
alias iattr_accessor instance_attribute
|
115
108
|
|
116
109
|
# @!method iattr_reader(name, **options)
|
117
|
-
# Alias for {
|
118
|
-
# @see .instance_attribute_reader
|
110
|
+
# Alias for {#instance_attribute_reader}
|
119
111
|
alias iattr_reader instance_attribute_reader
|
120
112
|
|
121
113
|
# @!method iattr_writer(name, **options, &block)
|
122
|
-
# Alias for {
|
123
|
-
# @see .instance_attribute_writer
|
114
|
+
# Alias for {#instance_attribute_writer}
|
124
115
|
alias iattr_writer instance_attribute_writer
|
125
116
|
|
126
117
|
# @!method iattrs
|
127
|
-
#
|
128
|
-
# @see .instance_attributes
|
118
|
+
# Alias for {#instance_attributes}
|
129
119
|
alias iattrs instance_attributes
|
130
120
|
|
131
121
|
# @!method iattr_defined?(name)
|
132
|
-
#
|
133
|
-
# @see .instance_attribute_defined?
|
122
|
+
# Alias for {#instance_attribute_defined?}
|
134
123
|
alias iattr_defined? instance_attribute_defined?
|
135
124
|
|
136
|
-
# @!method
|
137
|
-
#
|
138
|
-
# @see .instance_attribute_definition
|
125
|
+
# @!method iattr_definition(name)
|
126
|
+
# Alias for {#instance_attribute_definition}
|
139
127
|
alias iattr_definition instance_attribute_definition
|
140
128
|
|
141
129
|
private
|
142
130
|
|
143
|
-
#
|
131
|
+
# Internal registry of instance attributes defined on the class.
|
144
132
|
#
|
145
|
-
# @
|
146
|
-
|
147
|
-
|
148
|
-
ivar = definition[:ivar]
|
149
|
-
|
150
|
-
define_method(name) do
|
151
|
-
return instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
152
|
-
|
153
|
-
value = definition[:default].call
|
154
|
-
instance_variable_set(ivar, value)
|
155
|
-
end
|
133
|
+
# @return [Hash{Symbol => Cattri::Attribute}]
|
134
|
+
def __cattri_instance_attributes
|
135
|
+
@__cattri_instance_attributes ||= {}
|
156
136
|
end
|
157
137
|
|
158
|
-
#
|
138
|
+
# Returns the context used to define methods for this class.
|
159
139
|
#
|
160
|
-
#
|
161
|
-
#
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
end
|
140
|
+
# Used internally to encapsulate method definition and visibility rules.
|
141
|
+
#
|
142
|
+
# @return [Cattri::Context]
|
143
|
+
# :nocov:
|
144
|
+
def context
|
145
|
+
@context ||= Context.new(self)
|
167
146
|
end
|
147
|
+
# :nocov:
|
168
148
|
end
|
169
|
-
|
170
|
-
# Resets all defined attributes to their default values.
|
171
|
-
#
|
172
|
-
# @return [void]
|
173
|
-
def reset_instance_attributes!
|
174
|
-
reset_attributes!(self, self.class.__cattri_instance_attributes.values)
|
175
|
-
end
|
176
|
-
|
177
|
-
# Resets a specific attribute to its default value.
|
178
|
-
#
|
179
|
-
# @param name [Symbol, String]
|
180
|
-
# @return [void]
|
181
|
-
def reset_instance_attribute!(name)
|
182
|
-
definition = self.class.__cattri_instance_attributes[name]
|
183
|
-
return unless definition
|
184
|
-
|
185
|
-
reset_attributes!(self, [definition])
|
186
|
-
end
|
187
|
-
|
188
|
-
# @!method reset_iattrs!
|
189
|
-
# @return [void]
|
190
|
-
# @see .reset_instance_attributes!
|
191
|
-
alias reset_iattrs! reset_instance_attributes!
|
192
|
-
|
193
|
-
# @!method reset_iattr!(name)
|
194
|
-
# @return [void]
|
195
|
-
# @see .reset_instance_attribute!
|
196
|
-
alias reset_iattr! reset_instance_attribute!
|
197
149
|
end
|
198
150
|
end
|
data/lib/cattri/introspection.rb
CHANGED
data/lib/cattri/version.rb
CHANGED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cattri
|
4
|
+
# Cattri::Visibility tracks the current method visibility context (`public`, `protected`, `private`)
|
5
|
+
# when defining methods dynamically. It mimics Ruby's native visibility behavior so that
|
6
|
+
# `cattr` and `iattr` definitions can automatically infer the intended access level
|
7
|
+
# based on the current context in the source file.
|
8
|
+
#
|
9
|
+
# This module is intended to be extended by classes that include or extend Cattri.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# class MyClass
|
13
|
+
# include Cattri
|
14
|
+
#
|
15
|
+
# private
|
16
|
+
# cattr :sensitive_data
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # => :sensitive_data will be defined as a private method
|
20
|
+
module Visibility
|
21
|
+
# Returns the currently active visibility scope on the class or module.
|
22
|
+
#
|
23
|
+
# Defaults to `:public` unless changed explicitly via `public`, `protected`, or `private`.
|
24
|
+
#
|
25
|
+
# @return [Symbol] :public, :protected, or :private
|
26
|
+
def __cattri_visibility
|
27
|
+
@__cattri_visibility ||= :public
|
28
|
+
end
|
29
|
+
|
30
|
+
# Intercepts calls to `public` to update the visibility tracker.
|
31
|
+
#
|
32
|
+
# If no method names are passed, this sets the current visibility scope for future methods.
|
33
|
+
# Otherwise, delegates to Ruby’s native `Module#public`.
|
34
|
+
#
|
35
|
+
# @param args [Array<Symbol>] method names to make public, or empty to set context
|
36
|
+
# @return [void]
|
37
|
+
def public(*args)
|
38
|
+
@__cattri_visibility = :public if args.empty?
|
39
|
+
Module.instance_method(:public).bind(self).call(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Intercepts calls to `protected` to update the visibility tracker.
|
43
|
+
#
|
44
|
+
# If no method names are passed, this sets the current visibility scope for future methods.
|
45
|
+
# Otherwise, delegates to Ruby’s native `Module#protected`.
|
46
|
+
#
|
47
|
+
# @param args [Array<Symbol>] method names to make protected, or empty to set context
|
48
|
+
# @return [void]
|
49
|
+
def protected(*args)
|
50
|
+
@__cattri_visibility = :protected if args.empty?
|
51
|
+
Module.instance_method(:protected).bind(self).call(*args)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Intercepts calls to `private` to update the visibility tracker.
|
55
|
+
#
|
56
|
+
# If no method names are passed, this sets the current visibility scope for future methods.
|
57
|
+
# Otherwise, delegates to Ruby’s native `Module#private`.
|
58
|
+
#
|
59
|
+
# @param args [Array<Symbol>] method names to make private, or empty to set context
|
60
|
+
# @return [void]
|
61
|
+
def private(*args)
|
62
|
+
@__cattri_visibility = :private if args.empty?
|
63
|
+
Module.instance_method(:private).bind(self).call(*args)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/cattri.rb
CHANGED
@@ -1,36 +1,119 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "cattri/version"
|
4
|
+
require_relative "cattri/visibility"
|
4
5
|
require_relative "cattri/class_attributes"
|
5
6
|
require_relative "cattri/instance_attributes"
|
6
7
|
require_relative "cattri/introspection"
|
7
8
|
|
8
9
|
# The primary entry point for the Cattri gem.
|
9
10
|
#
|
10
|
-
# When included, it
|
11
|
-
#
|
11
|
+
# When included, it enables both class-level and instance-level attribute definitions
|
12
|
+
# via `cattr` and `iattr`-style DSLs, providing a lightweight alternative to traditional
|
13
|
+
# `attr_*` and `cattr_*` patterns.
|
12
14
|
#
|
13
|
-
#
|
14
|
-
#
|
15
|
+
# The module includes:
|
16
|
+
# - `Cattri::ClassAttributes` (for class-level configuration)
|
17
|
+
# - `Cattri::InstanceAttributes` (for instance-level configuration)
|
18
|
+
# - `Cattri::Visibility` (for default access control)
|
19
|
+
#
|
20
|
+
# It also installs a custom `.inherited` hook to ensure that subclassed classes
|
21
|
+
# receive deep copies of attribute metadata and current values.
|
22
|
+
#
|
23
|
+
# Note: The `Cattri::Introspection` module must be included manually if needed.
|
15
24
|
#
|
16
25
|
# @example Using both class and instance attributes
|
17
|
-
# class
|
26
|
+
# class Config
|
18
27
|
# include Cattri
|
19
28
|
#
|
20
29
|
# cattr :enabled, default: true
|
21
30
|
# iattr :name, default: "anonymous"
|
22
31
|
# end
|
23
32
|
#
|
24
|
-
#
|
25
|
-
#
|
33
|
+
# Config.enabled # => true
|
34
|
+
# Config.new.name # => "anonymous"
|
26
35
|
module Cattri
|
27
36
|
# Hook triggered when `include Cattri` is called.
|
28
|
-
#
|
37
|
+
#
|
38
|
+
# Installs core attribute DSLs and visibility settings into the host class.
|
39
|
+
# Also injects `.inherited` logic to propagate attribute metadata to subclasses.
|
29
40
|
#
|
30
41
|
# @param base [Class, Module] the receiving class or module
|
31
42
|
# @return [void]
|
32
43
|
def self.included(base)
|
44
|
+
base.extend(Cattri::Visibility)
|
33
45
|
base.extend(Cattri::ClassAttributes)
|
34
46
|
base.include(Cattri::InstanceAttributes)
|
47
|
+
|
48
|
+
base.singleton_class.define_method(:inherited) do |subclass|
|
49
|
+
super(subclass) if defined?(super)
|
50
|
+
|
51
|
+
%i[class instance].each do |type|
|
52
|
+
Cattri.send(:copy_attributes_to, self, subclass, type)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class << self
|
58
|
+
private
|
59
|
+
|
60
|
+
# Copies attribute definitions and backing values from one class to a subclass.
|
61
|
+
#
|
62
|
+
# This is invoked automatically via the `.inherited` hook to ensure
|
63
|
+
# subclass isolation and metadata integrity.
|
64
|
+
#
|
65
|
+
# @param origin [Class] the parent class
|
66
|
+
# @param subclass [Class] the child class inheriting the attributes
|
67
|
+
# @param type [Symbol] either `:class` or `:instance`
|
68
|
+
# @return [void]
|
69
|
+
# @raise [Cattri::AttributeError] if an ivar copy operation fails
|
70
|
+
def copy_attributes_to(origin, subclass, type)
|
71
|
+
ivar = :"@__cattri_#{type}_attributes"
|
72
|
+
attributes = origin.instance_variable_get(ivar) || {}
|
73
|
+
|
74
|
+
subclass_attributes = attributes.transform_values do |attribute|
|
75
|
+
copy_ivar_to(origin, subclass, attribute)
|
76
|
+
attribute.dup
|
77
|
+
end
|
78
|
+
|
79
|
+
subclass.instance_variable_set(ivar, subclass_attributes)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Duplicates the current value of an attribute's backing ivar to the subclass.
|
83
|
+
#
|
84
|
+
# Falls back to raw assignment if duplication is not supported.
|
85
|
+
#
|
86
|
+
# @param origin [Class] the parent class
|
87
|
+
# @param subclass [Class] the receiving subclass
|
88
|
+
# @param attribute [Cattri::Attribute]
|
89
|
+
# @return [void]
|
90
|
+
# @raise [Cattri::AttributeError] if the value cannot be safely duplicated
|
91
|
+
def copy_ivar_to(origin, subclass, attribute)
|
92
|
+
value = duplicate_value(origin, attribute)
|
93
|
+
subclass.instance_variable_set(attribute.ivar, value)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Attempts to duplicate the value of an attribute's backing ivar.
|
97
|
+
#
|
98
|
+
# This method first tries to duplicate the value stored in the ivar.
|
99
|
+
# If duplication is not supported due to the object's nature (e.g., it is frozen or immutable),
|
100
|
+
# it will fall back to returning the original value.
|
101
|
+
#
|
102
|
+
# @param origin [Class] the parent class from which the ivar value is being retrieved
|
103
|
+
# @param attribute [Cattri::Attribute] the attribute for which the ivar value is being duplicated
|
104
|
+
# @return [Object] the duplicated value or the original value if duplication is not possible
|
105
|
+
# @raise [Cattri::AttributeError] if duplication fails due to unsupported object types
|
106
|
+
def duplicate_value(origin, attribute)
|
107
|
+
value = origin.instance_variable_get(attribute.ivar)
|
108
|
+
|
109
|
+
begin
|
110
|
+
value.dup
|
111
|
+
rescue TypeError, FrozenError
|
112
|
+
puts "HERE"
|
113
|
+
value
|
114
|
+
end
|
115
|
+
rescue StandardError => e
|
116
|
+
raise Cattri::AttributeError, "Failed to duplicate value for attribute #{attribute}. Error: #{e.message}"
|
117
|
+
end
|
35
118
|
end
|
36
119
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cattri
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Lucas
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-04-
|
11
|
+
date: 2025-04-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -102,9 +102,7 @@ executables: []
|
|
102
102
|
extensions: []
|
103
103
|
extra_rdoc_files: []
|
104
104
|
files:
|
105
|
-
- ".idea/workspace.xml"
|
106
105
|
- ".rspec"
|
107
|
-
- ".rspec_status"
|
108
106
|
- ".rubocop.yml"
|
109
107
|
- CHANGELOG.md
|
110
108
|
- CODE_OF_CONDUCT.md
|
@@ -113,12 +111,15 @@ files:
|
|
113
111
|
- Rakefile
|
114
112
|
- cattri.gemspec
|
115
113
|
- lib/cattri.rb
|
114
|
+
- lib/cattri/attribute.rb
|
115
|
+
- lib/cattri/attribute_definer.rb
|
116
116
|
- lib/cattri/class_attributes.rb
|
117
|
+
- lib/cattri/context.rb
|
117
118
|
- lib/cattri/error.rb
|
118
|
-
- lib/cattri/helpers.rb
|
119
119
|
- lib/cattri/instance_attributes.rb
|
120
120
|
- lib/cattri/introspection.rb
|
121
121
|
- lib/cattri/version.rb
|
122
|
+
- lib/cattri/visibility.rb
|
122
123
|
- sig/cattri.rbs
|
123
124
|
homepage: https://github.com/bnlucas/cattri
|
124
125
|
licenses:
|