cattri 0.1.3 → 0.2.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +34 -0
  3. data/.gitignore +72 -0
  4. data/.rubocop.yml +6 -3
  5. data/CHANGELOG.md +41 -0
  6. data/Gemfile +12 -0
  7. data/README.md +163 -151
  8. data/Steepfile +6 -0
  9. data/bin/console +33 -0
  10. data/bin/setup +8 -0
  11. data/cattri.gemspec +5 -5
  12. data/lib/cattri/attribute.rb +119 -155
  13. data/lib/cattri/attribute_compiler.rb +104 -0
  14. data/lib/cattri/attribute_options.rb +183 -0
  15. data/lib/cattri/attribute_registry.rb +155 -0
  16. data/lib/cattri/context.rb +124 -106
  17. data/lib/cattri/context_registry.rb +36 -0
  18. data/lib/cattri/deferred_attributes.rb +73 -0
  19. data/lib/cattri/dsl.rb +54 -0
  20. data/lib/cattri/error.rb +17 -90
  21. data/lib/cattri/inheritance.rb +35 -0
  22. data/lib/cattri/initializer_patch.rb +37 -0
  23. data/lib/cattri/internal_store.rb +104 -0
  24. data/lib/cattri/introspection.rb +56 -49
  25. data/lib/cattri/version.rb +3 -1
  26. data/lib/cattri.rb +38 -99
  27. data/sig/lib/cattri/attribute.rbs +105 -0
  28. data/sig/lib/cattri/attribute_compiler.rbs +61 -0
  29. data/sig/lib/cattri/attribute_options.rbs +150 -0
  30. data/sig/lib/cattri/attribute_registry.rbs +95 -0
  31. data/sig/lib/cattri/context.rbs +130 -0
  32. data/sig/lib/cattri/context_registry.rbs +31 -0
  33. data/sig/lib/cattri/deferred_attributes.rbs +53 -0
  34. data/sig/lib/cattri/dsl.rbs +55 -0
  35. data/sig/lib/cattri/error.rbs +28 -0
  36. data/sig/lib/cattri/inheritance.rbs +21 -0
  37. data/sig/lib/cattri/initializer_patch.rbs +26 -0
  38. data/sig/lib/cattri/internal_store.rbs +75 -0
  39. data/sig/lib/cattri/introspection.rbs +61 -0
  40. data/sig/lib/cattri/types.rbs +19 -0
  41. data/sig/lib/cattri/visibility.rbs +55 -0
  42. data/sig/lib/cattri.rbs +37 -0
  43. data/spec/cattri/attribute_compiler_spec.rb +179 -0
  44. data/spec/cattri/attribute_options_spec.rb +267 -0
  45. data/spec/cattri/attribute_registry_spec.rb +257 -0
  46. data/spec/cattri/attribute_spec.rb +297 -0
  47. data/spec/cattri/context_registry_spec.rb +45 -0
  48. data/spec/cattri/context_spec.rb +346 -0
  49. data/spec/cattri/deferred_attrributes_spec.rb +117 -0
  50. data/spec/cattri/dsl_spec.rb +69 -0
  51. data/spec/cattri/error_spec.rb +37 -0
  52. data/spec/cattri/inheritance_spec.rb +60 -0
  53. data/spec/cattri/initializer_patch_spec.rb +35 -0
  54. data/spec/cattri/internal_store_spec.rb +139 -0
  55. data/spec/cattri/introspection_spec.rb +90 -0
  56. data/spec/cattri/visibility_spec.rb +68 -0
  57. data/spec/cattri_spec.rb +54 -0
  58. data/spec/simplecov_helper.rb +21 -0
  59. data/spec/spec_helper.rb +16 -0
  60. metadata +79 -6
  61. data/lib/cattri/attribute_definer.rb +0 -143
  62. data/lib/cattri/class_attributes.rb +0 -277
  63. data/lib/cattri/instance_attributes.rb +0 -276
  64. data/sig/cattri.rbs +0 -4
