attr_pouch 0.0.1

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.
@@ -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: