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.
@@ -1,64 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "attribute_definer"
4
+
3
5
  module Cattri
4
- # Provides a DSL for defining instance-level attributes with support for:
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
- # Example:
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
- # class MyObject
17
- # extend Cattri::InstanceAttributes
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
- # iattr :name, default: "anonymous"
20
- # iattr_writer :age, default: 0 do |v|
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
- include Cattri::Helpers
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
- # Class-level methods for defining instance attributes
27
+ # Defines instance-level attribute DSL methods.
37
28
  module ClassMethods
38
- include Cattri::Helpers
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 proc returning the value
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
- # @yield [value] optional coercion logic for the writer
51
- # @return [void]
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
- name, definition = define_attribute(name, options, block, DEFAULT_OPTIONS)
54
- __cattri_instance_attributes[name] = definition
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
- define_reader(name, definition) if options.fetch(:reader, true)
57
- define_writer(name, definition) if options.fetch(:writer, true)
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
- # @yield [value] coercion logic
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
- def __cattri_instance_attributes
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 whether an instance attribute is defined.
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
- # Fetches the full definition hash for a specific attribute.
93
+ # Returns the full attribute definition for a given name.
99
94
  #
100
95
  # @param name [Symbol, String]
101
- # @return [Hash, nil]
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 {.instance_attribute}
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 {.instance_attribute}
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 {.instance_attribute_reader}
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 {.instance_attribute_writer}
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
- # @return [Hash<Symbol, Hash>] all defined attributes
128
- # @see .instance_attributes
118
+ # Alias for {#instance_attributes}
129
119
  alias iattrs instance_attributes
130
120
 
131
121
  # @!method iattr_defined?(name)
132
- # @return [Boolean]
133
- # @see .instance_attribute_defined?
122
+ # Alias for {#instance_attribute_defined?}
134
123
  alias iattr_defined? instance_attribute_defined?
135
124
 
136
- # @!method iattr_for(name)
137
- # @return [Hash, nil]
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
- # Defines the reader method for an instance attribute.
131
+ # Internal registry of instance attributes defined on the class.
144
132
  #
145
- # @param name [Symbol] attribute name
146
- # @param definition [Hash] full attribute definition
147
- def define_reader(name, definition)
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
- # Defines the writer method for an instance attribute.
138
+ # Returns the context used to define methods for this class.
159
139
  #
160
- # @param name [Symbol] attribute name
161
- # @param definition [Hash] full attribute definition
162
- def define_writer(name, definition)
163
- define_method("#{name}=") do |value|
164
- coerced_value = definition[:setter].call(value)
165
- instance_variable_set(definition[:ivar], coerced_value)
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
@@ -39,7 +39,7 @@ module Cattri
39
39
 
40
40
  class_attributes.each_with_object({}) do |attribute, hash|
41
41
  hash[attribute] = send(attribute)
42
- end
42
+ end.freeze
43
43
  end
44
44
 
45
45
  # @!method snapshot_cattrs
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cattri
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
@@ -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 adds support for both class-level and instance-level
11
- # attribute declarations using `cattr` and `iattr` style methods.
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
- # This module does **not** include introspection helpers by default —
14
- # use `include Cattri::Introspection` explicitly if needed.
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 MyConfig
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
- # MyConfig.enabled # => true
25
- # MyConfig.new.name # => "anonymous"
33
+ # Config.enabled # => true
34
+ # Config.new.name # => "anonymous"
26
35
  module Cattri
27
36
  # Hook triggered when `include Cattri` is called.
28
- # Adds class and instance attribute support to the target.
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.0
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-19 00:00:00.000000000 Z
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: