cattri 0.2.2 → 0.2.3
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/CHANGELOG.md +6 -0
- data/lib/cattri/attribute.rb +1 -1
- data/lib/cattri/attribute_compiler.rb +1 -1
- data/lib/cattri/attribute_options.rb +10 -6
- data/lib/cattri/introspection.rb +5 -1
- data/lib/cattri/version.rb +1 -1
- data/sig/lib/cattri/attribute_options.rbs +2 -2
- data/spec/cattri/attribute_compiler_spec.rb +20 -0
- data/spec/cattri/attribute_options_spec.rb +12 -0
- data/spec/cattri/attribute_spec.rb +4 -4
- data/spec/cattri/context_registry_spec.rb +1 -0
- data/spec/cattri/context_spec.rb +3 -2
- data/spec/cattri/introspection_spec.rb +8 -0
- data/spec/cattri_spec.rb +43 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba04c1c408b5ffe93591ba80633bc3b7d7950d0e651e4adf738e30fe4ba795ea
|
|
4
|
+
data.tar.gz: a4d5f788238eee82419df14b8977b018ed8b7d91169f7719171186c26e9dde7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a923822a64d948f87bc16c14511ab2b9a6d278f3b3a9264ee4ab57515644f3f42dedaee7db4bd3b6d5a7d294daf866a00dd99b0d4d72b29faade58f6facc0e2
|
|
7
|
+
data.tar.gz: 26cc1589b802f4916b55b43312d567e7e68138a83e16780c6c5a49405b16e45a6325bfd5d5e39b7b80bbe5bdf53bbadd84b606765c156cf53183be6a49e251f9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.2.3] - 2025-12-14
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- New specs covering write-only exposure visibility for both instance and class attributes, ensuring readers are properly private/protected while writers remain public.
|
|
5
|
+
- Strengthened Context storage resolution tests to assert descriptive errors when instance receivers are missing.
|
|
6
|
+
|
|
1
7
|
## [0.2.2] - 2025-05-04
|
|
2
8
|
|
|
3
9
|
No breaking changes – the public DSL (cattri, final_cattri) remains identical to v0.2.0.
|
data/lib/cattri/attribute.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Cattri
|
|
|
33
33
|
return if attribute.expose == :none
|
|
34
34
|
|
|
35
35
|
define_accessor!(attribute, context)
|
|
36
|
-
define_writer!(attribute, context)
|
|
36
|
+
define_writer!(attribute, context) if attribute.writable?
|
|
37
37
|
define_predicate!(attribute, context) if attribute.with_predicate?
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -20,25 +20,29 @@ module Cattri
|
|
|
20
20
|
# Validates and normalizes the `expose` configuration.
|
|
21
21
|
#
|
|
22
22
|
# @param expose [Symbol, String] one of: :read, :write, :read_write, :none
|
|
23
|
+
# @param attribute_name [Symbol, nil] optional attribute name for error context
|
|
23
24
|
# @return [Symbol]
|
|
24
25
|
# @raise [Cattri::AttributeError] if the value is invalid
|
|
25
|
-
def validate_expose!(expose)
|
|
26
|
+
def validate_expose!(expose, attribute_name: nil)
|
|
26
27
|
expose = expose.to_sym
|
|
27
28
|
return expose if EXPOSE_OPTIONS.include?(expose) # steep:ignore
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
detail = attribute_name ? " for :#{attribute_name}" : ""
|
|
31
|
+
raise Cattri::AttributeError, "Invalid expose option `#{expose.inspect}`#{detail}"
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
# Validates and normalizes method visibility.
|
|
33
35
|
#
|
|
34
36
|
# @param visibility [Symbol, String] one of: :public, :protected, :private
|
|
37
|
+
# @param attribute_name [Symbol, nil] optional attribute name for error context
|
|
35
38
|
# @return [Symbol]
|
|
36
39
|
# @raise [Cattri::AttributeError] if the value is invalid
|
|
37
|
-
def validate_visibility!(visibility)
|
|
40
|
+
def validate_visibility!(visibility, attribute_name: nil)
|
|
38
41
|
visibility = visibility.to_sym
|
|
39
42
|
return visibility if VISIBILITIES.include?(visibility) # steep:ignore
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
detail = attribute_name ? " for :#{attribute_name}" : ""
|
|
45
|
+
raise Cattri::AttributeError, "Invalid visibility `#{visibility.inspect}`#{detail}"
|
|
42
46
|
end
|
|
43
47
|
end
|
|
44
48
|
|
|
@@ -88,8 +92,8 @@ module Cattri
|
|
|
88
92
|
@predicate = predicate
|
|
89
93
|
@default = normalize_default(default)
|
|
90
94
|
@transformer = normalize_transformer(transformer)
|
|
91
|
-
@expose = self.class.validate_expose!(expose)
|
|
92
|
-
@visibility = self.class.validate_visibility!(visibility)
|
|
95
|
+
@expose = self.class.validate_expose!(expose, attribute_name: @name)
|
|
96
|
+
@visibility = self.class.validate_visibility!(visibility, attribute_name: @name)
|
|
93
97
|
|
|
94
98
|
freeze
|
|
95
99
|
end
|
data/lib/cattri/introspection.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
3
5
|
module Cattri
|
|
4
6
|
# Provides a read-only interface for inspecting attributes defined via the Cattri DSL.
|
|
5
7
|
#
|
|
@@ -62,7 +64,9 @@ module Cattri
|
|
|
62
64
|
#
|
|
63
65
|
# @return [Hash{Symbol => Set<Symbol>}]
|
|
64
66
|
def attribute_methods
|
|
65
|
-
|
|
67
|
+
attribute_registry.defined_attributes(with_ancestors: true).transform_values do |attribute| # steep:ignore
|
|
68
|
+
Set.new(attribute.allowed_methods)
|
|
69
|
+
end
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
# Returns the original class or module where the given attribute was defined.
|
data/lib/cattri/version.rb
CHANGED
|
@@ -35,14 +35,14 @@ module Cattri
|
|
|
35
35
|
# @param expose [Symbol, String] one of: :read, :write, :read_write, :none
|
|
36
36
|
# @return [Symbol]
|
|
37
37
|
# @raise [Cattri::AttributeError] if the value is invalid
|
|
38
|
-
def self.validate_expose!: (expose_types | identifier expose) -> untyped
|
|
38
|
+
def self.validate_expose!: (expose_types | identifier expose, ?attribute_name: ::Symbol?) -> untyped
|
|
39
39
|
|
|
40
40
|
# Validates and normalizes method visibility.
|
|
41
41
|
#
|
|
42
42
|
# @param visibility [Symbol, String] one of: :public, :protected, :private
|
|
43
43
|
# @return [Symbol]
|
|
44
44
|
# @raise [Cattri::AttributeError] if the value is invalid
|
|
45
|
-
def self.validate_visibility!: (visibility_types | identifier visibility) -> untyped
|
|
45
|
+
def self.validate_visibility!: (visibility_types | identifier visibility, ?attribute_name: ::Symbol?) -> untyped
|
|
46
46
|
|
|
47
47
|
# Valid method visibility levels.
|
|
48
48
|
VISIBILITIES: ::Array[visibility_types]
|
|
@@ -67,6 +67,26 @@ RSpec.describe Cattri::AttributeCompiler do
|
|
|
67
67
|
expect(instance.enabled?).to eq(true)
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
|
+
|
|
71
|
+
context "when expose is :read" do
|
|
72
|
+
let(:attribute) do
|
|
73
|
+
Cattri::Attribute.new(
|
|
74
|
+
:visible,
|
|
75
|
+
defined_in: dummy_class,
|
|
76
|
+
default: -> { "shown" },
|
|
77
|
+
expose: :read
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "does not define a writer" do
|
|
82
|
+
described_class.define_accessor(attribute, context)
|
|
83
|
+
instance = dummy_class.new
|
|
84
|
+
|
|
85
|
+
expect(instance.visible).to eq("shown")
|
|
86
|
+
expect(instance.respond_to?(:visible=)).to be(false)
|
|
87
|
+
expect(instance.respond_to?(:visible=, true)).to be(false)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
70
90
|
end
|
|
71
91
|
|
|
72
92
|
describe ".define_accessor!" do
|
|
@@ -144,6 +144,18 @@ RSpec.describe Cattri::AttributeOptions do
|
|
|
144
144
|
expect(instance.instance_variable_get(:@visibility)).to eq(:private)
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
|
+
|
|
148
|
+
it "raises a helpful error when expose is invalid" do
|
|
149
|
+
expect do
|
|
150
|
+
described_class.new(name, expose: :bogus)
|
|
151
|
+
end.to raise_error(Cattri::AttributeError, /:bogus.*for :my_attribute/)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "raises a helpful error when visibility is invalid" do
|
|
155
|
+
expect do
|
|
156
|
+
described_class.new(name, visibility: :bogus)
|
|
157
|
+
end.to raise_error(Cattri::AttributeError, /:bogus.*for :my_attribute/)
|
|
158
|
+
end
|
|
147
159
|
end
|
|
148
160
|
|
|
149
161
|
describe "#[]" do
|
|
@@ -112,8 +112,8 @@ RSpec.describe Cattri::Attribute do
|
|
|
112
112
|
describe "#writable?" do
|
|
113
113
|
[
|
|
114
114
|
[true, :read, false],
|
|
115
|
-
[true, :write,
|
|
116
|
-
[true, :read_write,
|
|
115
|
+
[true, :write, true],
|
|
116
|
+
[true, :read_write, true],
|
|
117
117
|
[true, :none, false],
|
|
118
118
|
[false, :read, false],
|
|
119
119
|
[false, :write, true],
|
|
@@ -134,8 +134,8 @@ RSpec.describe Cattri::Attribute do
|
|
|
134
134
|
describe "#readonly?" do
|
|
135
135
|
[
|
|
136
136
|
[true, :read, true],
|
|
137
|
-
[true, :write,
|
|
138
|
-
[true, :read_write,
|
|
137
|
+
[true, :write, false],
|
|
138
|
+
[true, :read_write, false],
|
|
139
139
|
[true, :none, false],
|
|
140
140
|
[false, :read, true],
|
|
141
141
|
[false, :write, false],
|
data/spec/cattri/context_spec.rb
CHANGED
|
@@ -231,9 +231,10 @@ RSpec.describe Cattri::Context do
|
|
|
231
231
|
end
|
|
232
232
|
|
|
233
233
|
context "when a received cannot be resolved" do
|
|
234
|
-
it "raises
|
|
234
|
+
it "raises a descriptive Cattri::Error" do
|
|
235
235
|
expect { context.storage_receiver_for(instance_attribute, nil) }
|
|
236
|
-
.to raise_error
|
|
236
|
+
.to raise_error(Cattri::Error,
|
|
237
|
+
/Missing runtime instance for instance-level attribute :#{instance_attribute.name}/)
|
|
237
238
|
end
|
|
238
239
|
end
|
|
239
240
|
end
|
|
@@ -6,6 +6,7 @@ RSpec.describe Cattri::Introspection do
|
|
|
6
6
|
let(:klass) do
|
|
7
7
|
Class.new do
|
|
8
8
|
include Cattri
|
|
9
|
+
|
|
9
10
|
cattri :foo, 123
|
|
10
11
|
end
|
|
11
12
|
end
|
|
@@ -13,6 +14,7 @@ RSpec.describe Cattri::Introspection do
|
|
|
13
14
|
let(:subclass) do
|
|
14
15
|
Class.new(klass) do
|
|
15
16
|
include Cattri::Introspection
|
|
17
|
+
|
|
16
18
|
cattri :bar, "bar"
|
|
17
19
|
end
|
|
18
20
|
end
|
|
@@ -75,6 +77,12 @@ RSpec.describe Cattri::Introspection do
|
|
|
75
77
|
expect(methods.keys).to include(:bar)
|
|
76
78
|
expect(methods[:bar]).to include(:bar)
|
|
77
79
|
end
|
|
80
|
+
|
|
81
|
+
it "includes methods for inherited attributes" do
|
|
82
|
+
methods = subclass.attribute_methods
|
|
83
|
+
expect(methods.keys).to include(:foo)
|
|
84
|
+
expect(methods[:foo]).to include(:foo)
|
|
85
|
+
end
|
|
78
86
|
end
|
|
79
87
|
|
|
80
88
|
describe ".attribute_source" do
|
data/spec/cattri_spec.rb
CHANGED
|
@@ -35,6 +35,7 @@ RSpec.describe Cattri do
|
|
|
35
35
|
let(:introspective_class) do
|
|
36
36
|
Class.new do
|
|
37
37
|
include Cattri
|
|
38
|
+
|
|
38
39
|
cattri :id, "foo"
|
|
39
40
|
|
|
40
41
|
with_cattri_introspection
|
|
@@ -56,6 +57,7 @@ RSpec.describe Cattri do
|
|
|
56
57
|
it "allows one-time assignment to final instance attribute" do
|
|
57
58
|
klass = Class.new do
|
|
58
59
|
include Cattri
|
|
60
|
+
|
|
59
61
|
cattri :value, final: true
|
|
60
62
|
|
|
61
63
|
def initialize(value)
|
|
@@ -72,6 +74,7 @@ RSpec.describe Cattri do
|
|
|
72
74
|
it "prevents any assignment to final class attribute" do
|
|
73
75
|
klass = Class.new do
|
|
74
76
|
include Cattri
|
|
77
|
+
|
|
75
78
|
cattri :value, -> { "init" }, final: true, scope: :class
|
|
76
79
|
end
|
|
77
80
|
|
|
@@ -82,11 +85,13 @@ RSpec.describe Cattri do
|
|
|
82
85
|
it "allows shadowing parent class attributes" do
|
|
83
86
|
parent = Class.new do
|
|
84
87
|
include Cattri
|
|
88
|
+
|
|
85
89
|
cattri :enabled, true, final: true, scope: :class
|
|
86
90
|
end
|
|
87
91
|
|
|
88
92
|
child = Class.new do
|
|
89
93
|
include Cattri
|
|
94
|
+
|
|
90
95
|
cattri :enabled, false, final: true, scope: :class
|
|
91
96
|
end
|
|
92
97
|
|
|
@@ -97,6 +102,7 @@ RSpec.describe Cattri do
|
|
|
97
102
|
it "defines predicate method for instance attribute" do
|
|
98
103
|
klass = Class.new do
|
|
99
104
|
include Cattri
|
|
105
|
+
|
|
100
106
|
cattri :flag, false, predicate: true
|
|
101
107
|
end
|
|
102
108
|
|
|
@@ -110,6 +116,7 @@ RSpec.describe Cattri do
|
|
|
110
116
|
it "evaluates and stores default value lazily" do
|
|
111
117
|
klass = Class.new do
|
|
112
118
|
include Cattri
|
|
119
|
+
|
|
113
120
|
cattri :computed, -> { "value" }
|
|
114
121
|
end
|
|
115
122
|
|
|
@@ -123,6 +130,7 @@ RSpec.describe Cattri do
|
|
|
123
130
|
it "isolates instance and class attributes correctly" do
|
|
124
131
|
klass = Class.new do
|
|
125
132
|
include Cattri
|
|
133
|
+
|
|
126
134
|
cattri :config, scope: :class
|
|
127
135
|
cattri :state, scope: :instance
|
|
128
136
|
end
|
|
@@ -142,6 +150,7 @@ RSpec.describe Cattri do
|
|
|
142
150
|
mod = Module.new do
|
|
143
151
|
class << self
|
|
144
152
|
include Cattri
|
|
153
|
+
|
|
145
154
|
cattri :version, "0.1.0", final: true, scope: :class
|
|
146
155
|
end
|
|
147
156
|
end
|
|
@@ -152,6 +161,7 @@ RSpec.describe Cattri do
|
|
|
152
161
|
it "isolates class attributes across subclasses" do
|
|
153
162
|
parent = Class.new do
|
|
154
163
|
include Cattri
|
|
164
|
+
|
|
155
165
|
cattri :level, "parent", scope: :class
|
|
156
166
|
end
|
|
157
167
|
|
|
@@ -167,6 +177,7 @@ RSpec.describe Cattri do
|
|
|
167
177
|
it "applies custom coercion via block during assignment" do
|
|
168
178
|
klass = Class.new do
|
|
169
179
|
include Cattri
|
|
180
|
+
|
|
170
181
|
cattri :age do |value|
|
|
171
182
|
Integer(value)
|
|
172
183
|
end
|
|
@@ -181,6 +192,7 @@ RSpec.describe Cattri do
|
|
|
181
192
|
it "allows inherited initialize to set final attribute once" do
|
|
182
193
|
base = Class.new do
|
|
183
194
|
include Cattri
|
|
195
|
+
|
|
184
196
|
cattri :token, final: true
|
|
185
197
|
|
|
186
198
|
def initialize(token)
|
|
@@ -195,4 +207,35 @@ RSpec.describe Cattri do
|
|
|
195
207
|
expect { obj.token = "fail" }.to raise_error(Cattri::AttributeError)
|
|
196
208
|
end
|
|
197
209
|
end
|
|
210
|
+
|
|
211
|
+
describe "visibility with write-only exposure" do
|
|
212
|
+
let(:klass) do
|
|
213
|
+
Class.new do
|
|
214
|
+
include Cattri
|
|
215
|
+
|
|
216
|
+
cattri :token, expose: :write
|
|
217
|
+
cattri :level, :low, expose: :write, scope: :class
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "keeps the reader private while writer stays public on instances" do
|
|
222
|
+
instance = klass.new
|
|
223
|
+
|
|
224
|
+
expect(klass.public_instance_methods(false)).to include(:token=)
|
|
225
|
+
expect(klass.private_instance_methods(false)).to include(:token)
|
|
226
|
+
expect { instance.token }.to raise_error(NoMethodError)
|
|
227
|
+
|
|
228
|
+
instance.token = "set"
|
|
229
|
+
expect(instance.send(:token)).to eq("set")
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it "marks class-level readers as protected while keeping the writer public" do
|
|
233
|
+
expect(klass.singleton_class.public_instance_methods(false)).to include(:level=)
|
|
234
|
+
expect(klass.singleton_class.protected_instance_methods(false)).to include(:level)
|
|
235
|
+
expect { klass.level }.to raise_error(NoMethodError)
|
|
236
|
+
|
|
237
|
+
klass.level = :high
|
|
238
|
+
expect(klass.send(:level)).to eq(:high)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
198
241
|
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.2.
|
|
4
|
+
version: 0.2.3
|
|
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-
|
|
11
|
+
date: 2025-12-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: debride
|