speaky_csv 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,338 @@
1
+ require 'spec_helper'
2
+ require 'support/active_record'
3
+
4
+ describe SpeakyCsv::ActiveRecordImport, :db do
5
+ let(:presenter_klass) { Class.new SpeakyCsv::Base }
6
+
7
+ let(:io) { StringIO.new }
8
+ subject { presenter_klass.new.active_record_importer io, Book }
9
+
10
+ def record
11
+ unless defined? @record
12
+ records = subject.to_a
13
+ expect(records.length).to be <= 1
14
+ @record = records.first
15
+ end
16
+
17
+ @record
18
+ end
19
+
20
+ context 'with fields' do
21
+ before do
22
+ presenter_klass.class_eval do
23
+ define_csv_fields do |d|
24
+ d.field :id, :name, :author, :_destroy
25
+ end
26
+ end
27
+ end
28
+
29
+ context 'and csv has a new record' do
30
+ let(:io) do
31
+ StringIO.new <<-CSV
32
+ id,name,author
33
+ ,Big Fiction,Sneed
34
+ CSV
35
+ end
36
+
37
+ it 'returns new record' do
38
+ expect(record.attributes).to include('name' => 'Big Fiction',
39
+ 'author' => 'Sneed')
40
+ expect(record).to be_new_record
41
+ end
42
+ end
43
+
44
+ context 'and csv has no changes' do
45
+ let!(:book) { Book.create! id: 1, name: 'Big Fiction', author: 'Sneed' }
46
+
47
+ let(:io) do
48
+ StringIO.new <<-CSV
49
+ id,name,author
50
+ 1,Big Fiction,Sneed
51
+ CSV
52
+ end
53
+
54
+ it 'returns clean record' do
55
+ expect(record.attributes).to include('id' => 1,
56
+ 'name' => 'Big Fiction',
57
+ 'author' => 'Sneed')
58
+ expect(record).to_not be_changed
59
+ end
60
+ end
61
+
62
+ context 'and csv has changes' do
63
+ let!(:book) { Book.create! id: 1, name: 'Big Fiction', author: 'Sneed' }
64
+
65
+ let(:io) do
66
+ StringIO.new <<-CSV
67
+ id,name,author
68
+ 1,Wee Little Fiction,Sneed
69
+ CSV
70
+ end
71
+
72
+ it 'returns dirty record' do
73
+ expect(record.name).to eq 'Wee Little Fiction'
74
+ expect(record).to be_changed # not saved yet
75
+ end
76
+ end
77
+
78
+ context 'and csv has a destroy' do
79
+ let!(:book) { Book.create! id: 1, name: 'Big Fiction', author: 'Sneed' }
80
+
81
+ let(:io) do
82
+ StringIO.new <<-CSV
83
+ id,_destroy
84
+ 1,true
85
+ CSV
86
+ end
87
+
88
+ it 'returns record marked for destruction' do
89
+ expect(record).to be_marked_for_destruction
90
+ end
91
+ end
92
+ end
93
+
94
+ context 'with custom primary_key' do
95
+ before do
96
+ presenter_klass.class_eval do
97
+ define_csv_fields do |d|
98
+ d.primary_key = :name
99
+ d.field :author
100
+ end
101
+ end
102
+ end
103
+
104
+ let!(:book) { Book.create! id: 1, name: 'Big Fiction', author: 'Sneed' }
105
+
106
+ let(:io) do
107
+ StringIO.new <<-CSV
108
+ name,author
109
+ Big Fiction,Sneed
110
+ CSV
111
+ end
112
+
113
+ it 'finds by primary key' do
114
+ expect(record).to eq book
115
+ end
116
+ end
117
+
118
+ context 'and no record with primary key exists' do
119
+ before do
120
+ presenter_klass.class_eval do
121
+ define_csv_fields do |d|
122
+ d.field :id
123
+ end
124
+ end
125
+ end
126
+
127
+ let(:io) do
128
+ StringIO.new <<-CSV
129
+ id,name,author
130
+ 1,Big Fiction,Sneed
131
+ CSV
132
+ end
133
+
134
+ it 'returns an error' do
135
+ expect(subject.to_a).to eq []
136
+ expect(subject.errors[:row_2]).to be_present
137
+ end
138
+ end
139
+
140
+ context 'when record doesnt have defined attribute' do
141
+ before do
142
+ presenter_klass.class_eval do
143
+ define_csv_fields do |d|
144
+ d.field :id, :whats_this
145
+ end
146
+ end
147
+ end
148
+
149
+ let!(:book) { Book.create! id: 1, name: 'Big Fiction', author: 'Sneed' }
150
+
151
+ let(:io) do
152
+ StringIO.new <<-CSV
153
+ id,whats_this
154
+ 1,unknown
155
+ CSV
156
+ end
157
+
158
+ it 'adds an error' do
159
+ expect(record).to eq book
160
+ expect(subject.errors[:row_2]).to be_present
161
+ end
162
+ end
163
+
164
+ context 'with has_many field' do
165
+ before do
166
+ presenter_klass.class_eval do
167
+ define_csv_fields do |d|
168
+ d.field :id
169
+ d.has_many 'reviews' do |r|
170
+ r.field :id, :tomatoes, :publication, :_destroy
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ let!(:book) { Book.create! id: 1 }
177
+
178
+ def actual_review
179
+ unless defined? @review
180
+ expect(record.reviews.length).to be <= 1
181
+ @review = record.reviews.first
182
+ end
183
+
184
+ @review
185
+ end
186
+
187
+ context 'and csv has new associated record' do
188
+ let(:io) do
189
+ StringIO.new <<-CSV
190
+ id
191
+ 1,review_0_tomatoes,99,review_0_publication,Post
192
+ CSV
193
+ end
194
+
195
+ it 'builds new record' do
196
+ expect(actual_review.attributes).to include('book_id' => 1,
197
+ 'tomatoes' => 99,
198
+ 'publication' => 'Post')
199
+ expect(actual_review).to be_new_record
200
+ end
201
+ end
202
+
203
+ context 'and csv has unchanged associated record' do
204
+ let(:io) do
205
+ StringIO.new <<-CSV
206
+ id
207
+ 1,review_0_id,1,review_0_tomatoes,99,review_0_publication,Post
208
+ CSV
209
+ end
210
+
211
+ let!(:review) { Review.create! id: 1, book: book, tomatoes: 99, publication: 'Post' }
212
+
213
+ it 'returns clean record' do
214
+ expect(actual_review).to_not be_changed
215
+ end
216
+ end
217
+
218
+ context 'and csv changes associated record' do
219
+ let(:io) do
220
+ StringIO.new <<-CSV
221
+ id
222
+ 1,review_0_id,1,review_0_tomatoes,80,review_0_publication,Post
223
+ CSV
224
+ end
225
+
226
+ let!(:review) { Review.create! id: 1, book: book, tomatoes: 99, publication: 'Post' }
227
+
228
+ it 'returns clean record' do
229
+ expect(actual_review.tomatoes).to eq 80
230
+ expect(actual_review).to be_changed
231
+ end
232
+ end
233
+ context 'and csv destroys associated record' do
234
+ let(:io) do
235
+ StringIO.new <<-CSV
236
+ id
237
+ 1,review_0_id,1,review_0__destroy,true
238
+ CSV
239
+ end
240
+
241
+ let!(:review) { Review.create! id: 1, book: book, tomatoes: 99, publication: 'Post' }
242
+
243
+ it 'marks record for destruction' do
244
+ expect(actual_review).to be_marked_for_destruction
245
+ end
246
+ end
247
+ end
248
+
249
+ it "should fail when all headers aren't to the left"
250
+ it 'should ignore undefined variable columns'
251
+ it 'should fail when variable columns not pair up correctly'
252
+
253
+ describe 'batch behavior' do
254
+ before do
255
+ presenter_klass.class_eval do
256
+ define_csv_fields do |d|
257
+ d.field :id, :name, :author, :_destroy
258
+ end
259
+ end
260
+ end
261
+
262
+ let!(:book1) { Book.create! id: 1, name: 'Big Fiction', author: 'Sneed' }
263
+ let!(:book2) { Book.create! id: 2, name: 'Big NonFiction', author: 'Sneed' }
264
+ let!(:book3) { Book.create! id: 3, name: 'The Last Goodbye', author: 'Sneed' }
265
+
266
+ let(:io) do
267
+ StringIO.new <<-CSV
268
+ id,name,author,_destroy
269
+ ,Natty,Sneed
270
+ 1,Big Fiction,Sneed
271
+ 2,Wee Little NonFiction,Sneed
272
+ 3,,,true
273
+ CSV
274
+ end
275
+
276
+ def expect_changes_to_be_correct
277
+ records = subject.to_a
278
+
279
+ aggregate_failures 'changes' do
280
+ expect(records.length).to eq 4
281
+ new_one = records.find(&:new_record?)
282
+ expect(new_one.attributes).to include('name' => 'Natty',
283
+ 'author' => 'Sneed')
284
+ book1 = records.find { |b| b.id == 1 }
285
+ expect(book1).to_not be_changed
286
+
287
+ book2 = records.find { |b| b.id == 2 }
288
+ expect(book2.attributes).to include('name' => 'Wee Little NonFiction',
289
+ 'author' => 'Sneed')
290
+ expect(book2).to be_changed
291
+
292
+ book3 = records.find { |b| b.id == 3 }
293
+ expect(book3).to be_marked_for_destruction
294
+ end
295
+ end
296
+
297
+ context 'when batch size is less than csv size' do
298
+ before do
299
+ stub_const 'SpeakyCsv::ActiveRecordImport::QUERY_BATCH_SIZE', 2
300
+ end
301
+
302
+ it 'works' do
303
+ expect_changes_to_be_correct
304
+ end
305
+ end
306
+
307
+ context 'when batch size is same as csv size' do
308
+ before do
309
+ stub_const 'SpeakyCsv::ActiveRecordImport::QUERY_BATCH_SIZE', 4
310
+ end
311
+
312
+ it 'works' do
313
+ expect_changes_to_be_correct
314
+ end
315
+ end
316
+
317
+ context 'when batch size is greater than as csv size' do
318
+ before do
319
+ stub_const 'SpeakyCsv::ActiveRecordImport::QUERY_BATCH_SIZE', 6
320
+ end
321
+
322
+ it 'works' do
323
+ expect_changes_to_be_correct
324
+ end
325
+ end
326
+ end
327
+
328
+ context 'when csv is invalid' do
329
+ before do
330
+ allow(CSV).to receive(:new).and_raise(CSV::MalformedCSVError)
331
+ end
332
+
333
+ it 'adds an error' do
334
+ expect(subject.to_a).to eq []
335
+ expect(subject.errors[:csv]).to be_present
336
+ end
337
+ end
338
+ end
@@ -0,0 +1,205 @@
1
+ require 'spec_helper'
2
+
3
+ describe SpeakyCsv::AttrImport do
4
+ let(:presenter_klass) { Class.new SpeakyCsv::Base }
5
+
6
+ let(:io) { StringIO.new }
7
+ subject { presenter_klass.new.attr_importer io }
8
+
9
+ context 'with fields' do
10
+ before do
11
+ presenter_klass.class_eval do
12
+ define_csv_fields do |d|
13
+ d.field 'name', 'author'
14
+ end
15
+ end
16
+ end
17
+
18
+ let(:io) do
19
+ StringIO.new <<-CSV
20
+ name,author
21
+ Big Fiction,Sneed
22
+ True story,Honest Abe
23
+ CSV
24
+ end
25
+
26
+ it 'should return a data structure' do
27
+ expect(subject.to_a).to eq([
28
+ { 'name' => 'Big Fiction', 'author' => 'Sneed' },
29
+ { 'name' => 'True story', 'author' => 'Honest Abe' }
30
+ ])
31
+ end
32
+ end
33
+
34
+ context 'with export_only field' do
35
+ before do
36
+ presenter_klass.class_eval do
37
+ define_csv_fields do |d|
38
+ d.field :id
39
+ d.field :name, export_only: true
40
+ end
41
+ end
42
+ end
43
+
44
+ let(:io) do
45
+ StringIO.new <<-CSV
46
+ id,name
47
+ 22,Big Fiction
48
+ CSV
49
+ end
50
+
51
+ it 'should exclude field' do
52
+ expect(subject.to_a).to eq([{'id' => '22'}])
53
+ end
54
+ end
55
+
56
+ context 'with unknown field' do
57
+ before do
58
+ presenter_klass.class_eval do
59
+ define_csv_fields do |d|
60
+ d.field 'name'
61
+ end
62
+ end
63
+ end
64
+
65
+ let(:io) do
66
+ StringIO.new <<-CSV
67
+ name,setting
68
+ Big Fiction,Chicago
69
+ True story,NYC
70
+ CSV
71
+ end
72
+
73
+ it 'should ignores it' do
74
+ expect(subject.to_a).to eq([
75
+ { 'name' => 'Big Fiction' },
76
+ { 'name' => 'True story' }
77
+ ])
78
+ end
79
+ end
80
+
81
+ context 'with has_many fields' do
82
+ before do
83
+ presenter_klass.class_eval do
84
+ define_csv_fields do |d|
85
+ d.field 'name', 'author'
86
+ d.has_many 'reviews' do |r|
87
+ r.field :tomatoes, :publication
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ let(:io) do
94
+ StringIO.new <<-CSV
95
+ name,author,,,,,,,,
96
+ Big Fiction,Sneed,review_0_tomatoes,99,review_0_publication,Post,review_1_tomatoes,15,review_1_publication,Daily
97
+ True story,Honest Abe,review_0_tomatoes,50,review_0_publication,Daily
98
+ CSV
99
+ end
100
+
101
+ it 'should return attrs' do
102
+ expect(subject.to_a).to eq([
103
+ {
104
+ 'name' => 'Big Fiction',
105
+ 'author' => 'Sneed',
106
+ 'reviews' => [
107
+ { 'tomatoes' => '99', 'publication' => 'Post' },
108
+ { 'tomatoes' => '15', 'publication' => 'Daily' }
109
+ ]
110
+ },
111
+ {
112
+ 'name' => 'True story',
113
+ 'author' => 'Honest Abe',
114
+ 'reviews' => [
115
+ { 'tomatoes' => '50', 'publication' => 'Daily' }
116
+ ]
117
+ }
118
+ ])
119
+ end
120
+ end
121
+
122
+ context 'with export_only has_many field' do
123
+ before do
124
+ presenter_klass.class_eval do
125
+ define_csv_fields do |d|
126
+ d.field :id
127
+ d.has_many :reviews do |r|
128
+ r.field :tomatoes, export_only: true
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ let(:io) do
135
+ StringIO.new <<-CSV
136
+ id
137
+ 22,reviews_0_tomatoes,99
138
+ CSV
139
+ end
140
+
141
+ it 'should exclude field' do
142
+ expect(subject.to_a).to eq([{ 'id' => '22' }])
143
+ end
144
+ end
145
+
146
+ context 'with unknown has_many' do
147
+ before do
148
+ presenter_klass.class_eval do
149
+ define_csv_fields do |d|
150
+ d.field 'name'
151
+ end
152
+ end
153
+ end
154
+
155
+ let(:io) do
156
+ StringIO.new <<-CSV
157
+ name
158
+ Big Fiction,sales_0_count,99,sales_0_period,Q1
159
+ CSV
160
+ end
161
+
162
+ it 'should ignore it' do
163
+ expect(subject.to_a).to eq([{ 'name' => 'Big Fiction' }])
164
+ end
165
+ end
166
+
167
+ context 'with unknown has_many field' do
168
+ before do
169
+ presenter_klass.class_eval do
170
+ define_csv_fields do |d|
171
+ d.field 'name'
172
+ d.has_many 'reviews' do |r|
173
+ r.field :tomatoes
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ let(:io) do
180
+ StringIO.new <<-CSV
181
+ name
182
+ Big Fiction,review_0_tomatoes,99,review_0_auther,Meanie
183
+ CSV
184
+ end
185
+
186
+ it 'should ignore it' do
187
+ expect(subject.to_a).to eq [{ 'name' => 'Big Fiction', 'reviews' => [{ 'tomatoes' => '99' }] }]
188
+ end
189
+ end
190
+
191
+ context 'when csv is invalid' do
192
+ before do
193
+ allow(CSV).to receive(:new).and_raise(CSV::MalformedCSVError)
194
+ end
195
+
196
+ it 'adds an error' do
197
+ expect(subject.to_a).to eq []
198
+ expect(subject.errors[:csv]).to be_present
199
+ end
200
+ end
201
+
202
+ it "should fail when all headers aren't to the left"
203
+ it 'should ignore undefined variable columns'
204
+ it 'should fail when variable columns not pair up correctly'
205
+ end
data/spec/base_spec.rb ADDED
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe SpeakyCsv::Base do
4
+ describe 'define_csv_fields' do
5
+ let(:parent_klass) { Class.new SpeakyCsv::Base }
6
+ let(:child_klass) { Class.new parent_klass }
7
+
8
+ context 'when parent class has config' do
9
+ before do
10
+ parent_klass.class_eval do
11
+ define_csv_fields do |d|
12
+ d.field :name
13
+ d.has_many :reviews do |r|
14
+ r.field :tomatoes
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ it 'inherits it for child' do
21
+ expect(child_klass.csv_field_builder.fields).to eq [:name]
22
+ expect(child_klass.csv_field_builder.has_manys.keys).to eq [:reviews]
23
+ expect(child_klass.csv_field_builder.has_manys[:reviews].fields).to eq [:tomatoes]
24
+ end
25
+
26
+ context 'and child adds to it' do
27
+ before do
28
+ child_klass.class_eval do
29
+ define_csv_fields do |d|
30
+ d.field :author
31
+ d.has_many :sales do |s|
32
+ s.field :period
33
+ end
34
+ d.has_many :reviews do |r|
35
+ r.field :publication
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ it 'adds it to child' do
42
+ expect(child_klass.csv_field_builder.fields).to eq [:name, :author]
43
+ expect(child_klass.csv_field_builder.has_manys.keys.sort).to eq [:reviews, :sales].sort
44
+ expect(child_klass.csv_field_builder.has_manys[:reviews].fields).to eq [:tomatoes, :publication]
45
+ expect(child_klass.csv_field_builder.has_manys[:sales].fields).to eq [:period]
46
+ end
47
+
48
+ it 'doesnt change parents config' do
49
+ expect(parent_klass.csv_field_builder.fields).to_not include :author
50
+ expect(parent_klass.csv_field_builder.has_manys.keys).to_not include :sales
51
+ expect(parent_klass.csv_field_builder.has_manys[:reviews].fields).to_not include :publication
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end