speaky_csv 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,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