cattri 0.1.2 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +34 -0
- data/.gitignore +72 -0
- data/.rubocop.yml +6 -3
- data/CHANGELOG.md +50 -0
- data/Gemfile +12 -0
- data/README.md +163 -144
- data/Steepfile +6 -0
- data/bin/console +33 -0
- data/bin/setup +8 -0
- data/cattri.gemspec +5 -5
- data/lib/cattri/attribute.rb +119 -153
- data/lib/cattri/attribute_compiler.rb +104 -0
- data/lib/cattri/attribute_options.rb +183 -0
- data/lib/cattri/attribute_registry.rb +155 -0
- data/lib/cattri/context.rb +124 -106
- data/lib/cattri/context_registry.rb +36 -0
- data/lib/cattri/deferred_attributes.rb +73 -0
- data/lib/cattri/dsl.rb +54 -0
- data/lib/cattri/error.rb +17 -90
- data/lib/cattri/inheritance.rb +35 -0
- data/lib/cattri/initializer_patch.rb +37 -0
- data/lib/cattri/internal_store.rb +104 -0
- data/lib/cattri/introspection.rb +56 -49
- data/lib/cattri/version.rb +3 -1
- data/lib/cattri.rb +38 -99
- data/sig/lib/cattri/attribute.rbs +105 -0
- data/sig/lib/cattri/attribute_compiler.rbs +61 -0
- data/sig/lib/cattri/attribute_options.rbs +150 -0
- data/sig/lib/cattri/attribute_registry.rbs +95 -0
- data/sig/lib/cattri/context.rbs +130 -0
- data/sig/lib/cattri/context_registry.rbs +31 -0
- data/sig/lib/cattri/deferred_attributes.rbs +53 -0
- data/sig/lib/cattri/dsl.rbs +55 -0
- data/sig/lib/cattri/error.rbs +28 -0
- data/sig/lib/cattri/inheritance.rbs +21 -0
- data/sig/lib/cattri/initializer_patch.rbs +26 -0
- data/sig/lib/cattri/internal_store.rbs +75 -0
- data/sig/lib/cattri/introspection.rbs +61 -0
- data/sig/lib/cattri/types.rbs +19 -0
- data/sig/lib/cattri/visibility.rbs +55 -0
- data/sig/lib/cattri.rbs +37 -0
- data/spec/cattri/attribute_compiler_spec.rb +179 -0
- data/spec/cattri/attribute_options_spec.rb +267 -0
- data/spec/cattri/attribute_registry_spec.rb +257 -0
- data/spec/cattri/attribute_spec.rb +297 -0
- data/spec/cattri/context_registry_spec.rb +45 -0
- data/spec/cattri/context_spec.rb +346 -0
- data/spec/cattri/deferred_attrributes_spec.rb +117 -0
- data/spec/cattri/dsl_spec.rb +69 -0
- data/spec/cattri/error_spec.rb +37 -0
- data/spec/cattri/inheritance_spec.rb +60 -0
- data/spec/cattri/initializer_patch_spec.rb +35 -0
- data/spec/cattri/internal_store_spec.rb +139 -0
- data/spec/cattri/introspection_spec.rb +90 -0
- data/spec/cattri/visibility_spec.rb +68 -0
- data/spec/cattri_spec.rb +54 -0
- data/spec/simplecov_helper.rb +21 -0
- data/spec/spec_helper.rb +16 -0
- metadata +79 -6
- data/lib/cattri/attribute_definer.rb +0 -124
- data/lib/cattri/class_attributes.rb +0 -204
- data/lib/cattri/instance_attributes.rb +0 -226
- data/sig/cattri.rbs +0 -4
@@ -0,0 +1,105 @@
|
|
1
|
+
module Cattri
|
2
|
+
# @internal
|
3
|
+
#
|
4
|
+
# Attribute acts as a thin wrapper around AttributeOptions,
|
5
|
+
# exposing core attribute metadata and behavior in a safe, immutable way.
|
6
|
+
#
|
7
|
+
# Each Attribute instance represents a single logical property,
|
8
|
+
# and delegates its behavior (default, visibility, coercion, etc.) to its associated AttributeOptions.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# attribute = Attribute.new(:enabled, default: true, expose: :read_write)
|
12
|
+
# attribute.name # => :enabled
|
13
|
+
# attribute.default.call # => true
|
14
|
+
# attribute.expose # => :read_write
|
15
|
+
class Attribute
|
16
|
+
@options: AttributeOptions
|
17
|
+
|
18
|
+
@defined_in: ::Module
|
19
|
+
|
20
|
+
# @return [Module] the class or module this attribute was defined in
|
21
|
+
attr_reader defined_in: ::Module
|
22
|
+
|
23
|
+
# Initializes a new attribute definition.
|
24
|
+
#
|
25
|
+
# @param name [Symbol, String] the attribute name
|
26
|
+
# @param defined_in [Module] the class or module where this attribute is defined
|
27
|
+
# @param options [Hash] configuration options
|
28
|
+
# @option options [Boolean] :class whether the attribute is class-level (internally mapped to :class_attribute)
|
29
|
+
# @param transformer [Proc] optional block used to coerce/validate assigned values
|
30
|
+
def initialize: (identifier name, defined_in: ::Module, **attribute_options options) { (?) -> untyped } -> void
|
31
|
+
|
32
|
+
# Serializes this attribute and its configuration to a frozen hash.
|
33
|
+
#
|
34
|
+
# @return [Hash<Symbol, Object>]
|
35
|
+
def to_h: () -> ::Hash[::Symbol, untyped]
|
36
|
+
|
37
|
+
# @return [Symbol] the canonical name of the attribute
|
38
|
+
def name: () -> ::Symbol
|
39
|
+
|
40
|
+
# @return [Symbol] the backing instance variable (e.g., :@enabled)
|
41
|
+
def ivar: () -> ::Symbol
|
42
|
+
|
43
|
+
# @return [Proc] a callable lambda for the attribute’s default value
|
44
|
+
def default: () -> ::Proc
|
45
|
+
|
46
|
+
# @return [Proc] a callable transformer used to process assigned values
|
47
|
+
def transformer: () -> ::Proc
|
48
|
+
|
49
|
+
# @return [Symbol] method exposure type (:read, :write, :read_write, or :none)
|
50
|
+
def expose: () -> expose_types
|
51
|
+
|
52
|
+
# @return [Symbol] method visibility (:public, :protected, :private)
|
53
|
+
def visibility: () -> visibility_types
|
54
|
+
|
55
|
+
# @return [Boolean] whether the reader should remain internal
|
56
|
+
def internal_reader?: () -> bool
|
57
|
+
|
58
|
+
# @return [Boolean] whether the writer should remain internal
|
59
|
+
def internal_writer?: () -> bool
|
60
|
+
|
61
|
+
# @return [Boolean] whether the attribute allows reading
|
62
|
+
def readable?: () -> bool
|
63
|
+
|
64
|
+
# @return [Boolean] whether the attribute allows writing
|
65
|
+
def writable?: () -> bool
|
66
|
+
|
67
|
+
# @return [Boolean] whether the attribute is marked readonly
|
68
|
+
def readonly?: () -> bool
|
69
|
+
|
70
|
+
# @return [Boolean] whether the attribute is marked final (write-once)
|
71
|
+
def final?: () -> bool
|
72
|
+
|
73
|
+
# @return [Boolean] whether the attribute is class-level
|
74
|
+
def class_attribute?: () -> bool
|
75
|
+
|
76
|
+
# @return [Boolean] whether the attribute defines a predicate method (`:name?`)
|
77
|
+
def with_predicate?: () -> bool
|
78
|
+
|
79
|
+
# Returns the methods that will be defined for this attribute.
|
80
|
+
#
|
81
|
+
# Includes the base accessor, optional writer, and optional predicate.
|
82
|
+
#
|
83
|
+
# @return [Array<Symbol>] a list of method names
|
84
|
+
def allowed_methods: () -> ::Array[::Symbol]
|
85
|
+
|
86
|
+
# Validates whether this attribute is assignable in the current context.
|
87
|
+
#
|
88
|
+
# @raise [Cattri::AttributeError] if assignment is disallowed
|
89
|
+
def validate_assignment!: () -> void
|
90
|
+
|
91
|
+
# Resolves the default value for this attribute.
|
92
|
+
#
|
93
|
+
# @return [Object] the evaluated default
|
94
|
+
# @raise [Cattri::AttributeError] if default evaluation fails
|
95
|
+
def evaluate_default: () -> untyped
|
96
|
+
|
97
|
+
# Processes and transforms an incoming assignment for this attribute.
|
98
|
+
#
|
99
|
+
# @param args [Array] positional arguments to pass to the transformer
|
100
|
+
# @param kwargs [Hash] keyword arguments to pass to the transformer
|
101
|
+
# @return [Object] the transformed value
|
102
|
+
# @raise [Cattri::AttributeError] if transformation fails
|
103
|
+
def process_assignment: (*untyped args, **untyped kwargs) -> untyped
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Cattri
|
2
|
+
# @internal
|
3
|
+
#
|
4
|
+
# Responsible for defining methods on the target class/module
|
5
|
+
# based on the metadata in a {Cattri::Attribute}.
|
6
|
+
#
|
7
|
+
# This includes:
|
8
|
+
# - callable accessors (acting as both reader and writer)
|
9
|
+
# - predicate methods
|
10
|
+
# - explicit writers (`:name=` methods)
|
11
|
+
#
|
12
|
+
# Handles both instance and class-level attributes, including
|
13
|
+
# memoization and validation of default values for final attributes.
|
14
|
+
class AttributeCompiler
|
15
|
+
# Defines accessor methods for the given attribute in the provided context.
|
16
|
+
#
|
17
|
+
# For `final` + `class_attribute` attributes, the default is eagerly assigned.
|
18
|
+
# Then, if permitted by `expose`, the reader, writer, and/or predicate methods are defined.
|
19
|
+
#
|
20
|
+
# @param attribute [Cattri::Attribute] the attribute to define
|
21
|
+
# @param context [Cattri::Context] the target context for method definition
|
22
|
+
# @return [void]
|
23
|
+
def self.define_accessor: (Attribute attribute, Context context) -> void
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Defines a callable method that acts as both getter and setter.
|
28
|
+
#
|
29
|
+
# If called with no arguments, it returns the default (memoized).
|
30
|
+
# If called with arguments, it processes the assignment and writes the value.
|
31
|
+
#
|
32
|
+
# @param attribute [Cattri::Attribute]
|
33
|
+
# @param context [Cattri::Context]
|
34
|
+
# @return [void]
|
35
|
+
def self.define_accessor!: (Attribute attribute, Context context) -> void
|
36
|
+
|
37
|
+
# Defines a writer method `:name=`, assigning a transformed value to the backing store.
|
38
|
+
#
|
39
|
+
# @param attribute [Cattri::Attribute]
|
40
|
+
# @param context [Cattri::Context]
|
41
|
+
# @return [void]
|
42
|
+
def self.define_writer!: (Attribute attribute, Context context) -> void
|
43
|
+
|
44
|
+
# Defines a predicate method `:name?` that returns the truthiness of the value.
|
45
|
+
#
|
46
|
+
# @param attribute [Cattri::Attribute]
|
47
|
+
# @param context [Cattri::Context]
|
48
|
+
# @return [void]
|
49
|
+
def self.define_predicate!: (Attribute attribute, Context context) -> void
|
50
|
+
|
51
|
+
# Returns the default value for the attribute, memoizing it in the backing store.
|
52
|
+
#
|
53
|
+
# For `final` attributes, raises unless explicitly initialized.
|
54
|
+
#
|
55
|
+
# @param receiver [Object] the instance or class receiving the value
|
56
|
+
# @param attribute [Cattri::Attribute]
|
57
|
+
# @return [Object] the stored or evaluated default
|
58
|
+
# @raise [Cattri::AttributeError] if final attribute is unset or evaluation fails
|
59
|
+
def self.memoize_default_value: (InternalStore receiver, Attribute attribute) -> untyped
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Cattri
|
2
|
+
# @internal
|
3
|
+
#
|
4
|
+
# AttributeOptions encapsulates normalized metadata for a single Cattri-defined attribute.
|
5
|
+
#
|
6
|
+
# It validates, transforms, and freezes all input during initialization,
|
7
|
+
# ensuring attribute safety and immutability at runtime.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# options = AttributeOptions.new(:enabled, default: true, expose: :read_write)
|
11
|
+
# options.name # => :enabled
|
12
|
+
# options.default.call # => true
|
13
|
+
# options.expose # => :read_write
|
14
|
+
class AttributeOptions
|
15
|
+
@name: ::Symbol
|
16
|
+
|
17
|
+
@ivar: ::Symbol
|
18
|
+
|
19
|
+
@final: bool
|
20
|
+
|
21
|
+
@scope: scope_types
|
22
|
+
|
23
|
+
@predicate: bool
|
24
|
+
|
25
|
+
@default: ::Proc
|
26
|
+
|
27
|
+
@transformer: ::Proc
|
28
|
+
|
29
|
+
@expose: expose_types
|
30
|
+
|
31
|
+
@visibility: visibility_types
|
32
|
+
|
33
|
+
# Validates and normalizes the `expose` configuration.
|
34
|
+
#
|
35
|
+
# @param expose [Symbol, String] one of: :read, :write, :read_write, :none
|
36
|
+
# @return [Symbol]
|
37
|
+
# @raise [Cattri::AttributeError] if the value is invalid
|
38
|
+
def self.validate_expose!: (expose_types | identifier expose) -> untyped
|
39
|
+
|
40
|
+
# Validates and normalizes method visibility.
|
41
|
+
#
|
42
|
+
# @param visibility [Symbol, String] one of: :public, :protected, :private
|
43
|
+
# @return [Symbol]
|
44
|
+
# @raise [Cattri::AttributeError] if the value is invalid
|
45
|
+
def self.validate_visibility!: (visibility_types | identifier visibility) -> untyped
|
46
|
+
|
47
|
+
# Valid method visibility levels.
|
48
|
+
VISIBILITIES: ::Array[visibility_types]
|
49
|
+
|
50
|
+
# Valid expose options for method generation.
|
51
|
+
EXPOSE_OPTIONS: ::Array[expose_types]
|
52
|
+
|
53
|
+
# Valid scope types.
|
54
|
+
SCOPES: ::Array[scope_types]
|
55
|
+
|
56
|
+
# Built-in Ruby value types that are safe to reuse as-is (no dup needed).
|
57
|
+
SAFE_VALUE_TYPES: ::Array[untyped]
|
58
|
+
|
59
|
+
attr_reader name: ::Symbol
|
60
|
+
|
61
|
+
attr_reader ivar: ::Symbol
|
62
|
+
|
63
|
+
attr_reader final: bool
|
64
|
+
|
65
|
+
attr_reader scope: scope_types
|
66
|
+
|
67
|
+
attr_reader predicate: bool
|
68
|
+
|
69
|
+
attr_reader default: ::Proc
|
70
|
+
|
71
|
+
attr_reader transformer: ::Proc
|
72
|
+
|
73
|
+
attr_reader expose: expose_types
|
74
|
+
|
75
|
+
attr_reader visibility: visibility_types
|
76
|
+
|
77
|
+
# Initializes a frozen attribute configuration.
|
78
|
+
#
|
79
|
+
# @param name [Symbol, String] the attribute name
|
80
|
+
# @param ivar [Symbol, String, nil] optional custom instance variable name
|
81
|
+
# @param final [Boolean] marks the attribute as write-once
|
82
|
+
# @param class_attribute [Boolean] indicates if the attribute is class-level
|
83
|
+
# @param predicate [Boolean] whether to define a `?` predicate method
|
84
|
+
# @param default [Object, Proc, nil] default value or callable
|
85
|
+
# @param transformer [Proc, nil] optional coercion block
|
86
|
+
# @param expose [Symbol] access level to define (:read, :write, :read_write, :none)
|
87
|
+
# @param visibility [Symbol] method visibility (:public, :protected, :private)
|
88
|
+
def initialize: (
|
89
|
+
identifier name,
|
90
|
+
?ivar: identifier?,
|
91
|
+
?final: bool,
|
92
|
+
?scope: scope_types,
|
93
|
+
?predicate: bool,
|
94
|
+
?default: untyped,
|
95
|
+
?transformer: ::Proc?,
|
96
|
+
?expose: expose_types,
|
97
|
+
?visibility: visibility_types
|
98
|
+
) -> void
|
99
|
+
|
100
|
+
# Returns a frozen hash representation of this option set.
|
101
|
+
#
|
102
|
+
# @return [Hash<Symbol, Object>]
|
103
|
+
def to_h: () -> ::Hash[::Symbol, untyped]
|
104
|
+
|
105
|
+
# Allows hash-style access to the option set.
|
106
|
+
#
|
107
|
+
# @param key [Symbol, String]
|
108
|
+
# @return [Object]
|
109
|
+
def []: (untyped key) -> untyped
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# Normalizes the instance variable name, defaulting to @name.
|
114
|
+
#
|
115
|
+
# @param ivar [String, Symbol, nil]
|
116
|
+
# @return [Symbol]
|
117
|
+
def normalize_ivar: (identifier? ivar) -> ::Symbol
|
118
|
+
|
119
|
+
# Wraps the default in a Proc with immutability protection.
|
120
|
+
#
|
121
|
+
# - Returns original Proc if given.
|
122
|
+
# - Wraps immutable types as-is.
|
123
|
+
# - Duplicates mutable values at runtime.
|
124
|
+
#
|
125
|
+
# @param default [Object, Proc, nil]
|
126
|
+
# @return [Proc]
|
127
|
+
def normalize_default: (::Proc | untyped default) -> ::Proc
|
128
|
+
|
129
|
+
# Returns a normalized assignment transformer.
|
130
|
+
#
|
131
|
+
# Falls back to a default transformer that returns:
|
132
|
+
# - `kwargs` if `args.empty?`
|
133
|
+
# - the single argument if one is passed
|
134
|
+
# - `[*args, kwargs]` otherwise
|
135
|
+
#
|
136
|
+
# @param transformer [Proc, nil]
|
137
|
+
# @return [Proc]
|
138
|
+
def normalize_transformer: (::Proc? transformer) -> ::Proc
|
139
|
+
|
140
|
+
# Validates and normalizes the provided scope value.
|
141
|
+
#
|
142
|
+
# If `scope` is `nil`, it defaults to `:instance`. If it's one of the allowed
|
143
|
+
# values (`:class`, `:instance`), it is returned as-is. Otherwise, an error is raised.
|
144
|
+
#
|
145
|
+
# @param scope [Symbol, nil] the requested attribute scope
|
146
|
+
# @return [Symbol] the validated scope (`:class` or `:instance`)
|
147
|
+
# @raise [Cattri::AttributeError] if the scope is invalid
|
148
|
+
def validate_scope!: (scope_types scope) -> ::Symbol
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Cattri
|
2
|
+
# Cattri::AttributeRegistry is responsible for managing attribute definitions
|
3
|
+
# for a given context (class or module). It validates uniqueness, applies
|
4
|
+
# definition logic, and supports inheritance and introspection.
|
5
|
+
#
|
6
|
+
# It handles both eager and deferred attribute compilation and ensures correct
|
7
|
+
# behavior for `class: true`, `final: true`, and other attribute options.
|
8
|
+
class AttributeRegistry
|
9
|
+
@context: Context
|
10
|
+
|
11
|
+
@__cattri_registered_attributes: ::Hash[::Symbol, Attribute]
|
12
|
+
|
13
|
+
# @return [Cattri::Context] the context this registry operates within
|
14
|
+
attr_reader context: Context
|
15
|
+
|
16
|
+
# Initializes a new registry for the provided context.
|
17
|
+
#
|
18
|
+
# @param context [Cattri::Context]
|
19
|
+
def initialize: (Context context) -> void
|
20
|
+
|
21
|
+
# Returns the attributes registered directly on this context.
|
22
|
+
#
|
23
|
+
# @return [Hash{Symbol => Cattri::Attribute}]
|
24
|
+
def registered_attributes: () -> ::Hash[::Symbol, Attribute]
|
25
|
+
|
26
|
+
# Returns all known attributes, optionally including inherited definitions.
|
27
|
+
#
|
28
|
+
# @param with_ancestors [Boolean] whether to include ancestors
|
29
|
+
# @return [Hash{Symbol => Cattri::Attribute}]
|
30
|
+
def defined_attributes: (?with_ancestors: bool) -> ::Hash[::Symbol, Attribute]
|
31
|
+
|
32
|
+
# Fetches an attribute by name, or returns nil.
|
33
|
+
#
|
34
|
+
# @param name [String, Symbol]
|
35
|
+
# @param with_ancestors [Boolean]
|
36
|
+
# @return [Cattri::Attribute, nil]
|
37
|
+
def fetch_attribute: (identifier name, ?with_ancestors: bool) -> Attribute
|
38
|
+
|
39
|
+
# Fetches an attribute by name, or raises if not found.
|
40
|
+
#
|
41
|
+
# @param name [String, Symbol]
|
42
|
+
# @param with_ancestors [Boolean]
|
43
|
+
# @return [Cattri::Attribute]
|
44
|
+
# @raise [Cattri::AttributeError] if the attribute is not defined
|
45
|
+
def fetch_attribute!: (identifier name, ?with_ancestors: bool) -> Attribute
|
46
|
+
|
47
|
+
# Defines a new attribute and registers it on the current context.
|
48
|
+
#
|
49
|
+
# @param name [String, Symbol] the attribute name
|
50
|
+
# @param value [Object, Proc, nil] default value or initializer
|
51
|
+
# @param options [Hash] attribute options (`:class`, `:final`, etc.)
|
52
|
+
# @yield [*args] optional transformation block used as setter
|
53
|
+
# @return [Array<Symbol>] list of methods defined by this attribute
|
54
|
+
# @raise [Cattri::AttributeError] if the name is already defined
|
55
|
+
def define_attribute: (
|
56
|
+
identifier name,
|
57
|
+
Proc | untyped value,
|
58
|
+
**attribute_options options
|
59
|
+
) { (?) -> untyped } -> ::Array[::Symbol]
|
60
|
+
|
61
|
+
# Copies registered attributes from this context to another,
|
62
|
+
# preserving definitions and assigning values for `final: true, class: true`.
|
63
|
+
#
|
64
|
+
# @param target_context [Cattri::Context]
|
65
|
+
# @return [void]
|
66
|
+
def copy_attributes_to: (Context target_context) -> void
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# Validates that no attribute with the same name is already registered.
|
71
|
+
#
|
72
|
+
# @param name [Symbol]
|
73
|
+
# @raise [Cattri::AttributeError]
|
74
|
+
def validate_unique!: (::Symbol name) -> void
|
75
|
+
|
76
|
+
# Registers an attribute and applies or defers its definition.
|
77
|
+
#
|
78
|
+
# @param attribute [Cattri::Attribute]
|
79
|
+
# @return [void]
|
80
|
+
def register_attribute: (Attribute attribute) -> void
|
81
|
+
|
82
|
+
# Defers the attribute definition if in a module context.
|
83
|
+
#
|
84
|
+
# @param attribute [Cattri::Attribute]
|
85
|
+
# @return [void]
|
86
|
+
def defer_definition: (Attribute attribute) -> void
|
87
|
+
|
88
|
+
# Applies the attribute definition using the compiler.
|
89
|
+
#
|
90
|
+
# @param attribute [Cattri::Attribute]
|
91
|
+
# @return [void]
|
92
|
+
# @raise [Cattri::AttributeError]
|
93
|
+
def apply_definition!: (Attribute attribute) -> void
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Cattri
|
2
|
+
# Cattri::Context encapsulates the class or module that attributes are being defined on.
|
3
|
+
#
|
4
|
+
# It provides a safe interface for dynamically defining methods and tracking metadata,
|
5
|
+
# such as declared accessors, access visibility, and deferred attribute declarations.
|
6
|
+
#
|
7
|
+
# It handles:
|
8
|
+
# - Attribute method definitions (reader/writer/predicate)
|
9
|
+
# - Visibility enforcement
|
10
|
+
# - Target resolution (instance vs. class-level)
|
11
|
+
# - Method deduplication and tracking
|
12
|
+
#
|
13
|
+
# All method definitions occur directly on the resolved target (e.g., the class or its singleton).
|
14
|
+
class Context
|
15
|
+
@target: ::Module
|
16
|
+
|
17
|
+
@__cattri_defined_methods: ::Hash[::Symbol, ::Set[::Symbol]]
|
18
|
+
|
19
|
+
# The class or module that owns the attributes.
|
20
|
+
#
|
21
|
+
# @return [Module]
|
22
|
+
attr_reader target: ::Module
|
23
|
+
|
24
|
+
# @param target [Module, Class]
|
25
|
+
def initialize: (::Module target) -> void
|
26
|
+
|
27
|
+
# Returns a frozen copy of all attribute methods explicitly defined by this context.
|
28
|
+
#
|
29
|
+
# This does not include inherited or module-defined methods.
|
30
|
+
#
|
31
|
+
# @return [Hash{Symbol => Set<Symbol>}] map of attribute name to defined method names
|
32
|
+
def defined_methods: () -> ::Hash[::Symbol, ::Set[::Symbol]]
|
33
|
+
|
34
|
+
# Whether this target should defer method definitions (e.g., if it's a module).
|
35
|
+
#
|
36
|
+
# @return [Boolean]
|
37
|
+
def defer_definitions?: () -> bool
|
38
|
+
|
39
|
+
# Ensures the target includes Cattri::DeferredAttributes if needed.
|
40
|
+
#
|
41
|
+
# Used to prepare modules for later application of attributes when included elsewhere.
|
42
|
+
#
|
43
|
+
# @return [void]
|
44
|
+
def ensure_deferred_support!: () -> void
|
45
|
+
|
46
|
+
# All ancestors and included modules used for attribute lookup and inheritance.
|
47
|
+
#
|
48
|
+
# @return [Array<Module>]
|
49
|
+
def attribute_lookup_sources: () -> ::Array[::Module]
|
50
|
+
|
51
|
+
# Defines a method for the given attribute unless already defined locally.
|
52
|
+
#
|
53
|
+
# Respects attribute-level force overwrite and enforces visibility rules.
|
54
|
+
#
|
55
|
+
# @param attribute [Cattri::Attribute]
|
56
|
+
# @param name [Symbol, nil] optional method name override
|
57
|
+
# @yield method implementation block
|
58
|
+
# @raise [Cattri::AttributeError] if method is already defined and not forced
|
59
|
+
# @return [void]
|
60
|
+
def define_method: (Attribute attribute, ?name: identifier?) { (?) -> untyped } -> void
|
61
|
+
|
62
|
+
# Checks if the given method is already defined on the resolved target.
|
63
|
+
#
|
64
|
+
# Only checks methods directly defined on the class or singleton—not ancestors.
|
65
|
+
#
|
66
|
+
# @param attribute [Cattri::Attribute]
|
67
|
+
# @param name [Symbol, nil]
|
68
|
+
# @return [Boolean]
|
69
|
+
def method_defined?: (Attribute attribute, ?name: identifier?) -> bool
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Internal tracking of explicitly defined methods per attribute.
|
74
|
+
#
|
75
|
+
# @return [Hash{Symbol => Set<Symbol>}]
|
76
|
+
def __cattri_defined_methods: () -> ::Hash[::Symbol, ::Set[::Symbol]]
|
77
|
+
|
78
|
+
# Determines whether to define the method on the instance or singleton.
|
79
|
+
#
|
80
|
+
# @param attribute [Cattri::Attribute]
|
81
|
+
# @return [Module]
|
82
|
+
def target_for: (Attribute attribute) -> ::Module
|
83
|
+
|
84
|
+
# Defines the method and applies its access visibility.
|
85
|
+
#
|
86
|
+
# @param target [Module]
|
87
|
+
# @param attribute [Cattri::Attribute]
|
88
|
+
# @param name [Symbol]
|
89
|
+
# @yield method implementation
|
90
|
+
# @return [void]
|
91
|
+
def define_method!: (::Module target, Attribute attribute, ::Symbol name) { (?) -> untyped } -> void
|
92
|
+
|
93
|
+
# Applies visibility (`public`, `protected`, `private`) to a method.
|
94
|
+
#
|
95
|
+
# Skips application for `:public` (default in Ruby).
|
96
|
+
#
|
97
|
+
# @param target [Module]
|
98
|
+
# @param name [Symbol]
|
99
|
+
# @param attribute [Cattri::Attribute]
|
100
|
+
# @return [void]
|
101
|
+
def apply_visibility!: (::Module target, ::Symbol name, Attribute attribute) -> void
|
102
|
+
|
103
|
+
# Determines the effective visibility of the attribute.
|
104
|
+
#
|
105
|
+
# - If the attribute has no public writer or reader (i.e., `expose: :write` or `:none`)
|
106
|
+
# - Returns `:protected` for class-level attributes
|
107
|
+
# - Returns `:private` for instance-level attributes
|
108
|
+
# - Otherwise, returns the explicitly declared visibility (`attribute.visibility`)
|
109
|
+
#
|
110
|
+
# This ensures that internal-only attributes remain inaccessible outside their scope,
|
111
|
+
# while still being usable by subclasses if class-level.
|
112
|
+
#
|
113
|
+
# @param attribute [Cattri::Attribute]
|
114
|
+
# @return [Symbol]
|
115
|
+
def effective_visibility: (Attribute attribute, ::Symbol name) -> visibility_types
|
116
|
+
|
117
|
+
# Determines whether the given method name (accessor or writer)
|
118
|
+
# should be treated as internal-only based on the attribute's `expose` configuration.
|
119
|
+
#
|
120
|
+
# This is used when resolving method visibility (e.g., private vs protected).
|
121
|
+
#
|
122
|
+
# - Writer methods (`:attr=`) are considered internal if the attribute lacks public read access.
|
123
|
+
# - Reader methods (`:attr`) are considered internal if the attribute lacks public write access.
|
124
|
+
#
|
125
|
+
# @param attribute [Cattri::Attribute] the attribute definition
|
126
|
+
# @param name [Symbol, String] the method name being defined
|
127
|
+
# @return [Boolean] true if the method should be scoped for internal use only
|
128
|
+
def internal_method?: (Attribute attribute, Symbol name) -> bool
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Cattri
|
2
|
+
# Provides per-class or per-module access to the attribute registry and method definition context.
|
3
|
+
#
|
4
|
+
# This module is included into both the base and singleton class of any class using Cattri.
|
5
|
+
# It initializes and exposes a lazily-evaluated `attribute_registry` and `context` specific
|
6
|
+
# to the current scope, enabling safe and isolated attribute handling.
|
7
|
+
module ContextRegistry
|
8
|
+
@attribute_registry: AttributeRegistry
|
9
|
+
|
10
|
+
@context: Context
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
# Returns the attribute definition registry for this class or module.
|
15
|
+
#
|
16
|
+
# The registry is responsible for tracking all defined attributes, both class-level and
|
17
|
+
# instance-level, handling application logic, and copying across subclasses where needed.
|
18
|
+
#
|
19
|
+
# @return [Cattri::AttributeRegistry] the registry used to define and apply attributes
|
20
|
+
def attribute_registry: () -> AttributeRegistry
|
21
|
+
|
22
|
+
# Returns the method definition context for this class or module.
|
23
|
+
#
|
24
|
+
# The context wraps the current target (class or module) and provides utilities
|
25
|
+
# for defining attribute methods (readers, writers, predicates), managing visibility,
|
26
|
+
# and recording declared methods to avoid duplication.
|
27
|
+
#
|
28
|
+
# @return [Cattri::Context] the context used for method definition and visibility tracking
|
29
|
+
def context: () -> Context
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Cattri
|
2
|
+
# Provides support for defining attributes within a module that should be
|
3
|
+
# applied later to any class or module that includes or extends it.
|
4
|
+
#
|
5
|
+
# This allows DSL modules to define Cattri attributes without prematurely
|
6
|
+
# applying them to themselves, deferring application to the including/extending context.
|
7
|
+
module DeferredAttributes
|
8
|
+
@deferred_attributes: ::Hash[::Symbol, Attribute]
|
9
|
+
|
10
|
+
# Hook into the module extension lifecycle to ensure deferred attributes are
|
11
|
+
# applied when the module is included or extended.
|
12
|
+
#
|
13
|
+
# @param base [Module] the module that extended this module
|
14
|
+
# @return [void]
|
15
|
+
def self.extended: (::Module base) -> void
|
16
|
+
|
17
|
+
# Hook methods for inclusion/extension that trigger deferred application.
|
18
|
+
module Hook
|
19
|
+
# Called when a module including `DeferredAttributes` is included into another module/class.
|
20
|
+
#
|
21
|
+
# @param target [Module] the including class or module
|
22
|
+
# @return [void]
|
23
|
+
def included: (::Module target) -> void
|
24
|
+
|
25
|
+
# Called when a module including `DeferredAttributes` is extended into another module/class.
|
26
|
+
#
|
27
|
+
# @param target [Module] the extending class or module
|
28
|
+
# @return [void]
|
29
|
+
def extended: (::Module target) -> void
|
30
|
+
end
|
31
|
+
|
32
|
+
# Registers an attribute to be applied later when this module is included or extended.
|
33
|
+
#
|
34
|
+
# @param attribute [Cattri::Attribute] the attribute to defer
|
35
|
+
# @return [void]
|
36
|
+
def defer_attribute: (Attribute attribute) -> void
|
37
|
+
|
38
|
+
# Applies all deferred attributes to the target class or module.
|
39
|
+
#
|
40
|
+
# This is triggered automatically by the {Hook} on `included` or `extended`.
|
41
|
+
#
|
42
|
+
# @param target [Module] the class or module to apply the attributes to
|
43
|
+
# @return [void]
|
44
|
+
def apply_deferred_attributes: (::Module target) -> void
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Internal storage of deferred attributes for this module.
|
49
|
+
#
|
50
|
+
# @return [Hash{Symbol => Cattri::Attribute}]
|
51
|
+
def deferred_attributes: () -> ::Hash[::Symbol, Attribute]
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Cattri
|
2
|
+
# Provides the primary DSL for defining class-level and instance-level attributes.
|
3
|
+
#
|
4
|
+
# This module is extended into any class or module that includes Cattri,
|
5
|
+
# and exposes methods like `cattri` and `final_cattri` for concise attribute declaration.
|
6
|
+
#
|
7
|
+
# All attributes are defined through the underlying attribute registry and
|
8
|
+
# are associated with method accessors (reader, writer, predicate) based on options.
|
9
|
+
module Dsl
|
10
|
+
# Defines a new attribute with optional default, coercion, and visibility options.
|
11
|
+
#
|
12
|
+
# The attribute can be defined with a static value, a lazy-evaluated block,
|
13
|
+
# or with additional options like `final`, `predicate`, or `expose`.
|
14
|
+
#
|
15
|
+
# The attribute will be defined as either class-level or instance-level
|
16
|
+
# depending on the `class:` option.
|
17
|
+
#
|
18
|
+
# @param name [Symbol, String] the attribute name
|
19
|
+
# @param value [Object, nil] optional static value or default
|
20
|
+
# @param options [Hash] additional attribute configuration
|
21
|
+
# @option options [Boolean] :class whether this is a class-level attribute
|
22
|
+
# @option options [Boolean] :final whether the attribute is write-once
|
23
|
+
# @option options [Boolean] :predicate whether to define a predicate method
|
24
|
+
# @option options [Symbol] :expose whether to expose `:read`, `:write`, `:read_write`, or `:none`
|
25
|
+
# @option options [Symbol] :visibility the visibility for generated methods (`:public`, `:protected`, `:private`)
|
26
|
+
# @yield optional block to lazily evaluate the attribute’s default value
|
27
|
+
# @return [Array<Symbol>]
|
28
|
+
def cattri: (
|
29
|
+
identifier? name,
|
30
|
+
?untyped? value,
|
31
|
+
**attribute_options options
|
32
|
+
) { (?) -> untyped } -> ::Array[::Symbol]
|
33
|
+
|
34
|
+
# Defines a write-once (final) attribute.
|
35
|
+
#
|
36
|
+
# Final attributes can be written only once and raise on re-assignment.
|
37
|
+
# This is equivalent to `cattri(..., final: true)`.
|
38
|
+
#
|
39
|
+
# @param name [Symbol, String] the attribute name
|
40
|
+
# @param value [Object, nil] static or lazy default value
|
41
|
+
# @param options [Hash] additional attribute configuration
|
42
|
+
# @option options [Boolean] :class whether this is a class-level attribute
|
43
|
+
# @option options [Boolean] :final whether the attribute is write-once
|
44
|
+
# @option options [Boolean] :predicate whether to define a predicate method
|
45
|
+
# @option options [Symbol] :expose whether to expose `:read`, `:write`, `:read_write`, or `:none`
|
46
|
+
# @option options [Symbol] :visibility the visibility for generated methods (`:public`, `:protected`, `:private`)
|
47
|
+
# @yield optional block to lazily evaluate the default
|
48
|
+
# @return [Array<Symbol>]
|
49
|
+
def final_cattri: (
|
50
|
+
identifier name,
|
51
|
+
untyped value,
|
52
|
+
**attribute_options options
|
53
|
+
) { (?) -> untyped } -> ::Array[::Symbol]
|
54
|
+
end
|
55
|
+
end
|