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.
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 +50 -0
  6. data/Gemfile +12 -0
  7. data/README.md +163 -144
  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 -153
  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 -124
  62. data/lib/cattri/class_attributes.rb +0 -204
  63. data/lib/cattri/instance_attributes.rb +0 -226
  64. data/sig/cattri.rbs +0 -4
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "simplecov_helper"
4
+ require "cattri"
5
+
6
+ RSpec.configure do |config|
7
+ # Enable flags like --only-failures and --next-failure
8
+ config.example_status_persistence_file_path = ".rspec_status"
9
+
10
+ # Disable RSpec exposing methods globally on `Module` and `main`
11
+ config.disable_monkey_patching!
12
+
13
+ config.expect_with :rspec do |c|
14
+ c.syntax = :expect
15
+ end
16
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cattri
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
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-23 00:00:00.000000000 Z
11
+ date: 2025-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: debride
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rspec
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +94,20 @@ dependencies:
80
94
  - - ">="
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: steep
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: yard
85
113
  requirement: !ruby/object:Gem::Requirement
@@ -102,25 +130,69 @@ executables: []
102
130
  extensions: []
103
131
  extra_rdoc_files: []
104
132
  files:
133
+ - ".github/workflows/main.yml"
134
+ - ".gitignore"
105
135
  - ".rspec"
106
136
  - ".rubocop.yml"
107
137
  - CHANGELOG.md
108
138
  - CODE_OF_CONDUCT.md
139
+ - Gemfile
109
140
  - LICENSE.txt
110
141
  - README.md
111
142
  - Rakefile
143
+ - Steepfile
144
+ - bin/console
145
+ - bin/setup
112
146
  - cattri.gemspec
113
147
  - lib/cattri.rb
114
148
  - lib/cattri/attribute.rb
115
- - lib/cattri/attribute_definer.rb
116
- - lib/cattri/class_attributes.rb
149
+ - lib/cattri/attribute_compiler.rb
150
+ - lib/cattri/attribute_options.rb
151
+ - lib/cattri/attribute_registry.rb
117
152
  - lib/cattri/context.rb
153
+ - lib/cattri/context_registry.rb
154
+ - lib/cattri/deferred_attributes.rb
155
+ - lib/cattri/dsl.rb
118
156
  - lib/cattri/error.rb
119
- - lib/cattri/instance_attributes.rb
157
+ - lib/cattri/inheritance.rb
158
+ - lib/cattri/initializer_patch.rb
159
+ - lib/cattri/internal_store.rb
120
160
  - lib/cattri/introspection.rb
121
161
  - lib/cattri/version.rb
122
162
  - lib/cattri/visibility.rb
123
- - sig/cattri.rbs
163
+ - sig/lib/cattri.rbs
164
+ - sig/lib/cattri/attribute.rbs
165
+ - sig/lib/cattri/attribute_compiler.rbs
166
+ - sig/lib/cattri/attribute_options.rbs
167
+ - sig/lib/cattri/attribute_registry.rbs
168
+ - sig/lib/cattri/context.rbs
169
+ - sig/lib/cattri/context_registry.rbs
170
+ - sig/lib/cattri/deferred_attributes.rbs
171
+ - sig/lib/cattri/dsl.rbs
172
+ - sig/lib/cattri/error.rbs
173
+ - sig/lib/cattri/inheritance.rbs
174
+ - sig/lib/cattri/initializer_patch.rbs
175
+ - sig/lib/cattri/internal_store.rbs
176
+ - sig/lib/cattri/introspection.rbs
177
+ - sig/lib/cattri/types.rbs
178
+ - sig/lib/cattri/visibility.rbs
179
+ - spec/cattri/attribute_compiler_spec.rb
180
+ - spec/cattri/attribute_options_spec.rb
181
+ - spec/cattri/attribute_registry_spec.rb
182
+ - spec/cattri/attribute_spec.rb
183
+ - spec/cattri/context_registry_spec.rb
184
+ - spec/cattri/context_spec.rb
185
+ - spec/cattri/deferred_attrributes_spec.rb
186
+ - spec/cattri/dsl_spec.rb
187
+ - spec/cattri/error_spec.rb
188
+ - spec/cattri/inheritance_spec.rb
189
+ - spec/cattri/initializer_patch_spec.rb
190
+ - spec/cattri/internal_store_spec.rb
191
+ - spec/cattri/introspection_spec.rb
192
+ - spec/cattri/visibility_spec.rb
193
+ - spec/cattri_spec.rb
194
+ - spec/simplecov_helper.rb
195
+ - spec/spec_helper.rb
124
196
  homepage: https://github.com/bnlucas/cattri