@@ -0,0 +1,28 @@
1
+ module Cattri
2
+ # Base error class for all exceptions raised by Cattri.
3
+ #
4
+ # All Cattri-specific errors inherit from this class, allowing unified
5
+ # rescue handling at the framework level.
6
+ #
7
+ # The backtrace is preserved and may be filtered before raising.
8
+ #
9
+ # @example
10
+ # rescue Cattri::Error => e
11
+ # puts "Something went wrong with Cattri: #{e.message}"
12
+ class Error < StandardError
13
+ # Initializes the error with an optional message and caller backtrace.
14
+ #
15
+ # @param msg [String, nil] the error message
16
+ # @param backtrace [Array<String>] optional backtrace (defaults to `caller`)
17
+ def initialize: (?::String? msg, ?::Array[::String] backtrace) -> void
18
+ end
19
+
20
+ # Raised for any attribute-related definition or usage failure.
21
+ #
22
+ # This includes method conflicts, invalid configuration, and
23
+ # write attempts on `final` attributes.
24
+ #
25
+ # @see Cattri::Error
26
+ class AttributeError < Error
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ module Cattri
2
+ # Handles subclassing behavior for classes that use Cattri.
3
+ #
4
+ # This module installs a custom `.inherited` hook on the target class's singleton,
5
+ # ensuring that attribute definitions and values are deep-copied to the subclass.
6
+ #
7
+ # The hook preserves any existing `.inherited` behavior defined on the class,
8
+ # calling it before applying attribute propagation.
9
+ module Inheritance
10
+ # Installs an `inherited` hook on the given class.
11
+ #
12
+ # When the class is subclassed, Cattri will copy over attribute metadata and values
13
+ # using the subclass’s context. This ensures subclass safety and definition isolation.
14
+ #
15
+ # Any pre-existing `.inherited` method is preserved and invoked first.
16
+ #
17
+ # @param base [Class] the class to install the hook on
18
+ # @return [void]
19
+ def self.install: (::Module base) -> void
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ module Cattri
2
+ # Provides a patch to `#initialize` that ensures all final attributes
3
+ # are initialized with their default values if not already set.
4
+ #
5
+ # This module is prepended into Cattri-including classes to enforce
6
+ # write-once semantics for instance-level `final: true` attributes.
7
+ #
8
+ # @example
9
+ # class MyClass
10
+ # include Cattri
11
+ #
12
+ # cattri :id, -> { SecureRandom.uuid }, final: true
13
+ # end
14
+ #
15
+ # MyClass.new # => will have a UUID assigned to @id unless explicitly set
16
+ module InitializerPatch
17
+ # Hooked constructor that initializes final attributes using their defaults
18
+ # if no value has been set by the user.
19
+ #
20
+ # @param args [Array] any positional arguments passed to initialize
21
+ # @param kwargs [Hash] any keyword arguments passed to initialize
22
+ # @yield an optional block to pass to `super`
23
+ # @return [void]
24
+ def initialize: (*untyped args, **untyped kwargs) { (?) -> untyped } -> void
25
+ end
26
+ end
@@ -0,0 +1,75 @@
1
+ module Cattri
2
+ # Internal representation of a stored attribute value.
3
+ AttributeValue: untyped
4
+
5
+ # Provides an internal storage mechanism for attribute values defined via `cattri`.
6
+ #
7
+ # This module is included into any class or module using Cattri and replaces
8
+ # direct instance variable access with a namespaced store.
9
+ #
10
+ # It supports enforcement of `final` semantics and tracks explicit assignments.
11
+ module InternalStore
12
+ @__cattri_store: ::Hash[::Symbol, untyped]
13
+
14
+ @__cattri_set_variables: ::Set[::Symbol]
15
+
16
+ # Checks whether the internal store contains a value for the given key.
17
+ #
18
+ # @param key [String, Symbol] the attribute name or instance variable
19
+ # @return [Boolean] true if a value is present
20
+ def cattri_variable_defined?: (identifier key) -> bool
21
+
22
+ # Fetches the value for a given attribute key from the internal store.
23
+ #
24
+ # @param key [String, Symbol] the attribute name or instance variable
25
+ # @return [Object, nil] the stored value, or nil if not present
26
+ def cattri_variable_get: (identifier key) -> untyped
27
+
28
+ # Sets a value in the internal store for the given attribute key.
29
+ #
30
+ # Enforces final semantics if a final value was already set.
31
+ #
32
+ # @param key [String, Symbol] the attribute name or instance variable
33
+ # @param value [Object] the value to store
34
+ # @param final [Boolean] whether the value should be locked as final
35
+ # @return [Object] the stored value
36
+ def cattri_variable_set: (identifier key, untyped value, ?final: bool) -> untyped
37
+
38
+ # Evaluates and sets a value for the given key only if it hasn't already been set.
39
+ #
40
+ # If a value is already present, it is returned as-is. Otherwise, the provided block
41
+ # is called to compute the value, which is then stored. If `final: true` is passed,
42
+ # the value is marked as final and cannot be reassigned.
43
+ #
44
+ # @param key [String, Symbol] the attribute name or instance variable
45
+ # @param final [Boolean] whether to mark the value as final (immutable once set)
46
+ # @yieldreturn [Object] the value to memoize if not already present
47
+ # @return [Object] the existing or newly memoized value
48
+ # @raise [Cattri::AttributeError] if attempting to overwrite a final value
49
+ def cattri_variable_memoize: (identifier key, ?final: bool) { () -> untyped } -> untyped
50
+
51
+ private
52
+
53
+ # Returns the internal storage hash used for attribute values.
54
+ #
55
+ # @return [Hash<Symbol, Cattri::AttributeValue>]
56
+ def __cattri_store: () -> ::Hash[::Symbol, untyped]
57
+
58
+ # Returns the set of attribute keys that have been explicitly assigned.
59
+ #
60
+ # @return [Set<Symbol>]
61
+ def __cattri_set_variables: () -> ::Set[::Symbol]
62
+
63
+ # Normalizes the attribute key to a symbol without `@` prefix.
64
+ #
65
+ # @param key [String, Symbol]
66
+ # @return [Symbol]
67
+ def normalize_ivar: (identifier key) -> ::Symbol
68
+
69
+ # Raises if attempting to modify a value that was marked as final.
70
+ #
71
+ # @param key [Symbol]
72
+ # @raise [Cattri::AttributeError] if the key is final and already set
73
+ def guard_final!: (::Symbol key) -> void
74
+ end
75
+ end
@@ -0,0 +1,61 @@
1
+ module Cattri
2
+ # Provides a read-only interface for inspecting attributes defined via the Cattri DSL.
3
+ #
4
+ # When included, adds class-level methods to:
5
+ # - Check if an attribute is defined
6
+ # - Retrieve attribute definitions
7
+ # - List defined attribute methods
8
+ # - Trace the origin of an attribute
9
+ module Introspection
10
+ # @param base [Class, Module] the target that includes `Cattri`
11
+ # @return [void]
12
+ def self.included: (::Module base) -> void
13
+
14
+ # @api public
15
+ # Class-level introspection methods exposed via `.attribute_defined?`, `.attribute`, etc.
16
+ module ClassMethods
17
+ # Returns true if the given attribute has been defined on this class or any ancestor.
18
+ #
19
+ # @param name [Symbol, String] the attribute name
20
+ # @return [Boolean]
21
+ def attribute_defined?: (identifier name) -> bool
22
+
23
+ # Returns the attribute definition for the given name.
24
+ #
25
+ # Includes inherited definitions if available.
26
+ #
27
+ # @param name [Symbol, String] the attribute name
28
+ # @return [Cattri::Attribute, nil]
29
+ def attribute: (identifier name) -> Attribute?
30
+
31
+ # Returns a list of attribute names defined on this class.
32
+ #
33
+ # Includes inherited attributes if `with_ancestors` is true.
34
+ #
35
+ # @param with_ancestors [Boolean]
36
+ # @return [Array<Symbol>]
37
+ def attributes: (?with_ancestors: bool) -> ::Array[::Symbol]
38
+
39
+ # Returns a hash of attribute definitions defined on this class.
40
+ #
41
+ # Includes inherited attributes if `with_ancestors` is true.
42
+ #
43
+ # @param with_ancestors [Boolean]
44
+ # @return [Hash{Symbol => Cattri::Attribute}]
45
+ def attribute_definitions: (?with_ancestors: bool) -> ::Hash[::Symbol, Attribute]
46
+
47
+ # Returns a hash of all methods defined by Cattri attributes.
48
+ #
49
+ # This includes accessors, writers, and predicates where applicable.
50
+ #
51
+ # @return [Hash{Symbol => Set<Symbol>}]
52
+ def attribute_methods: () -> ::Hash[::Symbol, ::Set[::Symbol]]
53
+
54
+ # Returns the original class or module where the given attribute was defined.
55
+ #
56
+ # @param name [Symbol, String] the attribute name
57
+ # @return [Module, nil]
58
+ def attribute_source: (identifier name) -> ::Module?
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,19 @@
1
+ module Cattri
2
+ type identifier = ::String | ::Symbol
3
+
4
+ type scope_types = :class | :instance
5
+
6
+ type expose_types = :read_write | :read | :write | :none
7
+
8
+ type visibility_types = :public | :protected | :private
9
+
10
+ type attribute_options = {
11
+ ivar?: identifier,
12
+ final: bool,
13
+ scope?: scope_types,
14
+ predicate?: bool,
15
+ default?: ::Proc | untyped,
16
+ expose?: expose_types,
17
+ visibility?: visibility_types
18
+ }
19
+ end
@@ -0,0 +1,55 @@
1
+ module Cattri
2
+ # Cattri::Visibility tracks the current method visibility context (`public`, `protected`, `private`)
3
+ # when defining methods dynamically. It mimics Ruby's native visibility behavior so that
4
+ # `cattr` and `iattr` definitions can automatically infer the intended access level
5
+ # based on the current context in the source file.
6
+ #
7
+ # This module is intended to be extended by classes that include or extend Cattri.
8
+ #
9
+ # @example
10
+ # class MyClass
11
+ # include Cattri
12
+ #
13
+ # private
14
+ # cattr :sensitive_data
15
+ # end
16
+ #
17
+ # # => :sensitive_data will be defined as a private method
18
+ module Visibility
19
+ @__cattri_visibility: visibility_types
20
+
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: () -> visibility_types
27
+
28
+ # Intercepts calls to `public` to update the visibility tracker.
29
+ #
30
+ # If no method names are passed, this sets the current visibility scope for future methods.
31
+ # Otherwise, delegates to Ruby’s native `Module#public`.
32
+ #
33
+ # @param args [Array<Symbol>] method names to make public, or empty to set context
34
+ # @return [void]
35
+ def public: (*untyped args) -> void
36
+
37
+ # Intercepts calls to `protected` to update the visibility tracker.
38
+ #
39
+ # If no method names are passed, this sets the current visibility scope for future methods.
40
+ # Otherwise, delegates to Ruby’s native `Module#protected`.
41
+ #
42
+ # @param args [Array<Symbol>] method names to make protected, or empty to set context
43
+ # @return [void]
44
+ def protected: (*untyped args) -> void
45
+
46
+ # Intercepts calls to `private` to update the visibility tracker.
47
+ #
48
+ # If no method names are passed, this sets the current visibility scope for future methods.
49
+ # Otherwise, delegates to Ruby’s native `Module#private`.
50
+ #
51
+ # @param args [Array<Symbol>] method names to make private, or empty to set context
52
+ # @return [void]
53
+ def private: (*untyped args) -> void
54
+ end
55
+ end
@@ -0,0 +1,37 @@
1
+ # Main entrypoint for the Cattri DSL.
2
+ #
3
+ # When included in a class or module, this installs both class-level and instance-level
4
+ # attribute handling logic, visibility tracking, subclass inheritance propagation,
5
+ # and default value enforcement.
6
+ #
7
+ # It supports:
8
+ # - Defining class or instance attributes using `cattri` or `final_cattri`
9
+ # - Visibility tracking and method scoping
10
+ # - Write-once semantics for `final: true` attributes
11
+ # - Safe method generation with introspection support
12
+ module Cattri
13
+ VERSION: ::String
14
+
15
+ # Sets up the Cattri DSL on the including class or module.
16
+ #
17
+ # Includes internal storage and registry infrastructure into both the base and its singleton class,
18
+ # prepends initialization logic, extends visibility and DSL handling, and installs the
19
+ # subclassing hook to propagate attributes to descendants.
20
+ #
21
+ # @param base [Class, Module] the target that includes `Cattri`
22
+ # @return [void]
23
+ def self.included: (::Module base) -> void
24
+
25
+ # Provides opt-in class-level introspection support.
26
+ #
27
+ # This allows users to call methods like `.attribute_defined?`, `.attribute_methods`, etc.,
28
+ # to inspect which attributes have been defined.
29
+ module ClassMethods
30
+ # Enables Cattri's attribute introspection methods on the current class.
31
+ #
32
+ # @return [void]
33
+ def with_cattri_introspection: () -> void
34
+
35
+ include Introspection
36
+ end
37
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Cattri::AttributeCompiler do
6
+ let(:dummy_class) do
7
+ Class.new do
8
+ include Cattri
9
+ end
10
+ end
11
+
12
+ let(:context) { dummy_class.send(:context) }
13
+
14
+ describe ".define_accessor" do
15
+ context "when attribute is final and class-level" do
16
+ let(:attribute) do
17
+ Cattri::Attribute.new(
18
+ :count,
19
+ defined_in: dummy_class,
20
+ scope: :class,
21
+ final: true,
22
+ default: -> { 100 },
23
+ expose: :read_write
24
+ )
25
+ end
26
+
27
+ it "eagerly sets the default value on the target class" do
28
+ described_class.define_accessor(attribute, context)
29
+ expect(dummy_class.cattri_variable_get(:count)).to eq(100)
30
+ end
31
+ end
32
+
33
+ context "when expose is :none" do
34
+ let(:attribute) do
35
+ Cattri::Attribute.new(
36
+ :secret,
37
+ defined_in: dummy_class,
38
+ default: -> { "hidden" },
39
+ expose: :none
40
+ )
41
+ end
42
+
43
+ it "does not define any methods" do
44
+ described_class.define_accessor(attribute, context)
45
+ instance = dummy_class.new
46
+ expect(instance).not_to respond_to(:secret)
47
+ end
48
+ end
49
+
50
+ context "when expose is :read_write with predicate" do
51
+ let(:attribute) do
52
+ Cattri::Attribute.new(
53
+ :enabled,
54
+ defined_in: dummy_class,
55
+ default: -> { false },
56
+ predicate: true,
57
+ expose: :read_write
58
+ )
59
+ end
60
+
61
+ it "defines reader, writer, and predicate methods" do
62
+ described_class.define_accessor(attribute, context)
63
+ instance = dummy_class.new
64
+
65
+ expect(instance.enabled).to eq(false)
66
+ instance.enabled = true
67
+ expect(instance.enabled?).to eq(true)
68
+ end
69
+ end
70
+ end
71
+
72
+ describe ".define_accessor!" do
73
+ let(:attribute) do
74
+ Cattri::Attribute.new(
75
+ :setting,
76
+ defined_in: dummy_class,
77
+ default: -> { "default" },
78
+ expose: :read_write
79
+ )
80
+ end
81
+
82
+ it "defines a reader/writer method" do
83
+ described_class.send(:define_accessor!, attribute, context)
84
+ instance = dummy_class.new
85
+
86
+ expect(instance.setting).to eq("default")
87
+ instance.setting("updated")
88
+ expect(instance.setting).to eq("updated")
89
+ end
90
+ end
91
+
92
+ describe ".define_writer!" do
93
+ let(:attribute) do
94
+ Cattri::Attribute.new(
95
+ :mode,
96
+ defined_in: dummy_class,
97
+ default: -> { "auto" },
98
+ expose: :read_write
99
+ )
100
+ end
101
+
102
+ it "defines a writer method using 'name='" do
103
+ described_class.send(:define_accessor!, attribute, context)
104
+ described_class.send(:define_writer!, attribute, context)
105
+
106
+ instance = dummy_class.new
107
+ instance.mode = "manual"
108
+ expect(instance.mode).to eq("manual")
109
+ end
110
+ end
111
+
112
+ describe ".define_predicate!" do
113
+ let(:attribute) do
114
+ Cattri::Attribute.new(
115
+ :active,
116
+ defined_in: dummy_class,
117
+ default: -> {},
118
+ predicate: true,
119
+ expose: :read_write
120
+ )
121
+ end
122
+
123
+ it "defines a predicate method returning truthiness" do
124
+ described_class.send(:define_accessor!, attribute, context)
125
+ described_class.send(:define_predicate!, attribute, context)
126
+
127
+ instance = dummy_class.new
128
+ expect(instance.active?).to be false
129
+ instance.active("yes")
130
+ expect(instance.active?).to be true
131
+ end
132
+ end
133
+
134
+ describe ".memoize_default_value" do
135
+ let(:instance) { dummy_class.new }
136
+
137
+ context "non-final attribute" do
138
+ let(:attribute) do
139
+ Cattri::Attribute.new(
140
+ :foo,
141
+ defined_in: dummy_class,
142
+ default: -> { "bar" },
143
+ expose: :read_write
144
+ )
145
+ end
146
+
147
+ it "stores and returns the evaluated default" do
148
+ result = described_class.send(:memoize_default_value, instance, attribute)
149
+ expect(result).to eq("bar")
150
+ expect(instance.cattri_variable_get(:foo)).to eq("bar")
151
+ end
152
+ end
153
+
154
+ context "final attribute" do
155
+ let(:attribute) do
156
+ Cattri::Attribute.new(
157
+ :immutable,
158
+ defined_in: dummy_class,
159
+ final: true,
160
+ default: -> { "locked" },
161
+ expose: :read
162
+ )
163
+ end
164
+
165
+ it "raises if value is not already set" do
166
+ expect do
167
+ described_class.send(:memoize_default_value, instance, attribute)
168
+ end.to raise_error(Cattri::AttributeError, /Final attribute :immutable cannot be written to/)
169
+ end
170
+
171
+ it "returns value if already set" do
172
+ instance.cattri_variable_set(:immutable, "preset", final: true)
173
+ result = described_class.send(:memoize_default_value, instance, attribute)
174
+
175
+ expect(result).to eq("preset")
176
+ end
177
+ end
178
+ end
179
+ end