attr_pouch 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,482 @@
1
+ require 'spec_helper'
2
+
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
10
+ end
11
+ end.create
12
+ end
13
+
14
+ context "with a simple attribute" do
15
+ let(:pouchy) { make_pouchy(:foo, type: String) }
16
+
17
+ it "generates getter and setter" do
18
+ pouchy.foo = 'bar'
19
+ expect(pouchy.foo).to eq('bar')
20
+ end
21
+
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
29
+
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
37
+
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
44
+
45
+ it "requires the attribute to be present if read" do
46
+ expect { pouchy.foo }.to raise_error(AttrPouch::MissingRequiredFieldError)
47
+ end
48
+
49
+ context "with nil values" do
50
+ let(:pouchy) { make_pouchy(:f1, type: :nil_hater) }
51
+
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 }
56
+ end
57
+ end
58
+
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
64
+
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
69
+ end
70
+
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
98
+
99
+ context "with a boolean attribute" do
100
+ let(:pouchy) { make_pouchy(:foo, type: :bool) }
101
+
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
108
+
109
+ context "with a Time attribute" do
110
+ let(:pouchy) { make_pouchy(:foo, type: Time) }
111
+
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
119
+
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) }
123
+
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
132
+
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) }
136
+
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
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
153
+
154
+ context "with an attribute name that ends in a question mark" do
155
+ let(:pouchy) { make_pouchy(:foo?, type: :bool) }
156
+
157
+ it "generates normal getter" do
158
+ pouchy.attrs = Sequel.hstore(foo?: true)
159
+ expect(pouchy.foo?).to be true
160
+ end
161
+
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
167
+
168
+ context "with multiple attributes" do
169
+ let(:bepouched) do
170
+ Class.new(Sequel::Model(:items)) do
171
+ include AttrPouch
172
+
173
+ pouch(:attrs) do
174
+ field :f1, type: String
175
+ field :f2, type: :bool
176
+ field :f3, type: Integer
177
+ end
178
+ end
179
+ end
180
+ let(:pouchy) { bepouched.create }
181
+
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
188
+
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
200
+
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
212
+
213
+ context "with the deletable option" do
214
+ let(:pouchy) { make_pouchy(:foo, type: String,
215
+ default: 'hello',
216
+ deletable: true) }
217
+
218
+ it "it returns the default if the key is absent" do
219
+ expect(pouchy.foo).to eq('hello')
220
+ end
221
+
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
230
+
231
+ context "with the deletable option" do
232
+ let(:pouchy) { make_pouchy(:foo, type: Integer, deletable: true) }
233
+
234
+ it "is nil if the field is absent" do
235
+ expect(pouchy.foo).to be_nil
236
+ end
237
+
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
246
+
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
255
+
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
261
+
262
+ it "also deletes aliases from the was option" do
263
+ pouchy = make_pouchy(:foo, type: Integer, deletable: true, was: :bar)
264
+
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
271
+
272
+ context "with the mutable option" do
273
+ let(:pouchy) { make_pouchy(:foo, type: Integer, mutable: false) }
274
+
275
+ it "it allows setting the field value for the first time" do
276
+ pouchy.update(foo: 42)
277
+ end
278
+
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
286
+
287
+ context "with the was option" do
288
+ let(:pouchy) { make_pouchy(:foo, type: String, was: %w(bar baz)) }
289
+
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
294
+
295
+ it "supports multiple aliases" do
296
+ pouchy.update(attrs: Sequel.hstore(baz: 'hello'))
297
+ expect(pouchy.foo).to eq('hello')
298
+ end
299
+
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
305
+
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
312
+
313
+ context "with the raw_field option" do
314
+ let(:pouchy) { make_pouchy(:foo, type: Float, raw_field: :raw_foo) }
315
+
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
320
+
321
+ it "is required when read" do
322
+ expect do
323
+ pouchy.raw_foo
324
+ end.to raise_error(AttrPouch::MissingRequiredFieldError)
325
+ end
326
+
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
333
+
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
341
+
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
346
+
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
352
+
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
+
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
367
+
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
373
+
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
379
+
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
385
+
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
392
+
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
399
+
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
406
+
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
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
428
+ end
429
+ end.map(&:chr).join
430
+ end
431
+
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) }
436
+ end
437
+ end
438
+
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
447
+
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
456
+
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
465
+
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
472
+
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)
480
+ end
481
+ end
482
+ end
@@ -0,0 +1,44 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+
8
+ require 'bundler'
9
+ require 'attr_pouch'
10
+
11
+ conn = Sequel.connect(ENV['DATABASE_URL'])
12
+ conn.run 'CREATE EXTENSION IF NOT EXISTS "hstore"'
13
+
14
+ conn.extension :pg_hstore
15
+ Sequel.extension :pg_hstore_ops
16
+
17
+ conn.run 'CREATE EXTENSION IF NOT EXISTS "hstore"'
18
+ conn.run 'DROP TABLE IF EXISTS items'
19
+ conn.run <<-EOF
20
+ CREATE TABLE items(
21
+ id serial primary key,
22
+ attrs hstore default ''
23
+ )
24
+ EOF
25
+
26
+ RSpec.configure do |config|
27
+ config.run_all_when_everything_filtered = true
28
+ config.filter_run :focus
29
+
30
+ config.before(:example) do
31
+ conn.run 'TRUNCATE items'
32
+ end
33
+
34
+ # Run specs in random order to surface order dependencies. If you find an
35
+ # order dependency and want to debug it, you can fix the order by providing
36
+ # the seed, which is printed after each run.
37
+ # --seed 1234
38
+ config.order = 'random'
39
+
40
+ config.expect_with :rspec do |c|
41
+ c.syntax = :expect
42
+ end
43
+ end
44
+
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attr_pouch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Maciek Sakrejda
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-11-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.18.3
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.18.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: sequel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.13'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.13'
55
+ description: Schema-less attribute storage
56
+ email:
57
+ - m.sakrejda@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".travis.yml"
63
+ - Gemfile
64
+ - Gemfile.lock
65
+ - LICENSE
66
+ - README.md
67
+ - TODO
68
+ - attr_pouch.gemspec
69
+ - lib/attr_pouch.rb
70
+ - lib/attr_pouch/errors.rb
71
+ - lib/attr_pouch/version.rb
72
+ - spec/attr_pouch_spec.rb
73
+ - spec/spec_helper.rb
74
+ homepage: https://github.com/uhoh-itsmaciek/attr_pouch
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.4.5.1
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Sequel plugin for schema-less attribute storage
98
+ test_files:
99
+ - spec/attr_pouch_spec.rb
100
+ - spec/spec_helper.rb
101
+ has_rdoc: