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.
@@ -1,482 +1,551 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe AttrPouch do
4
- def make_pouchy(field_name, opts={})
5
- Class.new(Sequel::Model(:items)) do
6
- include AttrPouch
7
-
8
- pouch(:attrs) do
9
- field field_name, opts
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
- context "with a simple attribute" do
15
- let(:pouchy) { make_pouchy(:foo, type: String) }
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
- it "generates getter and setter" do
18
- pouchy.foo = 'bar'
19
- expect(pouchy.foo).to eq('bar')
20
- end
25
+ pouch(col_name) do
26
+ field field_name, opts
27
+ end
28
+ end.create
29
+ end
21
30
 
22
- it "clears on reload" do
23
- pouchy.update(foo: 'bar')
24
- expect(pouchy.foo).to eq('bar')
25
- pouchy.foo = 'baz'
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
- it "marks the field as modified" do
31
- pouchy.foo = 'bar'
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
- it "avoids marking the field as modified if it is not changing" do
39
- pouchy.foo = 'bar'
40
- expect(pouchy.save_changes).to_not be_nil
41
- pouchy.foo = 'bar'
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
- it "requires the attribute to be present if read" do
46
- expect { pouchy.foo }.to raise_error(AttrPouch::MissingRequiredFieldError)
47
- end
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
- context "with nil values" do
50
- let(:pouchy) { make_pouchy(:f1, type: :nil_hater) }
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
- before do
53
- AttrPouch.configure do |config|
54
- config.encode(:nil_hater) { |f,v| v.nil? ? (raise ArgumentError) : v }
55
- config.decode(:nil_hater) { |f,v| v.nil? ? (raise ArgumentError) : v }
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
- it "bypasses encoding" do
60
- pouchy.update(f1: 'foo')
61
- expect { pouchy.update(f1: nil) }.not_to raise_error
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
- it "bypasses decoding" do
66
- pouchy.update(attrs: Sequel.hstore(f1: nil))
67
- expect { pouchy.f1 }.not_to raise_error
68
- expect(pouchy.f1).to be_nil
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
- it "still records the value as nil if not present when writing" do
72
- pouchy.update(f1: nil)
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
- context "with a boolean attribute" do
100
- let(:pouchy) { make_pouchy(:foo, type: :bool) }
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
- it "preserves the type" do
103
- pouchy.update(foo: true)
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
- context "with a Time attribute" do
110
- let(:pouchy) { make_pouchy(:foo, type: Time) }
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
- it "preserves the type" do
113
- now = Time.now
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
- context "with a Sequel::Model attribute" do
121
- let(:model_class) { Class.new(Sequel::Model(:items)) }
122
- let(:pouchy) { make_pouchy(:foo, type: model_class) }
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
- it "preserves the type" do
125
- new_model = model_class.create
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
- context "with a Sequel::Model attribute provided as a String" do
134
- let(:model_class) { module A; class B < Sequel::Model(:items); end; end; A::B }
135
- let(:pouchy) { make_pouchy(:foo, type: model_class.name) }
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
- it "preserves the type" do
138
- new_model = model_class.create
139
- pouchy.update(foo: new_model)
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
- context "with an attribute that is not a simple method name" do
147
- it "raises an error when defining the class" do
148
- expect do
149
- make_pouchy(:"nope, not valid", type: String)
150
- end.to raise_error(AttrPouch::InvalidFieldError)
151
- end
152
- end
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
- context "with an attribute name that ends in a question mark" do
155
- let(:pouchy) { make_pouchy(:foo?, type: :bool) }
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
- it "generates normal getter" do
158
- pouchy.attrs = Sequel.hstore(foo?: true)
159
- expect(pouchy.foo?).to be true
160
- end
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
- it "generates setter by stripping trailing question mark" do
163
- pouchy.foo = true
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
- context "with multiple attributes" do
169
- let(:bepouched) do
170
- Class.new(Sequel::Model(:items)) do
171
- include AttrPouch
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
- pouch(:attrs) do
174
- field :f1, type: String
175
- field :f2, type: :bool
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
- it "allows updating multiple attributes simultaneously" do
183
- pouchy.update(f1: 'hello', f2: true, f3: 42)
184
- expect(pouchy.f1).to eq('hello')
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
- it "allows updating multiple attributes sequentially" do
190
- pouchy.f1 = 'hello'
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
- context "with the default option" do
202
- let(:pouchy) { make_pouchy(:foo, type: String, default: 'hello') }
203
-
204
- it "returns the default if the key is absent" do
205
- expect(pouchy.foo).to eq('hello')
206
- end
207
-
208
- it "returns the value if the key is present" do
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
- context "with the deletable option" do
214
- let(:pouchy) { make_pouchy(:foo, type: String,
215
- default: 'hello',
216
- deletable: true) }
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
- it "it returns the default if the key is absent" do
219
- expect(pouchy.foo).to eq('hello')
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
- it "it returns the default after the field has been deleted" do
223
- pouchy.update(foo: 'goodbye')
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
- context "with the deletable option" do
232
- let(:pouchy) { make_pouchy(:foo, type: Integer, deletable: true) }
230
+ it "returns the default if the key is absent" do
231
+ expect(pouchy.foo).to eq('hello')
232
+ end
233
233
 
