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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +34 -0
- data/.gitignore +72 -0
- data/.rubocop.yml +6 -3
- data/CHANGELOG.md +41 -0
- data/Gemfile +12 -0
- data/README.md +163 -151
- 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 -155
- 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 -143
- data/lib/cattri/class_attributes.rb +0 -277
- data/lib/cattri/instance_attributes.rb +0 -276
- 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
|