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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f432670c63ac3fc653b105988804c219c7f73e0023c7fc1e11b2f56a16a9ac66
4
- data.tar.gz: 4302d8ffafd8c3f0e321aea61ec5c06e42ec7c5178130768c967b25ed0bcdf43
3
+ metadata.gz: ba04c1c408b5ffe93591ba80633bc3b7d7950d0e651e4adf738e30fe4ba795ea
4
+ data.tar.gz: a4d5f788238eee82419df14b8977b018ed8b7d91169f7719171186c26e9dde7d
5
5
  SHA512:
6
- metadata.gz: '08c0a0719c6699e42a40c92081700a0466ae78f3eda2ff08e34c75f76006c9a9bf915b65235893ee72ff28a0402c553b2dbca4ea596591a33cf1acd28a6fa975'
7
- data.tar.gz: 99c6caeb8fd09b5c0a3de773efec82f0f63648d73ba1f2e7597351a17bea6450b7b05ec8ca952cf0f2e0e9abc7a974738807d4b67048b8ae5190cc85ef043427
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.
@@ -105,7 +105,7 @@ module Cattri
105
105
  def readonly?
106
106
  return false if @options.expose == :none
107
107
 
108
- @options.expose == :read || final?
108
+ @options.expose == :read
109
109
  end
110
110
 
111
111
  # @return [Boolean] whether the attribute is marked final (write-once)
@@ -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
- raise Cattri::AttributeError, "Invalid expose option `#{expose.inspect}` for :#{name}"
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
- raise Cattri::AttributeError, "Invalid visibility `#{visibility.inspect}` for :#{name}"
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
@@ -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
- context.defined_methods # steep:ignore
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.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cattri
4
4
  # :nocov:
5
- VERSION = "0.2.2"
5
+ VERSION = "0.2.3"
6
6
  # :nocov:
7
7
  end
@@ -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, false],
116
- [true, :read_write, false],
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, true],
138
- [true, :read_write, true],
137
+ [true, :write, false],
138
+ [true, :read_write, false],
139
139
  [true, :none, false],
140
140
  [false, :read, true],
141
141
  [false, :write, false],
@@ -6,6 +6,7 @@ RSpec.describe Cattri::ContextRegistry do
6
6
  let(:klass) do
7
7
  Class.new do
8
8
  include Cattri::ContextRegistry
9
+
9
10
  public :context, :attribute_registry
10
11
  end
11
12
  end
@@ -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 an error" do
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.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-05-04 00:00:00.000000000 Z
11
+ date: 2025-12-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: debride