translated_collection 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 +15 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.travis.yml +15 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +69 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +10 -0
- data/config/database.yml +37 -0
- data/config/database.yml.example +17 -0
- data/gemfiles/rails40.gemfile +8 -0
- data/gemfiles/rails40.gemfile.lock +69 -0
- data/gemfiles/rails41.gemfile +7 -0
- data/gemfiles/rails41.gemfile.lock +69 -0
- data/gemfiles/rails42.gemfile +7 -0
- data/gemfiles/rails42.gemfile.lock +69 -0
- data/lib/translated_collection/version.rb +3 -0
- data/lib/translated_collection/wrapper.rb +206 -0
- data/lib/translated_collection.rb +7 -0
- data/script/shell +43 -0
- data/spec/lib/translated_collection_wrapper_spec.rb +439 -0
- data/spec/spec_helper.rb +20 -0
- data/translated_collection.gemspec +31 -0
- metadata +187 -0
data/script/shell
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require 'yaml'
|
6
|
+
require 'active_record'
|
7
|
+
configs = YAML.load(File.read("config/database.yml.example"))
|
8
|
+
ActiveRecord::Base.establish_connection(configs['development'])
|
9
|
+
|
10
|
+
require 'translated_collection'
|
11
|
+
|
12
|
+
def conn
|
13
|
+
ActiveRecord::Base.connection
|
14
|
+
end
|
15
|
+
|
16
|
+
def upperfn
|
17
|
+
lambda {|elt| elt.upcase }
|
18
|
+
end
|
19
|
+
|
20
|
+
def lowerfn
|
21
|
+
lambda {|elt| elt.downcase }
|
22
|
+
end
|
23
|
+
|
24
|
+
def collection
|
25
|
+
%w[a b c d e f g h i j k l]
|
26
|
+
end
|
27
|
+
|
28
|
+
def wrapone
|
29
|
+
TranslatedCollection::Wrapper.new(collection, lowerfn, upperfn)
|
30
|
+
end
|
31
|
+
|
32
|
+
def wrapped
|
33
|
+
@wrapped ||= wrapone
|
34
|
+
end
|
35
|
+
|
36
|
+
begin
|
37
|
+
require 'pry'
|
38
|
+
Pry.start
|
39
|
+
rescue LoadError
|
40
|
+
require 'irb'
|
41
|
+
IRB.start
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,439 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'translated_collection/wrapper'
|
3
|
+
|
4
|
+
describe TranslatedCollection::Wrapper do
|
5
|
+
let :upperfn do
|
6
|
+
lambda {|elt| elt.upcase }
|
7
|
+
end
|
8
|
+
|
9
|
+
let :lowerfn do
|
10
|
+
lambda {|elt| elt.downcase }
|
11
|
+
end
|
12
|
+
|
13
|
+
subject { TranslatedCollection::Wrapper.new(collection, lowerfn, upperfn) }
|
14
|
+
|
15
|
+
let :collection do
|
16
|
+
%w[a b c]
|
17
|
+
end
|
18
|
+
|
19
|
+
let :xlated_collection do
|
20
|
+
collection.map {|elt| upperfn.call(elt)}
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'creation' do
|
24
|
+
let :collection do
|
25
|
+
%w[a b C]
|
26
|
+
end
|
27
|
+
|
28
|
+
context '#new' do
|
29
|
+
it 'should assign the same collection internally' do
|
30
|
+
subject.instance_variable_get("@collection").__id__.should == collection.__id__
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should not check conformity by default' do
|
34
|
+
expect { subject }.not_to raise_error
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should raise on creation if conformity check requested and failed' do
|
38
|
+
expect { TranslatedCollection::Wrapper.new(collection, lowerfn, upperfn, true) }.
|
39
|
+
to raise_error(ArgumentError)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should make the same collection available via #collection' do
|
44
|
+
subject.collection.__id__.should == collection.__id__
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'validation' do
|
49
|
+
context '#_conforming?' do
|
50
|
+
it 'should be true for conforming collection' do
|
51
|
+
subject._conforming?.should be_true
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should be false for non-conforming collection' do
|
55
|
+
TranslatedCollection::Wrapper.new(%w[a B C], lowerfn, upperfn)._conforming?.should be_false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context '#_make_conforming!' do
|
60
|
+
it 'should leave a conforming collection unaltered' do
|
61
|
+
oldcoll = subject.collection.dup
|
62
|
+
subject._make_conforming!
|
63
|
+
subject.collection.should == oldcoll
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should make a non-conforming collection conform' do
|
67
|
+
tcw = TranslatedCollection::Wrapper.new(%w[a B C], lowerfn, upperfn)
|
68
|
+
tcw._make_conforming!
|
69
|
+
tcw.collection.should == %w[a b c]
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should preserve collection type' do
|
73
|
+
tcw = TranslatedCollection::Wrapper.new(Set.new(%w[a B C]), lowerfn, upperfn)
|
74
|
+
tcw._make_conforming!
|
75
|
+
tcw.collection.should == Set.new(%w[a b c])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'reading' do
|
81
|
+
context '#[]' do
|
82
|
+
it 'should return xlated element at index' do
|
83
|
+
subject[0].should == 'A'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context '#fetch' do
|
88
|
+
it 'should return xlated element on hit' do
|
89
|
+
subject.fetch(1).should == 'B'
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should return xlated default element on miss' do
|
93
|
+
subject.fetch(99, 'x').should == 'X'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context '#each' do
|
98
|
+
it 'should yield each element in order' do
|
99
|
+
res = []
|
100
|
+
subject.each {|elt| res << elt}
|
101
|
+
res.should == xlated_collection
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
context '#map' do
|
107
|
+
it 'should yield each element in order, and return all' do
|
108
|
+
subject.map {|elt| elt+elt}.to_a.should == %w[AA BB CC]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context '#include?' do
|
113
|
+
it 'should indicate that the translated form is present' do
|
114
|
+
subject.should include('A')
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'should indicate that the un-translated form is NOT present' do
|
118
|
+
subject.should_not include('a')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'updating and returning a new collection' do
|
126
|
+
context '#reject' do
|
127
|
+
let :postreject do
|
128
|
+
subject.reject {|x| x.to_i % 2 == 0 }
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'should remove elements from new collection' do
|
132
|
+
postreject.to_a.should_not == subject.to_a
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'should return a new collection' do
|
136
|
+
postreject.__id__.should_not == subject.__id__
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'should not alter old collection' do
|
140
|
+
expect { postreject }.not_to change { subject.to_a }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
context 'destructive updates' do
|
146
|
+
context '#clear' do
|
147
|
+
it 'should return same class on clear' do
|
148
|
+
subject.clear.should be_a(described_class)
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'should be empty after clear' do
|
152
|
+
expect { subject.clear }.not_to change { subject.__id__ }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context '#reject!' do
|
157
|
+
let :postreject do
|
158
|
+
subject.reject! {|x| x.to_i % 2 == 0 }
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'should remove elements from original' do
|
162
|
+
expect { postreject }.to change { subject.to_a }
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'should not return a new collection' do
|
166
|
+
postreject.__id__.should == subject.__id__
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'should remove the correct elements' do
|
170
|
+
postreject.to_a.should == []
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'should return nil if no changes were made' do
|
174
|
+
subject.reject! {|x| x == Object.new }.should be_nil
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
context 'Enumerable' do
|
180
|
+
let :collection do
|
181
|
+
%w[a b c d e f g h i j k l]
|
182
|
+
end
|
183
|
+
|
184
|
+
context 'methods with optional block' do
|
185
|
+
context '#drop_while' do
|
186
|
+
it 'should invoke condition block on translated-out values' do
|
187
|
+
subject.drop_while {|x| x < 'C'}.to_a.should == %w[C D E F G H I J K L]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
context '#map' do
|
192
|
+
it 'should invoke map block on translated-out values' do
|
193
|
+
subject.map {|x| x * 3}.to_a.should == %w[AAA BBB CCC DDD EEE FFF GGG HHH III JJJ KKK LLL]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
context '#collect' do
|
198
|
+
it 'should invoke collect block on translated-out values' do
|
199
|
+
subject.collect {|x| x * 3}.to_a.should == %w[AAA BBB CCC DDD EEE FFF GGG HHH III JJJ KKK LLL]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
context '#find_all' do
|
204
|
+
it 'should invoke condition block on translated-out values' do
|
205
|
+
subject.find_all {|x| x.to_i % 2 == 0 }.to_a.should == %w[A B C D E F G H I J K L]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
context '#sort' do
|
210
|
+
let :collection do
|
211
|
+
(-5..0).to_a.reverse
|
212
|
+
end
|
213
|
+
|
214
|
+
subject do
|
215
|
+
TranslatedCollection::Wrapper.new(collection,
|
216
|
+
Proc.new {|x| -(x.abs)},
|
217
|
+
Proc.new {|x| x.abs})
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'should return a wrapped array by default' do
|
221
|
+
subject.sort.should be_a_kind_of(TranslatedCollection::Wrapper)
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'should return a bare array if configured' do
|
225
|
+
subject.wrap_results = false
|
226
|
+
subject.sort.class.should == Array
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'should sort elements by comparison on translated-out elements' do
|
230
|
+
subject.sort.to_a.should == (0..5).to_a
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'should sort elements by comparison on translated-out elements' do
|
234
|
+
subject.sort.collection.should == (-5..0).to_a.reverse
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
context '#sort_by' do
|
239
|
+
let :collection do
|
240
|
+
(-5..0).to_a.reverse
|
241
|
+
end
|
242
|
+
|
243
|
+
subject do
|
244
|
+
TranslatedCollection::Wrapper.new(collection,
|
245
|
+
Proc.new {|x| x-1},
|
246
|
+
Proc.new {|x| x+1})
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'should sort elements by comparison on translated-out elements' do
|
250
|
+
subject.sort_by(&:abs).to_a.should == [0, 1, -1, -2, -3, -4]
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'should sort elements by comparison on translated-out elements' do
|
254
|
+
subject.sort_by(&:abs).collection.should == [-1, 0, -2, -3, -4, -5]
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
context '#take_while' do
|
259
|
+
it 'should evaluate condition on translated-out elements' do
|
260
|
+
subject.take_while {|x| x <= 'C'}.to_a.should == %w[A B C]
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'should internally store the non-translated elements' do
|
264
|
+
subject.take_while {|x| x <= 'C'}.collection.should == %w[a b c]
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
context 'copying' do
|
272
|
+
context '#clone' do
|
273
|
+
it 'should return a new instance of itself'do
|
274
|
+
subject.clone.__id__.should_not == subject.__id__
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'should wrap a copy of the collection' do
|
278
|
+
coll_id = subject.collection.__id__
|
279
|
+
subject.clone.collection.__id__.should_not == coll_id
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'should keep the translated-in versions of elements in the copy' do
|
283
|
+
subject.clone.collection.should == subject.collection
|
284
|
+
end
|
285
|
+
|
286
|
+
it 'should preserve frozen status' do
|
287
|
+
subject.freeze
|
288
|
+
copy = subject.clone
|
289
|
+
|
290
|
+
copy.should be_frozen
|
291
|
+
copy.collection.should be_frozen
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
|
296
|
+
context '#dup' do
|
297
|
+
it 'should return a new instance of itself'do
|
298
|
+
subject.dup.__id__.should_not == subject.__id__
|
299
|
+
end
|
300
|
+
|
301
|
+
it 'should wrap a copy of the collection' do
|
302
|
+
coll_id = subject.collection.__id__
|
303
|
+
subject.dup.collection.__id__.should_not == coll_id
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'should keep the translated-in versions of elements in the copy' do
|
307
|
+
subject.dup.collection.should == subject.collection
|
308
|
+
end
|
309
|
+
|
310
|
+
it 'should not preserve frozen status' do
|
311
|
+
subject.freeze
|
312
|
+
copy = subject.dup
|
313
|
+
|
314
|
+
copy.should_not be_frozen
|
315
|
+
copy.collection.should_not be_frozen
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
context 'introspection' do
|
321
|
+
it 'should claim to be an instance of proxied collection if Array' do
|
322
|
+
subject.should be_a_kind_of(Array)
|
323
|
+
subject.should_not be_a_kind_of(Set)
|
324
|
+
end
|
325
|
+
|
326
|
+
it 'should claim to be an instance of proxied collection if Set' do
|
327
|
+
wrapped = TranslatedCollection::Wrapper.new(Set.new(%w[a b c]), lowerfn, upperfn)
|
328
|
+
wrapped.should be_a_kind_of(Set)
|
329
|
+
wrapped.should_not be_a_kind_of(Array)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
context 'observation' do
|
334
|
+
let :state do
|
335
|
+
Struct.new(:count, :events) do
|
336
|
+
def observeit(object, event, *args)
|
337
|
+
self.count += 1
|
338
|
+
self.events << event
|
339
|
+
end
|
340
|
+
end.new(0, [])
|
341
|
+
end
|
342
|
+
|
343
|
+
before do
|
344
|
+
subject.add_observer(state, :observeit)
|
345
|
+
end
|
346
|
+
|
347
|
+
context '#[]=' do
|
348
|
+
it 'should trigger update' do
|
349
|
+
expect { subject[5] = 'foo' }.
|
350
|
+
to change { state.count }.by(1)
|
351
|
+
end
|
352
|
+
|
353
|
+
it 'should add event to list' do
|
354
|
+
expect { subject[5] = 'foo' }.
|
355
|
+
to change { state.events }.from([]).to([:set])
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
context '#clear' do
|
360
|
+
it 'should trigger update' do
|
361
|
+
expect { subject.clear}.
|
362
|
+
to change { state.count }.by(1)
|
363
|
+
end
|
364
|
+
|
365
|
+
it 'should add event to list' do
|
366
|
+
expect { subject.clear }.
|
367
|
+
to change { state.events }.from([]).to([:clear])
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
context '#delete' do
|
372
|
+
it 'should trigger update' do
|
373
|
+
expect { subject.delete('a') }.
|
374
|
+
to change { state.count }.by(1)
|
375
|
+
end
|
376
|
+
|
377
|
+
it 'should add event to list' do
|
378
|
+
expect { subject.delete('a') }.
|
379
|
+
to change { state.events }.from([]).to([:delete])
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
context '#delete_at' do
|
384
|
+
it 'should trigger update' do
|
385
|
+
expect { subject.delete_at(0) }.
|
386
|
+
to change { state.count }.by(1)
|
387
|
+
end
|
388
|
+
|
389
|
+
it 'should add event to list' do
|
390
|
+
expect { subject.delete_at(0) }.
|
391
|
+
to change { state.events }.from([]).to([:delete])
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
context '#<<' do
|
396
|
+
it 'should trigger update' do
|
397
|
+
expect { subject << 'foo' }.
|
398
|
+
to change { state.count }.by(1)
|
399
|
+
end
|
400
|
+
|
401
|
+
it 'should add event to list' do
|
402
|
+
expect { subject << 'foo' }.
|
403
|
+
to change { state.events }.from([]).to([:push])
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
context '#push' do
|
408
|
+
it 'should trigger update' do
|
409
|
+
expect { subject.push 'foo' }.
|
410
|
+
to change { state.count }.by(1)
|
411
|
+
end
|
412
|
+
|
413
|
+
it 'should add event to list' do
|
414
|
+
expect { subject.push 'foo' }.
|
415
|
+
to change { state.events }.from([]).to([:push])
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
context '#pop' do
|
420
|
+
it 'should trigger update' do
|
421
|
+
expect { subject.pop }.
|
422
|
+
to change { state.count }.by(1)
|
423
|
+
end
|
424
|
+
|
425
|
+
it 'should add event to list' do
|
426
|
+
expect { subject.pop }.
|
427
|
+
to change { state.events }.from([]).to([:pop])
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
context '[]' do
|
432
|
+
it 'should not trigger update' do
|
433
|
+
expect { subject[0] }.
|
434
|
+
not_to change { state.count }.by(1)
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
if ENV['COVERAGE'] == 'true'
|
5
|
+
require 'simplecov'
|
6
|
+
SimpleCov.start
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'yaml'
|
10
|
+
require 'active_record'
|
11
|
+
configs = YAML.load(File.read("config/database.yml.example"))
|
12
|
+
ActiveRecord::Base.establish_connection(configs['test'])
|
13
|
+
|
14
|
+
require 'translated_collection'
|
15
|
+
|
16
|
+
begin
|
17
|
+
require 'pry'
|
18
|
+
rescue LoadError
|
19
|
+
#
|
20
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'translated_collection/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
# coding: utf-8
|
8
|
+
spec.name = "translated_collection"
|
9
|
+
spec.version = TranslatedCollection::VERSION
|
10
|
+
spec.authors = ["Robert Sanders"]
|
11
|
+
spec.email = ["robert@curioussquid.com"]
|
12
|
+
spec.summary = %q{Transparently wrap a collection with mapping functions applied at item insertion/retrieval.}
|
13
|
+
spec.description = %q{Utility class for (somewhat) transparently wrapping a collection with mapping functions applied as values are added to and read from the collection. Especially useful for ActiveRecord serialized or PG Array fields}
|
14
|
+
spec.homepage = "http://github.com/rsanders/translated_collection"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features|gemfiles|coverage)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
# for testing with ActiveRecord serialized and PG Array attributes
|
23
|
+
spec.add_development_dependency "activerecord", '~> 4.0', '>= 4.0.0'
|
24
|
+
spec.add_development_dependency "pg", '~> 0.17', '>= 0.17.1'
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
27
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
28
|
+
spec.add_development_dependency "rspec", "~> 2.14"
|
29
|
+
spec.add_development_dependency "simplecov", '~> 0.9'
|
30
|
+
spec.add_development_dependency "wwtd", '~> 0.7'
|
31
|
+
end
|