234
- it "is nil if the field is absent" do
235
- expect(pouchy.foo).to be_nil
236
- end
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
- it "supports deleting existing fields" do
239
- pouchy.update(foo: 42)
240
- expect(pouchy.foo).to eq(42)
241
- pouchy.delete_foo
242
- expect(pouchy.attrs).not_to have_key(:foo)
243
- pouchy.reload
244
- expect(pouchy.foo).to eq(42)
245
- end
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
- it "supports deleting existing fields and immediately persisting changes" do
248
- pouchy.update(foo: 42)
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
- it "ignores deleting absent fields" do
257
- expect(pouchy.attrs).not_to have_key(:foo)
258
- pouchy.delete_foo
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
- it "also deletes aliases from the was option" do
263
- pouchy = make_pouchy(:foo, type: Integer, deletable: true, was: :bar)
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
- pouchy.update(attrs: Sequel.hstore(bar: 42))
266
- expect(pouchy.foo).to eq(42)
267
- pouchy.delete_foo
268
- expect(pouchy.attrs).not_to have_key(:bar)
269
- end
270
- end
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
- context "with the mutable option" do
273
- let(:pouchy) { make_pouchy(:foo, type: Integer, mutable: false) }
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
- it "it allows setting the field value for the first time" do
276
- pouchy.update(foo: 42)
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
- it "forbids subsequent modifications to the field" do
280
- pouchy.update(foo: 42)
281
- expect do
282
- pouchy.update(foo: 43)
283
- end.to raise_error(AttrPouch::ImmutableFieldUpdateError)
284
- end
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
- context "with the was option" do
288
- let(:pouchy) { make_pouchy(:foo, type: String, was: %w(bar baz)) }
298
+ context "with the mutable option" do
299
+ let(:pouchy) { make_pouchy(:foo, type: Integer, mutable: false) }
289
300
 