125
197
  licenses:
126
198
  - MIT
@@ -128,6 +200,7 @@ metadata:
128
200
  homepage_uri: https://github.com/bnlucas/cattri
129
201
  source_code_uri: https://github.com/bnlucas/cattri
130
202
  changelog_uri: https://github.com/bnlucas/cattri/blob/main/CHANGELOG.md
203
+ documentation_uri: https://www.rubydoc.info/gems/cattri
131
204
  rubygems_mfa_required: 'true'
132
205
  post_install_message:
133
206
  rdoc_options: []
@@ -1,124 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Cattri
4
- # Defines attribute accessors on a given target class using Cattri's Context.
5
- #
6
- # This class provides a set of utility methods to generate reader and writer
7
- # methods dynamically, with support for default values, coercion, and memoization.
8
- #
9
- # All accessors are defined through the Cattri::Context abstraction to ensure
10
- # consistent scoping, visibility, and method tracking.
11
- class AttributeDefiner
12
- class << self
13
- # Defines a callable accessor for class-level attributes.
14
- #
15
- # The generated method:
16
- # - Returns the memoized default value when called with no args or if readonly
17
- # - Otherwise, calls the attribute’s setter and memoizes the result
18
- #
19
- # If the attribute is not readonly, a writer (`foo=`) is also defined.
20
- #
21
- # @param attribute [Cattri::Attribute]
22
- # @param context [Cattri::Context]
23
- # @return [void]
24
- # @raise [Cattri::AttributeError] if the setter raises an error
25
- def define_callable_accessor(attribute, context)
26
- return unless attribute.class_level?
27
-
28
- context.define_method(attribute) do |*args, **kwargs|
29
- readonly = (args.empty? && kwargs.empty?) || attribute[:readonly]
30
- return AttributeDefiner.send(:memoize_default_value, self, attribute) if readonly
31
-
32
- value = attribute.invoke_setter(*args, **kwargs)
33
- instance_variable_set(attribute.ivar, value)
34
- end
35
-
36
- define_writer(attribute, context) unless attribute[:readonly]
37
- end
38
-
39
- # Defines an instance-level reader for class-level attributes.
40
- #
41
- # This method delegates the instance-level call to the class method.
42
- # It is used when `instance_reader: true` is specified.
43
- #
44
- # @param attribute [Cattri::Attribute]
45
- # @param context [Cattri::Context]
46
- # @return [void]
47
- def define_instance_level_reader(attribute, context)
48
- return unless attribute.class_level?
49
-
50
- context.target.define_method(attribute.name) do
51
- self.class.__send__(attribute.name)
52
- end
53
-
54
- context.send(:apply_access, attribute.name, attribute)
55
- end
56
-
57
- # Defines standard reader and writer methods for instance-level attributes.
58
- #
59
- # Skips definition if `reader: false` or `writer: false` is specified.
60
- #
61
- # @param attribute [Cattri::Attribute]
62
- # @param context [Cattri::Context]
63
- # @return [void]
64
- def define_accessor(attribute, context)
65
- define_reader(attribute, context) if attribute[:reader]
66
- define_writer(attribute, context) if attribute[:writer]
67
- end
68
-
69
- # Defines a memoizing reader for the given attribute.
70
- #
71
- # This is used for both class and instance attributes, and ensures that
72
- # the default value is computed only once and stored in the ivar.
73
- #
74
- # @param attribute [Cattri::Attribute]
75
- # @param context [Cattri::Context]
76
- # @return [void]
77
- def define_reader(attribute, context)
78
- context.define_method(attribute) do
79
- AttributeDefiner.send(:memoize_default_value, self, attribute)
80
- end
81
- end
82
-
83
- # Defines a writer method (`foo=`) that sets and coerces a value via the attribute setter.
84
- #
85
- # @param attribute [Cattri::Attribute]
86
- # @param context [Cattri::Context]
87
- # @return [void]
88
- def define_writer(attribute, context)
89
- context.define_method(attribute, name: :"#{attribute.name}=") do |value|
90
- coerced_value = attribute.setter.call(value)
91
- instance_variable_set(attribute.ivar, coerced_value)
92
- end
93
- end
94
-
95
- # Defines, or redefines, a writer method (`foo=`) that sets and coerces a value via the attribute setter.
96
- #
97
- # @param attribute [Cattri::Attribute]
98
- # @param context [Cattri::Context]
99
- # @return [void]
100
- def define_writer!(attribute, context)
101
- context.define_method!(attribute, name: :"#{attribute.name}=") do |value|
102
- coerced_value = attribute.setter.call(value)
103
- instance_variable_set(attribute.ivar, coerced_value)
104
- end
105
- end
106
-
107
- private
108
-
109
- # Returns the memoized value for an attribute or computes it from the default.
110
- #
111
- # This helper ensures lazy initialization while guarding against errors in the default proc.
112
- #
113
- # @param receiver [Object]
114
- # @param attribute [Cattri::Attribute]
115
- # @return [Object]
116
- # @raise [Cattri::AttributeError] if the default block raises an error
117
- def memoize_default_value(receiver, attribute)
118
- return receiver.instance_variable_get(attribute.ivar) if receiver.instance_variable_defined?(attribute.ivar)
119
-
120
- receiver.instance_variable_set(attribute.ivar, attribute.invoke_default)
121
- end
122
- end
123
- end
124
- end
@@ -1,204 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "attribute"
4
- require_relative "context"
5
- require_relative "attribute_definer"
6
- require_relative "visibility"
7
-
8
- module Cattri
9
- # Mixin that provides support for defining class-level attributes.
10
- #
11
- # This module is intended to be extended onto a class and provides a DSL
12
- # for defining configuration-style attributes at the class level using `cattr`.
13
- #
14
- # Features:
15
- # - Default values (static, frozen, or callable)
16
- # - Optional coercion via setter blocks
17
- # - Optional instance-level readers
18
- # - Visibility enforcement (`:public`, `:protected`, `:private`)
19
- #
20
- # Class attributes are stored internally as `Cattri::Attribute` instances and
21
- # values are memoized using class-level instance variables.
22
- module ClassAttributes
23
- # Defines one or more class-level attributes with optional default, coercion, and reader access.
24
- #
25
- # This method supports defining multiple attributes at once, provided they share the same options.
26
- # If a block is given, only one attribute may be defined to avoid ambiguity.
27
- #
28
- # @example Define multiple attributes with shared options
29
- # class_attribute :foo, :bar, default: 42
30
- #
31
- # @example Define a single attribute with a coercion block
32
- # class_attribute :path do |val|
33
- # Pathname(val)
34
- # end
35
- #
36
- # @param names [Array<Symbol | String>] the names of the attributes to define
37
- # @param options [Hash] additional attribute options
38
- # @option options [Object, Proc] :default the default value or lambda
39
- # @option options [Boolean] :readonly whether the attribute is read-only
40
- # @option options [Boolean] :instance_reader whether to define an instance-level reader (default: true)
41
- # @option options [Symbol] :access visibility level (:public, :protected, :private)
42
- # @yieldparam value [Object] an optional custom setter block
43
- # @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
44
- # `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
45
- # already defined or an error occurs while defining methods)
46
- # @return [void]
47
- def class_attribute(*names, **options, &block)
48
- raise Cattri::AmbiguousBlockError if names.size > 1 && block_given?
49
-
50
- names.each { |name| define_class_attribute(name, options, block) }
51
- end
52
-
53
- # Defines a read-only class attribute.
54
- #
55
- # Equivalent to calling `class_attribute(name, readonly: true, ...)`
56
- #
57
- # @param names [Array<Symbol | String>] the names of the attributes to define
58
- # @param options [Hash] additional attribute options
59
- # @option options [Object, Proc] :default the default value or lambda
60
- # @option options [Boolean] :readonly whether the attribute is read-only
61
- # @option options [Boolean] :instance_reader whether to define an instance-level reader (default: true)
62
- # @option options [Symbol] :access visibility level (:public, :protected, :private)
63
- # @raise [Cattri::AttributeError] or its subclasses, including `Cattri::AttributeDefinedError` or
64
- # `Cattri::AttributeDefinitionError` if defining the attribute fails (e.g., if the attribute is
65
- # already defined or an error occurs while defining methods)
66
- # @return [void]
67
- def class_attribute_reader(*names, **options)
68
- class_attribute(*names, **options, readonly: true)
69
- end
70
-
71
- # Updates the setter behavior of an existing class-level attribute.
72
- #
73
- # This allows coercion logic to be defined or overridden after the attribute
74
- # has been declared using `cattr`, as long as the writer method exists.
75
- #
76
- # @example Add coercion to an existing attribute
77
- # cattr :format
78
- # cattr_setter :format do |val|
79
- # val.to_s.downcase.to_sym
80
- # end
81
- #
82
- # @param name [Symbol, String] the name of the attribute
83
- # @yieldparam value [Object] the value passed to the setter
84
- # @yieldreturn [Object] the coerced value to be assigned
85
- # @raise [Cattri::AttributeNotDefinedError] if the attribute is not defined or the writer method does not exist
86
- # @raise [Cattri::AttributeDefinitionError] if method redefinition fails
87
- # @return [void]
88
- def class_attribute_setter(name, &block)
89
- name = name.to_sym
90
- attribute = __cattri_class_attributes[name]
91
- puts "<<< #{attribute} = #{name}>"
92
-
93
- if attribute.nil? || !context.method_defined?(:"#{name}=")
94
- raise Cattri::AttributeNotDefinedError.new(:class, name)
95
- end
96
-
97
- attribute.instance_variable_set(:@setter, attribute.send(:normalize_setter, block))
98
- Cattri::AttributeDefiner.define_writer!(attribute, context)
99
- end
100
-
101
- # Returns a list of defined class attribute names.
102
- #
103
- # @return [Array<Symbol>]
104
- def class_attributes
105
- __cattri_class_attributes.keys
106
- end
107
-
108
- # Checks whether a class attribute has been defined.
109
- #
110
- # @param name [Symbol]
111
- # @return [Boolean]
112
- def class_attribute_defined?(name)
113
- __cattri_class_attributes.key?(name.to_sym)
114
- end
115
-
116
- # Returns the full attribute definition object.
117
- #
118
- # @param name [Symbol]
119
- # @return [Cattri::Attribute, nil]
120
- def class_attribute_definition(name)
121
- __cattri_class_attributes[name.to_sym]
122
- end
123
-
124
- # @!method cattr(name, **options, &block)
125
- # Alias for {.class_attribute}
126
- # @see #class_attribute
127
- alias cattr class_attribute
128
-
129
- # @!method cattr_accessor(name, **options, &block)
130
- # Alias for {.class_attribute}
131
- # @see #class_attribute
132
- alias cattr_accessor class_attribute
133
-
134
- # @!method cattr_reader(name, **options)
135
- # Alias for {.class_attribute_reader}
136
- # @see #class_attribute_reader
137
- alias cattr_reader class_attribute_reader
138
-
139
- # @!method cattrs
140
- # Alias for {.class_attributes}
141
- # @return [Array<Symbol>]
142
- alias cattrs class_attributes
143
-
144
- # @!method cattr_defined?(name)
145
- # Alias for {.class_attribute_defined?}
146
- # @param name [Symbol]
147
- # @return [Boolean]
148
- alias cattr_defined? class_attribute_defined?
149
-
150
- # @!method cattr_definition(name)
151
- # Alias for {.class_attribute_definition}
152
- # @param name [Symbol]
153
- # @return [Cattri::Attribute, nil]
154
- alias cattr_definition class_attribute_definition
155
-
156
- private
157
-
158
- # Defines a single class-level attribute.
159
- #
160
- # This is the internal implementation used by {.class_attribute} and its aliases.
161
- # It constructs a `Cattri::Attribute`, registers it, and defines the necessary
162
- # class and instance methods.
163
- #
164
- # @param name [Symbol] the name of the attribute to define
165
- # @param options [Hash] additional attribute options (e.g., :default, :readonly)
166
- # @param block [Proc, nil] an optional setter block for coercion
167
- #
168
- # @raise [Cattri::AttributeDefinedError] if the attribute has already been defined
169
- # @raise [Cattri::AttributeDefinitionError] if method definition fails
170
- #
171
- # @return [void]
172
- def define_class_attribute(name, options, block) # rubocop:disable Metrics/AbcSize
173
- options[:access] ||= __cattri_visibility
174
- attribute = Cattri::Attribute.new(name, :class, options, block)
175
-
176
- raise Cattri::AttributeDefinedError.new(:class, name) if class_attribute_defined?(attribute.name)
177
-
178
- begin
179
- __cattri_class_attributes[name] = attribute
180
-
181
- Cattri::AttributeDefiner.define_callable_accessor(attribute, context)
182
- Cattri::AttributeDefiner.define_instance_level_reader(attribute, context) if attribute[:instance_reader]
183
- rescue StandardError => e
184
- raise Cattri::AttributeDefinitionError.new(self, attribute, e)
185
- end
186
- end
187
-
188
- # Internal registry of defined class-level attributes.
189
- #
190
- # @return [Hash{Symbol => Cattri::Attribute}]
191
- def __cattri_class_attributes
192
- @__cattri_class_attributes ||= {}
193
- end
194
-
195
- # Context object used to define accessors with scoped visibility.
196
- #
197
- # @return [Cattri::Context]
198
- # :nocov:
199
- def context
200
- @context ||= Context.new(self)
201
- end
202
- # :nocov:
203
- end
204
- end