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,297 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Cattri::Attribute do
4
+ let(:name) { :attr }
5
+ let(:klass) { Class.new }
6
+ let(:final) { true }
7
+ let(:expose) { :read_write }
8
+ let(:scope) { :class }
9
+ let(:predicate) { false }
10
+ let(:default) { "default" }
11
+ let(:transformer) { ->(value) { value } }
12
+
13
+ subject(:attribute) do
14
+ described_class.new(
15
+ name,
16
+ defined_in: klass,
17
+ final: final,
18
+ expose: expose,
19
+ scope: scope,
20
+ predicate: predicate,
21
+ default: default,
22
+ transformer: transformer
23
+ )
24
+ end
25
+
26
+ describe "#initialize" do
27
+ it "instantiates an Attribute" do
28
+ expect(attribute).to be_a(described_class)
29
+ expect(attribute.defined_in).to eq(klass)
30
+ end
31
+ end
32
+
33
+ describe "#to_h" do
34
+ it "returns the Cattri::AttributeOptions hash" do
35
+ hash = attribute.to_h
36
+ options_hash = attribute.instance_variable_get(:@options).to_h
37
+
38
+ expect(hash).to be_a(Hash)
39
+ expect(hash).to eq(options_hash.merge(defined_in: klass))
40
+ end
41
+ end
42
+
43
+ %i[
44
+ name
45
+ ivar
46
+ default
47
+ transformer
48
+ expose
49
+ visibility
50
+ ].each do |option|
51
+ describe "##{option}" do
52
+ it "proxies to @options.#{option}" do
53
+ result = attribute.public_send(option)
54
+ options_value = attribute.instance_variable_get(:@options).public_send(option)
55
+
56
+ expect(result).to eq(options_value)
57
+ end
58
+ end
59
+ end
60
+
61
+ describe "#internal_reader?" do
62
+ [
63
+ [:read, false],
64
+ [:write, true],
65
+ [:read_write, false],
66
+ [:none, true]
67
+ ].each do |(value, expected)|
68
+ context "when instantiated with `expose: :#{value}`" do
69
+ let(:expose) { value }
70
+
71
+ it "returns #{expected}" do
72
+ expect(attribute.internal_reader?).to eq(expected)
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ describe "#internal_writer?" do
79
+ [
80
+ [:read, true],
81
+ [:write, false],
82
+ [:read_write, false],
83
+ [:none, true]
84
+ ].each do |(value, expected)|
85
+ context "when instantiated with `expose: :#{value}`" do
86
+ let(:expose) { value }
87
+
88
+ it "returns #{expected}" do
89
+ expect(attribute.internal_writer?).to eq(expected)
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ describe "#readable?" do
96
+ [
97
+ [:read, true],
98
+ [:write, false],
99
+ [:read_write, true],
100
+ [:none, false]
101
+ ].each do |(value, expected)|
102
+ context "when instantiated with `expose: #{value}`" do
103
+ let(:expose) { value }
104
+
105
+ it "returns #{expected}" do
106
+ expect(attribute.readable?).to eq(expected)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ describe "#writable?" do
113
+ [
114
+ [true, :read, false],
115
+ [true, :write, false],
116
+ [true, :read_write, false],
117
+ [true, :none, false],
118
+ [false, :read, false],
119
+ [false, :write, true],
120
+ [false, :read_write, true],
121
+ [false, :none, false]
122
+ ].each do |(final_value, expose_value, expected)|
123
+ context "when instantiated with `final: #{final_value}, expose: #{expose_value}`" do
124
+ let(:final) { final_value }
125
+ let(:expose) { expose_value }
126
+
127
+ it "returns #{expected}" do
128
+ expect(attribute.writable?).to eq(expected)
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ describe "#readonly?" do
135
+ [
136
+ [true, :read, true],
137
+ [true, :write, true],
138
+ [true, :read_write, true],
139
+ [true, :none, false],
140
+ [false, :read, true],
141
+ [false, :write, false],
142
+ [false, :read_write, false],
143
+ [false, :none, false]
144
+ ].each do |(final_value, expose_value, expected)|
145
+ context "when instantiated with `final: #{final_value}, expose: #{expose_value}`" do
146
+ let(:final) { final_value }
147
+ let(:expose) { expose_value }
148
+
149
+ it "returns #{expected}" do
150
+ expect(attribute.readonly?).to eq(expected)
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ describe "#final?" do
157
+ [true, false].each do |value|
158
+ context "when instantiated with `final: #{value}`" do
159
+ let(:final) { value }
160
+
161
+ it "returns #{value}" do
162
+ expect(attribute.final?).to eq(value)
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ describe "#class_attribute?" do
169
+ %i[class instance].each do |value|
170
+ context "when instantiated with `scope: #{value}`" do
171
+ let(:scope) { value }
172
+
173
+ it "returns #{value}" do
174
+ expect(attribute.class_attribute?).to eq(value == :class)
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ describe "#with_predicate?" do
181
+ [true, false].each do |value|
182
+ context "when instantiated with `predicate: #{value}`" do
183
+ let(:predicate) { value }
184
+
185
+ it "returns #{value}" do
186
+ expect(attribute.with_predicate?).to eq(value)
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ describe "#allowed_methods" do
193
+ shared_examples_for "allowed method set" do |writable, predicate, expected|
194
+ let(:expose) { writable ? :read_write : :read }
195
+ let(:predicate) { predicate }
196
+ let(:final) { false }
197
+
198
+ it "returns #{expected.inspect} for writable: #{writable}, predicate: #{predicate}" do
199
+ expect(attribute.allowed_methods).to eq(expected)
200
+ end
201
+ end
202
+
203
+ it_behaves_like "allowed method set", true, true, %i[attr attr= attr?]
204
+ it_behaves_like "allowed method set", true, false, %i[attr attr=]
205
+ it_behaves_like "allowed method set", false, true, %i[attr attr?]
206
+ it_behaves_like "allowed method set", false, false, [:attr]
207
+ end
208
+
209
+ describe "#validate_assignment!" do
210
+ [
211
+ [true, :read, true, false],
212
+ [true, :write, true, false],
213
+ [true, :read_write, true, false],
214
+ [true, :none, true, false],
215
+ [false, :read, false, true],
216
+ [false, :write, false, false],
217
+ [false, :read_write, false, false],
218
+ [false, :none, false, false]
219
+ ].each do |(final_value, expose_value, raise_final, raise_readonly)|
220
+ context "when instantiated with `final: #{final_value}, expose: #{expose_value}`" do
221
+ let(:final) { final_value }
222
+ let(:expose) { expose_value }
223
+
224
+ it "raises an error" do
225
+ message =
226
+ if raise_final
227
+ "Cannot assign to final attribute `:#{attribute.name}`"
228
+ elsif raise_readonly
229
+ "Cannot assign to readonly attribute `:#{attribute.name}`"
230
+ end
231
+
232
+ if raise_final || raise_readonly
233
+ expect { attribute.validate_assignment! }
234
+ .to raise_error(Cattri::AttributeError, message)
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ context "when instantiated with `final: false, expose: :read_write`" do
241
+ let(:final) { false }
242
+ let(:expose) { :read_write }
243
+
244
+ it "does not raise an error" do
245
+ expect { attribute.validate_assignment! }.not_to raise_error
246
+ end
247
+ end
248
+ end
249
+
250
+ describe "#evaluate_default" do
251
+ context "when the default value succeeds" do
252
+ it "calls the transformer and returns the result" do
253
+ expect(attribute.evaluate_default).to eq(default)
254
+ end
255
+ end
256
+
257
+ context "when the default value raises an error" do
258
+ let(:default) { double("invalid") }
259
+
260
+ before do
261
+ allow(default).to receive(:dup).and_raise(TypeError, "boom")
262
+ end
263
+
264
+ it "raises Cattri::AttributeError" do
265
+ expect { attribute.evaluate_default }
266
+ .to raise_error(
267
+ Cattri::AttributeError,
268
+ "Failed to evaluate the default value for `:#{attribute.name}`. Error: boom"
269
+ )
270
+ end
271
+ end
272
+ end
273
+
274
+ describe "#process_assignment" do
275
+ context "when the transformer succeeds" do
276
+ it "calls the transformer and returns the result" do
277
+ expect(attribute.process_assignment("cattri")).to eq("cattri")
278
+ end
279
+ end
280
+
281
+ context "when the transformer raises an error" do
282
+ let(:transformer) { double("invalid") }
283
+
284
+ before do
285
+ allow(transformer).to receive(:call).and_raise(TypeError, "boom")
286
+ end
287
+
288
+ it "raises Cattri::AttributeError" do
289
+ expect { attribute.process_assignment(1, a: 2) }
290
+ .to raise_error(
291
+ Cattri::AttributeError,
292
+ "Failed to evaluate the setter for `:#{attribute.name}`. Error: boom"
293
+ )
294
+ end
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Cattri::ContextRegistry do
6
+ let(:klass) do
7
+ Class.new do
8
+ include Cattri::ContextRegistry
9
+ public :context, :attribute_registry
10
+ end
11
+ end
12
+
13
+ subject(:instance) { klass.new }
14
+
15
+ describe "#context" do
16
+ it "returns a Cattri::Context wrapping self" do
17
+ result = instance.context
18
+ expect(result).to be_a(Cattri::Context)
19
+ expect(result.target).to eq(instance)
20
+ end
21
+
22
+ it "memoizes the context" do
23
+ expect(instance.context).to equal(instance.context)
24
+ end
25
+ end
26
+
27
+ describe "#attribute_registry" do
28
+ it "returns a Cattri::AttributeRegistry instance" do
29
+ result = instance.attribute_registry
30
+ expect(result).to be_a(Cattri::AttributeRegistry)
31
+ end
32
+
33
+ it "is initialized with the context" do
34
+ expect(Cattri::AttributeRegistry).to receive(:new)
35
+ .with(instance.context)
36
+ .and_call_original
37
+
38
+ instance.attribute_registry
39
+ end
40
+
41
+ it "memoizes the registry" do
42
+ expect(instance.attribute_registry).to equal(instance.attribute_registry)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Cattri::Context do
6
+ let(:context_target) { Class.new }
7
+
8
+ def define_attribute(name, value, scope: :instance, visibility: :public)
9
+ Cattri::Attribute.new(
10
+ name,
11
+ default: value,
12
+ scope: scope,
13
+ defined_in: context_target,
14
+ visibility: visibility
15
+ )
16
+ end
17
+
18
+ let(:class_attribute) { define_attribute(:class_attr, "class", scope: :class) }
19
+ let(:instance_attribute) { define_attribute(:instance_attr, "instance") }
20
+ let(:attribute_options) { {} }
21
+
22
+ subject(:context) { described_class.new(context_target) }
23
+
24
+ describe "#initialize" do
25
+ it "instantiates a new Cattri::Context" do
26
+ expect(context).to be_a(described_class)
27
+ expect(context.target).to eq(context_target)
28
+ end
29
+ end
30
+
31
+ describe "#defined_methods" do
32
+ it "returns a Hash" do
33
+ expect(context.defined_methods).to be_a(Hash)
34
+ end
35
+
36
+ it "returns a frozen object" do
37
+ expect(context.defined_methods).to be_frozen
38
+ end
39
+
40
+ it "returns a duplicate of the internal hash" do
41
+ original = { foo: :bar }
42
+ context.instance_variable_set(:@__cattri_defined_methods, original.dup)
43
+
44
+ result = context.defined_methods
45
+
46
+ expect(result).to eq(original)
47
+ expect(result).not_to equal(original)
48
+ end
49
+ end
50
+
51
+ describe "#defer_definitions?" do
52
+ subject(:result) { described_class.new(target).defer_definitions? }
53
+
54
+ context "when the target is a Module but NOT a Class" do
55
+ let(:target) { Module.new }
56
+ it { is_expected.to be(true) }
57
+ end
58
+
59
+ context "when the target is a Class" do
60
+ let(:target) { Class.new }
61
+ it { is_expected.to be(false) }
62
+ end
63
+ end
64
+
65
+ describe "#ensure_deferred_support!" do
66
+ let(:context_target) { Module.new }
67
+
68
+ subject(:call) { context.ensure_deferred_support! }
69
+
70
+ context "when the target already extends Cattri::DeferredAttributes" do
71
+ let(:context_target) do
72
+ Class.new do
73
+ include Cattri::DeferredAttributes
74
+ end
75
+ end
76
+
77
+ it "returns immediately without calling #extend again" do
78
+ expect(context_target).not_to receive(:extend).with(Cattri::DeferredAttributes)
79
+ call
80
+ end
81
+ end
82
+
83
+ context "when the target has not yet extended Cattri::DeferredAttributes" do
84
+ it "extends the target with Cattri::DeferredAttributes" do
85
+ expect(context_target).to receive(:extend).with(Cattri::DeferredAttributes).and_call_original
86
+ call
87
+ expect(context_target.singleton_class.ancestors).to include(Cattri::DeferredAttributes)
88
+ end
89
+ end
90
+ end
91
+
92
+ describe "#attribute_lookup_sources" do
93
+ let(:mod_a) { Module.new }
94
+ let(:mod_b) { Module.new }
95
+ let(:sc_double) { instance_double(Class, included_modules: [mod_b, mod_a]) }
96
+
97
+ subject(:sources) { context.attribute_lookup_sources }
98
+
99
+ before do
100
+ allow(context_target).to receive(:ancestors).and_return([context_target, mod_a, Object])
101
+ allow(context_target).to receive(:singleton_class).and_return(sc_double)
102
+ end
103
+
104
+ it "returns an ordered, duplicate-free list of sources" do
105
+ expect(sources).to eq([context_target, mod_a, Object, mod_b])
106
+ expect(sources.size).to eq(sources.uniq.size)
107
+ end
108
+
109
+ it "starts with the target itself" do
110
+ expect(sources.first).to equal(context_target)
111
+ end
112
+ end
113
+
114
+ describe "#define_method" do
115
+ let(:attribute_name) { class_attribute.name }
116
+ let(:impl) { proc { :something } }
117
+ let(:target) { context.send(:target_for, class_attribute) }
118
+
119
+ def stub_method_defined?(flag, attribute)
120
+ allow(context).to receive(:method_defined?)
121
+ .with(attribute, name: attribute.name).and_return(flag)
122
+ end
123
+
124
+ context "when the method does not exist" do
125
+ before { stub_method_defined?(false, class_attribute) }
126
+
127
+ it "delegates to #define_method!" do
128
+ expect(context).to receive(:define_method!)
129
+ .with(target, class_attribute, attribute_name, &impl)
130
+
131
+ context.define_method(class_attribute, &impl)
132
+ end
133
+ end
134
+
135
+ context "when the method already exists" do
136
+ before { stub_method_defined?(true, class_attribute) }
137
+
138
+ it "raises Cattri::AttributeError" do
139
+ expect(context).not_to receive(:define_method!)
140
+
141
+ expect do
142
+ context.define_method(class_attribute, &impl)
143
+ end.to raise_error(Cattri::AttributeError, /Method `:#{attribute_name}` already defined on #{target}/)
144
+ end
145
+ end
146
+ end
147
+
148
+ describe "#method_defined?" do
149
+ let(:attribute_name) { class_attribute.name }
150
+
151
+ subject(:result) { context.method_defined?(class_attribute) }
152
+
153
+ context "when the method is defined directly on the target class" do
154
+ before do
155
+ context.send(:target_for, class_attribute)
156
+ .define_method(attribute_name) { "cattri" }
157
+ end
158
+
159
+ it { is_expected.to be(true) }
160
+ end
161
+
162
+ context "when the method is NOT on the class but is tracked internally" do
163
+ before { context.send(:__cattri_defined_methods)[attribute_name] << attribute_name }
164
+ it { is_expected.to be(true) }
165
+ end
166
+
167
+ context "when the method is neither on the class nor tracked" do
168
+ it { is_expected.to be(false) }
169
+ end
170
+
171
+ context "when a different :name argument is supplied" do
172
+ before do
173
+ context.send(:target_for, class_attribute)
174
+ .define_method(attribute_name) { "cattri" }
175
+ end
176
+
177
+ it "checks the explicitly-passed name, not the attribute’s name" do
178
+ expect(context.method_defined?(class_attribute, name: attribute_name)).to be(true)
179
+ expect(context.method_defined?(class_attribute, name: :foo)).to be(false)
180
+ end
181
+ end
182
+ end
183
+
184
+ describe "#__cattri_defined_methods" do
185
+ it "returns a Hash" do
186
+ expect(context.send(:__cattri_defined_methods)).to be_a(Hash)
187
+ end
188
+
189
+ it "uses a default block to assign empty arrays" do
190
+ hash = context.send(:__cattri_defined_methods)
191
+ hash[:new_key] << :value
192
+
193
+ expect(hash[:new_key].to_a).to eq([:value])
194
+ end
195
+
196
+ it "memoizes the result" do
197
+ first_call = context.send(:__cattri_defined_methods)
198
+ second_call = context.send(:__cattri_defined_methods)
199
+
200
+ expect(first_call).to equal(second_call)
201
+ end
202
+ end
203
+
204
+ describe "#target_for" do
205
+ subject(:result) { context.send(:target_for, attribute) }
206
+
207
+ context "when a class-level attribute is provided" do
208
+ let(:attribute) { class_attribute }
209
+
210
+ it "returns the singleton_class" do
211
+ expect(result).to eq(context_target.singleton_class)
212
+ end
213
+ end
214
+
215
+ context "when a instance-level attribute is provided" do
216
+ let(:attribute) { instance_attribute }
217
+
218
+ it "returns the context's target" do
219
+ expect(result).to eq(context_target)
220
+ end
221
+ end
222
+ end
223
+
224
+ describe "#define_method!" do
225
+ let(:target) { context.send(:target_for, class_attribute) }
226
+ let(:attribute_name) { class_attribute.name }
227
+ let(:implementation) { proc { :impl } }
228
+
229
+ context "when method definition succeeds" do
230
+ before { allow(context).to receive(:apply_visibility!) }
231
+
232
+ it "defines the method, tracks it, and applies access" do
233
+ context.send(:define_method!, target, class_attribute, attribute_name, &implementation)
234
+
235
+ expect(target.instance_method(attribute_name)).to be_a(UnboundMethod)
236
+ expect(context_target.send(attribute_name)).to eq(:impl)
237
+
238
+ defined_set = context.send(:__cattri_defined_methods)[attribute_name]
239
+ expect(defined_set).to include(attribute_name)
240
+
241
+ expect(context).to have_received(:apply_visibility!)
242
+ .with(target, attribute_name, class_attribute)
243
+ end
244
+ end
245
+
246
+ context "when `class_eval` raises an error" do
247
+ before do
248
+ allow(target).to receive(:class_eval).and_raise(StandardError, "boom")
249
+ end
250
+
251
+ it "wraps the error in AttributeError" do
252
+ expect do
253
+ context.send(:define_method!, target, class_attribute, attribute_name, &implementation)
254
+ end.to raise_error(Cattri::AttributeError, /#{attribute_name}/)
255
+
256
+ defined_set = context.send(:__cattri_defined_methods)[attribute_name]
257
+ expect(defined_set).to be_empty
258
+ end
259
+ end
260
+ end
261
+
262
+ describe "#apply_visibility!" do
263
+ let(:target) { context.send(:target_for, attribute) }
264
+ let(:unbound_method) { instance_double(UnboundMethod) }
265
+ let(:unbound) { instance_double(Method) }
266
+ let(:name) { :foo }
267
+
268
+ subject(:call) { context.send(:apply_visibility!, target, name, attribute) }
269
+
270
+ context "when attribute.access == :public" do
271
+ let(:attribute) { define_attribute(:test_cattr, "test", scope: :class) }
272
+
273
+ it "returns immediately" do
274
+ call
275
+
276
+ expect(context).not_to receive(:resolve_access)
277
+ expect(context).not_to receive(:target_for)
278
+ expect(Module).not_to receive(:instance_method)
279
+ end
280
+ end
281
+
282
+ context "when attribute.access == :protected" do
283
+ let(:attribute) do
284
+ define_attribute(:test_cattr, "test", scope: :class, visibility: :protected)
285
+ end
286
+
287
+ it "applies protected visibility to the attribute method definition" do
288
+ expect(Module).to receive(:instance_method).with(:protected).and_return(unbound_method)
289
+ expect(unbound_method).to receive(:bind).with(target).and_return(unbound)
290
+ expect(unbound).to receive(:call).with(name)
291
+
292
+ call
293
+ end
294
+ end
295
+ end
296
+
297
+ describe "#effective_visibility" do
298
+ before do
299
+ stub_const("Cattri::AttributeOptions", Module.new)
300
+ allow(Cattri::AttributeOptions).to receive(:validate_visibility!).and_return(:explicit)
301
+ end
302
+
303
+ it "returns :protected for internal class-level methods" do
304
+ attr = double("Attribute", class_attribute?: true, visibility: nil)
305
+ allow(context).to receive(:internal_method?).with(attr, :foo).and_return(true)
306
+
307
+ expect(context.send(:effective_visibility, attr, :foo)).to eq(:protected)
308
+ end
309
+
310
+ it "returns :private for internal instance-level methods" do
311
+ attr = double("Attribute", class_attribute?: false, visibility: nil)
312
+ allow(context).to receive(:internal_method?).with(attr, :foo).and_return(true)
313
+
314
+ expect(context.send(:effective_visibility, attr, :foo)).to eq(:private)
315
+ end
316
+
317
+ it "returns declared visibility for public method" do
318
+ attr = double("Attribute", class_attribute?: false, visibility: :protected)
319
+ allow(context).to receive(:internal_method?).with(attr, :foo).and_return(false)
320
+
321
+ expect(context.send(:effective_visibility, attr, :foo)).to eq(:explicit)
322
+ end
323
+ end
324
+
325
+ describe "#internal_method?" do
326
+ it "returns true for writer when attribute has no public reader" do
327
+ attr = double("Attribute", internal_writer?: true, internal_reader?: false)
328
+ expect(context.send(:internal_method?, attr, :foo=)).to be true
329
+ end
330
+
331
+ it "returns false for writer when attribute has public reader" do
332
+ attr = double("Attribute", internal_writer?: false, internal_reader?: false)
333
+ expect(context.send(:internal_method?, attr, :foo=)).to be false
334
+ end
335
+
336
+ it "returns true for reader when attribute has no public writer" do
337
+ attr = double("Attribute", internal_writer?: false, internal_reader?: true)
338
+ expect(context.send(:internal_method?, attr, :foo)).to be true
339
+ end
340
+
341
+ it "returns false for reader when attribute has public writer" do
342
+ attr = double("Attribute", internal_writer?: false, internal_reader?: false)
343
+ expect(context.send(:internal_method?, attr, :foo)).to be false
344
+ end
345
+ end
346
+ end