290
- it "supports aliases for renaming fields" do
291
- pouchy.update(attrs: Sequel.hstore(bar: 'hello'))
292
- expect(pouchy.foo).to eq('hello')
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
- it "supports multiple aliases" do
296
- pouchy.update(attrs: Sequel.hstore(baz: 'hello'))
297
- expect(pouchy.foo).to eq('hello')
298
- end
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
- it "deletes old names when writing the current one" do
301
- pouchy.update(attrs: Sequel.hstore(bar: 'hello'))
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
- it "supports a shorthand for the single-alias case" do
307
- pouchy = make_pouchy(:foo, type: String, was: :bar)
308
- pouchy.update(attrs: Sequel.hstore(bar: 'hello'))
309
- expect(pouchy.foo).to eq('hello')
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
- context "with the raw_field option" do
314
- let(:pouchy) { make_pouchy(:foo, type: Float, raw_field: :raw_foo) }
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
- it "supports direct access to the encoded value" do
317
- pouchy.update(foo: 2.78)
318
- expect(pouchy.raw_foo).to eq('2.78')
319
- end
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
- it "is required when read" do
322
- expect do
323
- pouchy.raw_foo
324
- end.to raise_error(AttrPouch::MissingRequiredFieldError)
325
- end
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
- it "avoids marking the field as modified if it is not changing" do
328
- pouchy.raw_foo = 'bar'
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
- it "obeys the 'mutable' option" do
335
- pouchy = make_pouchy(:foo, type: Float, raw_field: :raw_foo, mutable: false)
336
- pouchy.update(foo: 42)
337
- expect do
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
- it "is nil when the 'default' option is present" do
343
- pouchy = make_pouchy(:foo, type: Float, raw_field: :raw_foo, default: 7.2)
344
- expect(pouchy.raw_foo).to be_nil
345
- end
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
- it "obeys the 'was' option when reading" do
348
- pouchy = make_pouchy(:foo, type: String, raw_field: :raw_foo, was: :bar)
349
- pouchy.attrs = Sequel.hstore(bar: 'hello')
350
- expect(pouchy.raw_foo).to eq('hello')
351
- end
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
- it "obeys the 'was' option when writing" do
354
- pouchy = make_pouchy(:foo, type: String, raw_field: :raw_foo, was: :bar)
355
- pouchy.attrs = Sequel.hstore(bar: 'hello')
356
- pouchy.update(raw_foo: 'goodbye')
357
- expect(pouchy.attrs).not_to have_key(:bar)
358
- end
359
- end
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
- context "inferring field types" do
362
- it "infers field named num_foo to be of type Integer" do
363
- pouchy = make_pouchy(:num_foo)
364
- pouchy.update(num_foo: 42)
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
- it "infers field named foo_count to be of type Integer" do
369
- pouchy = make_pouchy(:foo_count)
370
- pouchy.update(foo_count: 42)
371
- expect(pouchy.foo_count).to eq(42)
372
- end
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
- it "infers field named foo_size to be of type Integer" do
375
- pouchy = make_pouchy(:foo_size)
376
- pouchy.update(foo_size: 42)
377
- expect(pouchy.foo_size).to eq(42)
378
- end
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
- it "infers field named foo? to be of type :bool" do
381
- pouchy = make_pouchy(:foo?)
382
- pouchy.update(foo: true)
383
- expect(pouchy.foo?).to be true
384
- end
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
- it "infers field named foo_at to be of type Time" do
387
- now = Time.now
388
- pouchy = make_pouchy(:foo_at)
389
- pouchy.update(foo_at: now)
390
- expect(pouchy.foo_at).to eq(now)
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
- it "infers field named foo_by to be of type Time" do
394
- now = Time.now
395
- pouchy = make_pouchy(:foo_by)
396
- pouchy.update(foo_by: now)
397
- expect(pouchy.foo_by).to eq(now)
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
- it "infers field named foo to be of type String" do
401
- pouchy = make_pouchy(:foo)
402
- pouchy.update(foo: 'hello')
403
- expect(pouchy.foo).to eq('hello')
404
- end
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
- context "with dataset methods" do
408
- let(:bepouched) do
409
- Class.new(Sequel::Model(:items)) do
410
- include AttrPouch
411
-
412
- pouch(:attrs) do
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
- def rot13(str)
422
- str.each_byte.map do |c|
423
- case c
424
- when 'a'.ord..('z'.ord - 13)
425
- c + 13
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
- before do
433
- AttrPouch.configure do |config|
434
- config.encode(:rot13) { |f,v| rot13(v.to_s) }
435
- config.decode(:rot13) { |f,v| rot13(v) }
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
- it "finds the right item with a scalar field value" do
440
- pouchy = bepouched.create(f1: 'foo', f2: 'bar', f3: 'baz')
441
- bepouched.create(f1: 'bar', f2: 'foo', f3: 'baz') # *not* matching
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
- it "finds the right item with an array field value" do
449
- p1 = bepouched.create(f1: 'foo', f2: 'bar', f3: 'baz')
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
- it "finds the right item with a missing field value" do
458
- p1 = bepouched.create(f2: 'bar', f3: 'baz')
459
- bepouched.create(f1: '', f2: 'foo', f3: 'baz') # *not* matching
460
- bepouched.create(f1: 'baz', f2: 'foo', f3: 'bar') # *not* matching
461
- matching = bepouched.where_pouch(:attrs, f1: nil).all
462
- expect(matching.count).to eq(1)
463
- expect(matching.first.id).to eq(p1.id)
464
- end
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
- it "finds the right item with a nil field value" do
467
- p1 = bepouched.create(attrs: Sequel.hstore(f1: nil))
468
- matching = bepouched.where_pouch(:attrs, f1: nil).all
469
- expect(matching.count).to eq(1)
470
- expect(matching.first.id).to eq(p1.id)
471
- end
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
- it "uses the associated encoder for lookups" do
474
- encoded = rot13('hello')
475
- p1 = bepouched.create(f4: 'hello')
476
- expect(p1.attrs[:f4]).to eq(encoded) # nothing behind the curtain
477
- matching = bepouched.where_pouch(:attrs, f4: 'hello')
478
- expect(matching.count).to eq(1)
479
- expect(matching.first.id).to eq(p1.id)
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