attr_pouch 0.0.1 → 0.1.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/Gemfile.lock +1 -1
- data/README.md +82 -23
- data/TODO +3 -1
- data/lib/attr_pouch/errors.rb +2 -0
- data/lib/attr_pouch/version.rb +1 -1
- data/lib/attr_pouch.rb +107 -61
- data/spec/attr_pouch_spec.rb +468 -399
- data/spec/spec_helper.rb +9 -2
- metadata +2 -2
data/spec/attr_pouch_spec.rb
CHANGED
@@ -1,482 +1,551 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe AttrPouch do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
4
|
+
%i[hstore json jsonb].each do |storage_kind|
|
5
|
+
|
6
|
+
context "with a #{storage_kind} backing column" do
|
7
|
+
|
8
|
+
let(:column_name) { "attrs_#{storage_kind}".to_sym }
|
9
|
+
let(:storage_wrapper) do
|
10
|
+
case storage_kind
|
11
|
+
when :hstore
|
12
|
+
->(hash) { Sequel.hstore(hash) }
|
13
|
+
when :json, :jsonb
|
14
|
+
->(hash) { Sequel.pg_json(hash) }
|
15
|
+
else
|
16
|
+
raise ArgumentError, "Unknown kind, #{kind}"
|
17
|
+
end
|
10
18
|
end
|
11
|
-
end.create
|
12
|
-
end
|
13
19
|
|
14
|
-
|
15
|
-
|
20
|
+
def make_pouchy(field_name, opts={})
|
21
|
+
col_name = column_name
|
22
|
+
Class.new(Sequel::Model(:items)) do
|
23
|
+
include AttrPouch
|
16
24
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
25
|
+
pouch(col_name) do
|
26
|
+
field field_name, opts
|
27
|
+
end
|
28
|
+
end.create
|
29
|
+
end
|
21
30
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
pouchy.reload
|
27
|
-
expect(pouchy.foo).to eq('bar')
|
28
|
-
end
|
31
|
+
def wrap_hash(hash)
|
32
|
+
hash = Hash[hash.map { |k,v| [ k.to_s, v ] }]
|
33
|
+
storage_wrapper.call(hash)
|
34
|
+
end
|
29
35
|
|
30
|
-
|
31
|
-
|
32
|
-
result = pouchy.save_changes
|
33
|
-
expect(result).to_not be_nil
|
34
|
-
pouchy.reload
|
35
|
-
expect(pouchy.foo).to eq('bar')
|
36
|
-
end
|
36
|
+
context "with a simple attribute" do
|
37
|
+
let(:pouchy) { make_pouchy(:foo, type: String) }
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
expect(pouchy.save_changes).to be_nil
|
43
|
-
end
|
39
|
+
it "generates getter and setter" do
|
40
|
+
pouchy.foo = 'bar'
|
41
|
+
expect(pouchy.foo).to eq('bar')
|
42
|
+
end
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
it "clears on reload" do
|
45
|
+
pouchy.update(foo: 'bar')
|
46
|
+
expect(pouchy.foo).to eq('bar')
|
47
|
+
pouchy.foo = 'baz'
|
48
|
+
pouchy.reload
|
49
|
+
expect(pouchy.foo).to eq('bar')
|
50
|
+
end
|
48
51
|
|
49
|
-
|
50
|
-
|
52
|
+
it "marks the field as modified" do
|
53
|
+
pouchy.foo = 'bar'
|
54
|
+
result = pouchy.save_changes
|
55
|
+
expect(result).to_not be_nil
|
56
|
+
pouchy.reload
|
57
|
+
expect(pouchy.foo).to eq('bar')
|
58
|
+
end
|
51
59
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
60
|
+
it "avoids marking the field as modified if it is not changing" do
|
61
|
+
pouchy.foo = 'bar'
|
62
|
+
expect(pouchy.save_changes).to_not be_nil
|
63
|
+
pouchy.foo = 'bar'
|
64
|
+
expect(pouchy.save_changes).to be_nil
|
56
65
|
end
|
57
|
-
end
|
58
66
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
expect(pouchy.f1).to be_nil
|
63
|
-
end
|
67
|
+
it "requires the attribute to be present if read" do
|
68
|
+
expect { pouchy.foo }.to raise_error(AttrPouch::MissingRequiredFieldError)
|
69
|
+
end
|
64
70
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
71
|
+
context "with nil values" do
|
72
|
+
let(:pouchy) { make_pouchy(:f1, type: :nil_hater) }
|
73
|
+
|
74
|
+
before do
|
75
|
+
AttrPouch.configure do |config|
|
76
|
+
config.encode(:nil_hater) { |f,v| v.nil? ? (raise ArgumentError) : v }
|
77
|
+
config.decode(:nil_hater) { |f,v| v.nil? ? (raise ArgumentError) : v }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it "bypasses encoding" do
|
82
|
+
pouchy.update(f1: 'foo')
|
83
|
+
expect { pouchy.update(f1: nil) }.not_to raise_error
|
84
|
+
expect(pouchy.f1).to be_nil
|
85
|
+
end
|
86
|
+
|
87
|
+
it "bypasses decoding" do
|
88
|
+
pouchy.update(column_name => wrap_hash(f1: nil))
|
89
|
+
expect { pouchy.f1 }.not_to raise_error
|
90
|
+
expect(pouchy.f1).to be_nil
|
91
|
+
end
|
92
|
+
|
93
|
+
it "still records the value as nil if not present when writing" do
|
94
|
+
pouchy.update(f1: nil)
|
95
|
+
expect(pouchy[column_name]).to have_key('f1')
|
96
|
+
expect(pouchy[column_name].fetch('f1')).to be_nil
|
97
|
+
end
|
98
|
+
end
|
69
99
|
end
|
70
100
|
|
71
|
-
|
72
|
-
pouchy
|
73
|
-
expect(pouchy.attrs).to have_key(:f1)
|
74
|
-
expect(pouchy.attrs[:f1]).to be_nil
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
context "with an integer attribute" do
|
80
|
-
let(:pouchy) { make_pouchy(:foo, type: Integer) }
|
81
|
-
|
82
|
-
it "preserves the type" do
|
83
|
-
pouchy.update(foo: 42)
|
84
|
-
pouchy.reload
|
85
|
-
expect(pouchy.foo).to eq(42)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
context "with a float attribute" do
|
90
|
-
let(:pouchy) { make_pouchy(:foo, type: Float) }
|
91
|
-
|
92
|
-
it "preserves the type" do
|
93
|
-
pouchy.update(foo: 2.78)
|
94
|
-
pouchy.reload
|
95
|
-
expect(pouchy.foo).to eq(2.78)
|
96
|
-
end
|
97
|
-
end
|
101
|
+
context "with an integer attribute" do
|
102
|
+
let(:pouchy) { make_pouchy(:foo, type: Integer) }
|
98
103
|
|
99
|
-
|
100
|
-
|
104
|
+
it "preserves the type" do
|
105
|
+
pouchy.update(foo: 42)
|
106
|
+
pouchy.reload
|
107
|
+
expect(pouchy.foo).to eq(42)
|
108
|
+
end
|
109
|
+
end
|
101
110
|
|
102
|
-
|
103
|
-
|
104
|
-
pouchy.reload
|
105
|
-
expect(pouchy.foo).to be true
|
106
|
-
end
|
107
|
-
end
|
111
|
+
context "with a float attribute" do
|
112
|
+
let(:pouchy) { make_pouchy(:foo, type: Float) }
|
108
113
|
|
109
|
-
|
110
|
-
|
114
|
+
it "preserves the type" do
|
115
|
+
pouchy.update(foo: 2.78)
|
116
|
+
pouchy.reload
|
117
|
+
expect(pouchy.foo).to eq(2.78)
|
118
|
+
end
|
119
|
+
end
|
111
120
|
|
112
|
-
|
113
|
-
|
114
|
-
pouchy.update(foo: now)
|
115
|
-
pouchy.reload
|
116
|
-
expect(pouchy.foo).to eq(now)
|
117
|
-
end
|
118
|
-
end
|
121
|
+
context "with a boolean attribute" do
|
122
|
+
let(:pouchy) { make_pouchy(:foo, type: :bool) }
|
119
123
|
|
120
|
-
|
121
|
-
|
122
|
-
|
124
|
+
it "preserves the type" do
|
125
|
+
pouchy.update(foo: true)
|
126
|
+
pouchy.reload
|
127
|
+
expect(pouchy.foo).to be true
|
128
|
+
end
|
129
|
+
end
|
123
130
|
|
124
|
-
|
125
|
-
|
126
|
-
pouchy.update(foo: new_model)
|
127
|
-
pouchy.reload
|
128
|
-
expect(pouchy.foo).to be_a(model_class)
|
129
|
-
expect(pouchy.foo.id).to eq(new_model.id)
|
130
|
-
end
|
131
|
-
end
|
131
|
+
context "with a Time attribute" do
|
132
|
+
let(:pouchy) { make_pouchy(:foo, type: Time) }
|
132
133
|
|
133
|
-
|
134
|
-
|
135
|
-
|
134
|
+
it "preserves the type" do
|
135
|
+
now = Time.now
|
136
|
+
pouchy.update(foo: now)
|
137
|
+
pouchy.reload
|
138
|
+
expect(pouchy.foo).to eq(now)
|
139
|
+
end
|
140
|
+
end
|
136
141
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
pouchy.reload
|
141
|
-
expect(pouchy.foo).to be_a(model_class)
|
142
|
-
expect(pouchy.foo.id).to eq(new_model.id)
|
143
|
-
end
|
144
|
-
end
|
142
|
+
context "with a Sequel::Model attribute" do
|
143
|
+
let(:model_class) { Class.new(Sequel::Model(:items)) }
|
144
|
+
let(:pouchy) { make_pouchy(:foo, type: model_class) }
|
145
145
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
146
|
+
it "preserves the type" do
|
147
|
+
new_model = model_class.create
|
148
|
+
pouchy.update(foo: new_model)
|
149
|
+
pouchy.reload
|
150
|
+
expect(pouchy.foo).to be_a(model_class)
|
151
|
+
expect(pouchy.foo.id).to eq(new_model.id)
|
152
|
+
end
|
153
|
+
end
|
153
154
|
|
154
|
-
|
155
|
-
|
155
|
+
context "with a Sequel::Model attribute provided as a String" do
|
156
|
+
let(:model_class) do
|
157
|
+
module A; class B < Sequel::Model(:items); end; end; A::B
|
158
|
+
end
|
159
|
+
let(:pouchy) { make_pouchy(:foo, type: model_class.name) }
|
160
|
+
|
161
|
+
it "preserves the type" do
|
162
|
+
new_model = model_class.create
|
163
|
+
pouchy.update(foo: new_model)
|
164
|
+
pouchy.reload
|
165
|
+
expect(pouchy.foo).to be_a(model_class)
|
166
|
+
expect(pouchy.foo.id).to eq(new_model.id)
|
167
|
+
end
|
168
|
+
end
|
156
169
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
170
|
+
context "with an attribute that is not a simple method name" do
|
171
|
+
it "raises an error when defining the class" do
|
172
|
+
expect do
|
173
|
+
make_pouchy(:"nope, not valid", type: String)
|
174
|
+
end.to raise_error(AttrPouch::InvalidFieldError)
|
175
|
+
end
|
176
|
+
end
|
161
177
|
|
162
|
-
|
163
|
-
|
164
|
-
expect(pouchy.foo?).to be true
|
165
|
-
end
|
166
|
-
end
|
178
|
+
context "with an attribute name that ends in a question mark" do
|
179
|
+
let(:pouchy) { make_pouchy(:foo?, type: :bool) }
|
167
180
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
181
|
+
it "generates normal getter" do
|
182
|
+
pouchy[column_name] = wrap_hash(foo?: true)
|
183
|
+
expect(pouchy.foo?).to be true
|
184
|
+
end
|
172
185
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
field :f3, type: Integer
|
186
|
+
it "generates setter by stripping trailing question mark" do
|
187
|
+
pouchy.foo = true
|
188
|
+
expect(pouchy.foo?).to be true
|
177
189
|
end
|
178
190
|
end
|
179
|
-
end
|
180
|
-
let(:pouchy) { bepouched.create }
|
181
191
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
expect(pouchy.f2).to eq(true)
|
186
|
-
expect(pouchy.f3).to eq(42)
|
187
|
-
end
|
192
|
+
context "with multiple attributes" do
|
193
|
+
let(:bepouched) do
|
194
|
+
col_name = column_name
|
188
195
|
|
189
|
-
|
190
|
-
|
191
|
-
pouchy.f2 = true
|
192
|
-
pouchy.f3 = 42
|
193
|
-
pouchy.save_changes
|
194
|
-
pouchy.reload
|
195
|
-
expect(pouchy.f1).to eq('hello')
|
196
|
-
expect(pouchy.f2).to eq(true)
|
197
|
-
expect(pouchy.f3).to eq(42)
|
198
|
-
end
|
199
|
-
end
|
196
|
+
Class.new(Sequel::Model(:items)) do
|
197
|
+
include AttrPouch
|
200
198
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
pouchy.update(foo: 'goodbye')
|
210
|
-
expect(pouchy.foo).to eq('goodbye')
|
211
|
-
end
|
199
|
+
pouch(col_name) do
|
200
|
+
field :f1, type: String
|
201
|
+
field :f2, type: :bool
|
202
|
+
field :f3, type: Integer
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
let(:pouchy) { bepouched.create }
|
212
207
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
208
|
+
it "allows updating multiple attributes simultaneously" do
|
209
|
+
pouchy.update(f1: 'hello', f2: true, f3: 42)
|
210
|
+
expect(pouchy.f1).to eq('hello')
|
211
|
+
expect(pouchy.f2).to eq(true)
|
212
|
+
expect(pouchy.f3).to eq(42)
|
213
|
+
end
|
217
214
|
|
218
|
-
|
219
|
-
|
215
|
+
it "allows updating multiple attributes sequentially" do
|
216
|
+
pouchy.f1 = 'hello'
|
217
|
+
pouchy.f2 = true
|
218
|
+
pouchy.f3 = 42
|
219
|
+
pouchy.save_changes
|
220
|
+
pouchy.reload
|
221
|
+
expect(pouchy.f1).to eq('hello')
|
222
|
+
expect(pouchy.f2).to eq(true)
|
223
|
+
expect(pouchy.f3).to eq(42)
|
224
|
+
end
|
220
225
|
end
|
221
226
|
|
222
|
-
|
223
|
-
pouchy
|
224
|
-
expect(pouchy.foo).to eq('goodbye')
|
225
|
-
pouchy.delete_foo
|
226
|
-
expect(pouchy.foo).to eq('hello')
|
227
|
-
end
|
228
|
-
end
|
229
|
-
end
|
227
|
+
context "with the default option" do
|
228
|
+
let(:pouchy) { make_pouchy(:foo, type: String, default: 'hello') }
|
230
229
|
|
231
|
-
|
232
|
-
|
230
|
+
it "returns the default if the key is absent" do
|
231
|
+
expect(pouchy.foo).to eq('hello')
|
232
|
+
end
|
233
233
|
|
234
|
-
|
235
|
-
|
236
|
-
|
234
|
+
it "returns the value if the key is present" do
|
235
|
+
pouchy.update(foo: 'goodbye')
|
236
|
+
expect(pouchy.foo).to eq('goodbye')
|
237
|
+
end
|
237
238
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
239
|
+
context "with the deletable option" do
|
240
|
+
let(:pouchy) { make_pouchy(:foo, type: String,
|
241
|
+
default: 'hello',
|
242
|
+
deletable: true) }
|
243
|
+
|
244
|
+
it "it returns the default if the key is absent" do
|
245
|
+
expect(pouchy.foo).to eq('hello')
|
246
|
+
end
|
247
|
+
|
248
|
+
it "it returns the default after the field has been deleted" do
|
249
|
+
pouchy.update(foo: 'goodbye')
|
250
|
+
expect(pouchy.foo).to eq('goodbye')
|
251
|
+
pouchy.delete_foo
|
252
|
+
expect(pouchy.foo).to eq('hello')
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
246
256
|
|
247
|
-
|
248
|
-
|
249
|
-
expect(pouchy.foo).to eq(42)
|
250
|
-
pouchy.delete_foo!
|
251
|
-
expect(pouchy.attrs).not_to have_key(:foo)
|
252
|
-
pouchy.reload
|
253
|
-
expect(pouchy.attrs).not_to have_key(:foo)
|
254
|
-
end
|
257
|
+
context "with the deletable option" do
|
258
|
+
let(:pouchy) { make_pouchy(:foo, type: Integer, deletable: true) }
|
255
259
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
expect(pouchy.attrs).not_to have_key(:foo)
|
260
|
-
end
|
260
|
+
it "is nil if the field is absent" do
|
261
|
+
expect(pouchy.foo).to be_nil
|
262
|
+
end
|
261
263
|
|
262
|
-
|
263
|
-
|
264
|
+
it "supports deleting existing fields" do
|
265
|
+
pouchy.update(foo: 42)
|
266
|
+
expect(pouchy.foo).to eq(42)
|
267
|
+
pouchy.delete_foo
|
268
|
+
expect(pouchy[column_name]).not_to have_key(:foo)
|
269
|
+
pouchy.reload
|
270
|
+
expect(pouchy.foo).to eq(42)
|
271
|
+
end
|
264
272
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
273
|
+
it "supports deleting existing fields and immediately persisting changes" do
|
274
|
+
pouchy.update(foo: 42)
|
275
|
+
expect(pouchy.foo).to eq(42)
|
276
|
+
pouchy.delete_foo!
|
277
|
+
expect(pouchy[column_name]).not_to have_key(:foo)
|
278
|
+
pouchy.reload
|
279
|
+
expect(pouchy[column_name]).not_to have_key(:foo)
|
280
|
+
end
|
271
281
|
|
272
|
-
|
273
|
-
|
282
|
+
it "ignores deleting absent fields" do
|
283
|
+
expect(pouchy[column_name]).not_to have_key(:foo)
|
284
|
+
pouchy.delete_foo
|
285
|
+
expect(pouchy[column_name]).not_to have_key(:foo)
|
286
|
+
end
|
274
287
|
|
275
|
-
|
276
|
-
|
277
|
-
end
|
288
|
+
it "also deletes aliases from the was option" do
|
289
|
+
pouchy = make_pouchy(:foo, type: Integer, deletable: true, was: :bar)
|
278
290
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
end
|
291
|
+
pouchy.update(column_name => wrap_hash(bar: 42))
|
292
|
+
expect(pouchy.foo).to eq(42)
|
293
|
+
pouchy.delete_foo
|
294
|
+
expect(pouchy[column_name]).not_to have_key(:bar)
|
295
|
+
end
|
296
|
+
end
|
286
297
|
|
287
|
-
|
288
|
-
|
298
|
+
context "with the mutable option" do
|
299
|
+
let(:pouchy) { make_pouchy(:foo, type: Integer, mutable: false) }
|
289
300
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
end
|
301
|
+
it "it allows setting the field value for the first time" do
|
302
|
+
pouchy.update(foo: 42)
|
303
|
+
end
|
294
304
|
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
305
|
+
it "forbids subsequent modifications to the field" do
|
306
|
+
pouchy.update(foo: 42)
|
307
|
+
expect do
|
308
|
+
pouchy.update(foo: 43)
|
309
|
+
end.to raise_error(AttrPouch::ImmutableFieldUpdateError)
|
310
|
+
end
|
311
|
+
end
|
299
312
|
|
300
|
-
|
301
|
-
|
302
|
-
pouchy.update(foo: 'goodbye')
|
303
|
-
expect(pouchy.attrs).not_to have_key(:bar)
|
304
|
-
end
|
313
|
+
context "with the was option" do
|
314
|
+
let(:pouchy) { make_pouchy(:foo, type: String, was: %w(bar baz)) }
|
305
315
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
end
|
311
|
-
end
|
316
|
+
it "supports aliases for renaming fields" do
|
317
|
+
pouchy.update(column_name => wrap_hash(bar: 'hello'))
|
318
|
+
expect(pouchy.foo).to eq('hello')
|
319
|
+
end
|
312
320
|
|
313
|
-
|
314
|
-
|
321
|
+
it "supports multiple aliases" do
|
322
|
+
pouchy.update(column_name => wrap_hash(baz: 'hello'))
|
323
|
+
expect(pouchy.foo).to eq('hello')
|
324
|
+
end
|
315
325
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
326
|
+
it "deletes old names when writing the current one" do
|
327
|
+
pouchy.update(column_name => wrap_hash(bar: 'hello'))
|
328
|
+
pouchy.update(foo: 'goodbye')
|
329
|
+
expect(pouchy[column_name]).not_to have_key(:bar)
|
330
|
+
end
|
320
331
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
332
|
+
it "supports a shorthand for the single-alias case" do
|
333
|
+
pouchy = make_pouchy(:foo, type: String, was: :bar)
|
334
|
+
pouchy.update(column_name => wrap_hash(bar: 'hello'))
|
335
|
+
expect(pouchy.foo).to eq('hello')
|
336
|
+
end
|
337
|
+
end
|
326
338
|
|
327
|
-
|
328
|
-
|
329
|
-
expect(pouchy.save_changes).to_not be_nil
|
330
|
-
pouchy.raw_foo = 'bar'
|
331
|
-
expect(pouchy.save_changes).to be_nil
|
332
|
-
end
|
339
|
+
context "with the raw_field option" do
|
340
|
+
let(:pouchy) { make_pouchy(:foo, type: Float, raw_field: :raw_foo) }
|
333
341
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
pouchy.update(foo: 43)
|
339
|
-
end.to raise_error(AttrPouch::ImmutableFieldUpdateError)
|
340
|
-
end
|
342
|
+
it "supports direct access to the encoded value" do
|
343
|
+
pouchy.update(foo: 2.78)
|
344
|
+
expect(pouchy.raw_foo).to eq('2.78')
|
345
|
+
end
|
341
346
|
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
347
|
+
it "is required when read" do
|
348
|
+
expect do
|
349
|
+
pouchy.raw_foo
|
350
|
+
end.to raise_error(AttrPouch::MissingRequiredFieldError)
|
351
|
+
end
|
346
352
|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
353
|
+
it "avoids marking the field as modified if it is not changing" do
|
354
|
+
pouchy.raw_foo = 'bar'
|
355
|
+
expect(pouchy.save_changes).to_not be_nil
|
356
|
+
pouchy.raw_foo = 'bar'
|
357
|
+
expect(pouchy.save_changes).to be_nil
|
358
|
+
end
|
352
359
|
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
+
it "obeys the 'mutable' option" do
|
361
|
+
pouchy = make_pouchy(:foo, type: Float,
|
362
|
+
raw_field: :raw_foo,
|
363
|
+
mutable: false)
|
364
|
+
pouchy.update(foo: 42)
|
365
|
+
expect do
|
366
|
+
pouchy.update(foo: 43)
|
367
|
+
end.to raise_error(AttrPouch::ImmutableFieldUpdateError)
|
368
|
+
end
|
360
369
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
expect(pouchy.num_foo).to eq(42)
|
366
|
-
end
|
370
|
+
it "is nil when the 'default' option is present" do
|
371
|
+
pouchy = make_pouchy(:foo, type: Float, raw_field: :raw_foo, default: 7.2)
|
372
|
+
expect(pouchy.raw_foo).to be_nil
|
373
|
+
end
|
367
374
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
375
|
+
it "obeys the 'was' option when reading" do
|
376
|
+
pouchy = make_pouchy(:foo, type: String, raw_field: :raw_foo, was: :bar)
|
377
|
+
pouchy[column_name] = wrap_hash(bar: 'hello')
|
378
|
+
expect(pouchy.raw_foo).to eq('hello')
|
379
|
+
end
|
373
380
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
381
|
+
it "obeys the 'was' option when writing" do
|
382
|
+
pouchy = make_pouchy(:foo, type: String, raw_field: :raw_foo, was: :bar)
|
383
|
+
pouchy[column_name] = wrap_hash(bar: 'hello')
|
384
|
+
pouchy.update(raw_foo: 'goodbye')
|
385
|
+
expect(pouchy[column_name]).not_to have_key(:bar)
|
386
|
+
end
|
387
|
+
end
|
379
388
|
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
389
|
+
context "inferring field types" do
|
390
|
+
it "infers field named num_foo to be of type Integer" do
|
391
|
+
pouchy = make_pouchy(:num_foo)
|
392
|
+
pouchy.update(num_foo: 42)
|
393
|
+
expect(pouchy.num_foo).to eq(42)
|
394
|
+
end
|
385
395
|
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
end
|
396
|
+
it "infers field named foo_count to be of type Integer" do
|
397
|
+
pouchy = make_pouchy(:foo_count)
|
398
|
+
pouchy.update(foo_count: 42)
|
399
|
+
expect(pouchy.foo_count).to eq(42)
|
400
|
+
end
|
392
401
|
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
end
|
402
|
+
it "infers field named foo_size to be of type Integer" do
|
403
|
+
pouchy = make_pouchy(:foo_size)
|
404
|
+
pouchy.update(foo_size: 42)
|
405
|
+
expect(pouchy.foo_size).to eq(42)
|
406
|
+
end
|
399
407
|
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
end
|
408
|
+
it "infers field named foo? to be of type :bool" do
|
409
|
+
pouchy = make_pouchy(:foo?)
|
410
|
+
pouchy.update(foo: true)
|
411
|
+
expect(pouchy.foo?).to be true
|
412
|
+
end
|
406
413
|
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
field :f1, type: String
|
414
|
-
field :f2, type: String
|
415
|
-
field :f3, type: String
|
416
|
-
field :f4, type: :rot13
|
417
|
-
end
|
418
|
-
end
|
419
|
-
end
|
414
|
+
it "infers field named foo_at to be of type Time" do
|
415
|
+
now = Time.now
|
416
|
+
pouchy = make_pouchy(:foo_at)
|
417
|
+
pouchy.update(foo_at: now)
|
418
|
+
expect(pouchy.foo_at).to eq(now)
|
419
|
+
end
|
420
420
|
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
when ('z'.ord - 13)..'z'.ord
|
427
|
-
c - 13
|
421
|
+
it "infers field named foo_by to be of type Time" do
|
422
|
+
now = Time.now
|
423
|
+
pouchy = make_pouchy(:foo_by)
|
424
|
+
pouchy.update(foo_by: now)
|
425
|
+
expect(pouchy.foo_by).to eq(now)
|
428
426
|
end
|
429
|
-
end.map(&:chr).join
|
430
|
-
end
|
431
427
|
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
428
|
+
it "infers field named foo to be of type String" do
|
429
|
+
pouchy = make_pouchy(:foo)
|
430
|
+
pouchy.update(foo: 'hello')
|
431
|
+
expect(pouchy.foo).to eq('hello')
|
432
|
+
end
|
436
433
|
end
|
437
|
-
end
|
438
434
|
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
matching = bepouched.where_pouch(:attrs, f1: 'foo').all
|
443
|
-
expect(matching.count).to eq(1)
|
444
|
-
match = matching.first
|
445
|
-
expect(match.id).to eq(pouchy.id)
|
446
|
-
end
|
435
|
+
context "with dataset methods" do
|
436
|
+
let(:bepouched) do
|
437
|
+
col_name = column_name
|
447
438
|
|
448
|
-
|
449
|
-
|
450
|
-
p2 = bepouched.create(f1: 'bar', f2: 'foo', f3: 'baz')
|
451
|
-
bepouched.create(f1: 'baz', f2: 'foo', f3: 'bar') # *not* matching
|
452
|
-
matching = bepouched.where_pouch(:attrs, f1: %w(foo bar)).all
|
453
|
-
expect(matching.count).to eq(2)
|
454
|
-
expect(matching.map(&:id)).to include(p1.id, p2.id)
|
455
|
-
end
|
439
|
+
Class.new(Sequel::Model(:items)) do
|
440
|
+
include AttrPouch
|
456
441
|
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
442
|
+
pouch(col_name) do
|
443
|
+
field :f1, type: String
|
444
|
+
field :f2, type: String
|
445
|
+
field :f3, type: String
|
446
|
+
field :f4, type: :rot13
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
465
450
|
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
451
|
+
def rot13(str)
|
452
|
+
str.each_byte.map do |c|
|
453
|
+
case c
|
454
|
+
when 'a'.ord..('z'.ord - 13)
|
455
|
+
c + 13
|
456
|
+
when ('z'.ord - 13)..'z'.ord
|
457
|
+
c - 13
|
458
|
+
end
|
459
|
+
end.map(&:chr).join
|
460
|
+
end
|
472
461
|
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
462
|
+
before do
|
463
|
+
AttrPouch.configure do |config|
|
464
|
+
config.encode(:rot13) { |f,v| rot13(v.to_s) }
|
465
|
+
config.decode(:rot13) { |f,v| rot13(v) }
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
if storage_kind == :json
|
470
|
+
it "does not support dataset methods" do
|
471
|
+
pouchy = bepouched.create(f1: 'foo', f2: 'bar', f3: 'baz')
|
472
|
+
bepouched.create(f1: 'bar', f2: 'foo', f3: 'baz') # *not* matching
|
473
|
+
expect do
|
474
|
+
bepouched.where_pouch(column_name, f1: 'foo').all
|
475
|
+
end.to raise_error(::AttrPouch::UnsupportedError)
|
476
|
+
end
|
477
|
+
else
|
478
|
+
it "finds the right item with a scalar field value" do
|
479
|
+
pouchy = bepouched.create(f1: 'foo', f2: 'bar', f3: 'baz')
|
480
|
+
bepouched.create(f1: 'bar', f2: 'foo', f3: 'baz') # *not* matching
|
481
|
+
matching = bepouched.where_pouch(column_name, f1: 'foo').all
|
482
|
+
expect(matching.count).to eq(1)
|
483
|
+
match = matching.first
|
484
|
+
expect(match.id).to eq(pouchy.id)
|
485
|
+
end
|
486
|
+
|
487
|
+
it "finds the right item with an array field value" do
|
488
|
+
p1 = bepouched.create(f1: 'foo', f2: 'bar', f3: 'baz')
|
489
|
+
p2 = bepouched.create(f1: 'bar', f2: 'foo', f3: 'baz')
|
490
|
+
bepouched.create(f1: 'baz', f2: 'foo', f3: 'bar') # *not* matching
|
491
|
+
matching = bepouched.where_pouch(column_name, f1: %w(foo bar)).all
|
492
|
+
expect(matching.count).to eq(2)
|
493
|
+
expect(matching.map(&:id)).to include(p1.id, p2.id)
|
494
|
+
end
|
495
|
+
|
496
|
+
it "finds the right item with a missing field value" do
|
497
|
+
p1 = bepouched.create(f2: 'bar', f3: 'baz')
|
498
|
+
bepouched.create(f1: '', f2: 'foo', f3: 'baz') # *not* matching
|
499
|
+
bepouched.create(f1: 'baz', f2: 'foo', f3: 'bar') # *not* matching
|
500
|
+
matching = bepouched.where_pouch(column_name, f1: nil).all
|
501
|
+
expect(matching.count).to eq(1)
|
502
|
+
expect(matching.first.id).to eq(p1.id)
|
503
|
+
end
|
504
|
+
|
505
|
+
it "finds the right item with a nil field value" do
|
506
|
+
p1 = bepouched.create(column_name => wrap_hash(f1: nil))
|
507
|
+
matching = bepouched.where_pouch(column_name, f1: nil).all
|
508
|
+
expect(matching.count).to eq(1)
|
509
|
+
expect(matching.first.id).to eq(p1.id)
|
510
|
+
end
|
511
|
+
|
512
|
+
it "uses the associated encoder for lookups" do
|
513
|
+
encoded = rot13('hello')
|
514
|
+
p1 = bepouched.create(f4: 'hello')
|
515
|
+
expect(p1[column_name]['f4']).to eq(encoded) # nothing behind the curtain
|
516
|
+
matching = bepouched.where_pouch(column_name, f4: 'hello')
|
517
|
+
expect(matching.count).to eq(1)
|
518
|
+
expect(matching.first.id).to eq(p1.id)
|
519
|
+
end
|
520
|
+
|
521
|
+
context "using indexes" do
|
522
|
+
before do
|
523
|
+
bepouched.create(column_name => wrap_hash(f1: nil))
|
524
|
+
end
|
525
|
+
|
526
|
+
def plan_when_looking_for(values)
|
527
|
+
stmt = bepouched.where_pouch(column_name, f1: values).sql
|
528
|
+
db = bepouched.db
|
529
|
+
db.transaction do
|
530
|
+
db.run("SET LOCAL enable_seqscan = false")
|
531
|
+
db.fetch("EXPLAIN #{stmt}").all
|
532
|
+
end.map { |line| line.fetch(:"QUERY PLAN") }.join("\n")
|
533
|
+
end
|
534
|
+
|
535
|
+
it "uses index when looking for a single value" do
|
536
|
+
expect(plan_when_looking_for('hello')).to match(/index/i)
|
537
|
+
end
|
538
|
+
|
539
|
+
it "uses index when looking for multiple values" do
|
540
|
+
expect(plan_when_looking_for(%(hello world))).to match(/index/i)
|
541
|
+
end
|
542
|
+
|
543
|
+
xit "uses index when looking for a nil value" do
|
544
|
+
expect(plan_when_looking_for(nil)).to match(/index/i)
|
545
|
+
end
|
546
|
+
end
|
547
|
+
end
|
548
|
+
end
|
480
549
|
end
|
481
550
|
end
|
482
551
|
end
|