speaky_csv 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,81 +1,17 @@
1
1
  module SpeakyCsv
2
- # An instance of this class is yielded to the block passed to
3
- # define_csv_fields. Used to configure speaky csv.
4
- class Builder
5
- attr_reader \
6
- :export_only_fields,
7
- :fields,
8
- :has_manys,
9
- :has_ones,
10
- :primary_key
11
-
12
- def initialize
13
- @export_only_fields = []
14
- @fields = []
15
- @has_manys = {}
16
- @has_ones = {}
17
- @primary_key = :id
18
- end
19
-
20
- # Add one or many fields to the csv format.
21
- #
22
- # If options are passed, they apply to all given fields.
23
- def field(*fields, export_only: false)
24
- @fields += fields.map(&:to_sym)
25
- @fields.uniq!
26
-
27
- if export_only
28
- @export_only_fields += fields.map(&:to_sym)
29
- @export_only_fields.uniq!
30
- end
31
-
32
- nil
33
- end
34
-
35
- # Define a custom primary key. By default an `id` column as used.
36
- #
37
- # Accepts the same options as #field
38
- def primary_key=(name, options = {})
39
- field name, options
40
- @primary_key = name.to_sym
41
- end
42
-
43
- def has_one(name)
44
- @has_ones[name.to_sym] ||= self.class.new
45
- yield @has_ones[name.to_sym]
46
-
47
- nil
48
- end
49
-
50
- def has_many(name)
51
- @has_manys[name.to_sym] ||= self.class.new
52
- yield @has_manys[name.to_sym]
53
-
54
- nil
55
- end
56
-
57
- def dup
58
- other = super
59
- other.instance_variable_set '@has_manys', @has_manys.deep_dup
60
- other.instance_variable_set '@has_ones', @has_ones.deep_dup
61
-
62
- other
63
- end
64
- end
65
-
66
2
  # Inherit from this class when using SpeakyCsv
67
3
  class Base
68
- class_attribute :csv_field_builder
69
- self.csv_field_builder = Builder.new
4
+ class_attribute :speaky_csv_config
5
+ self.speaky_csv_config = Config.new
70
6
 
71
7
  def self.define_csv_fields
72
- self.csv_field_builder = csv_field_builder.deep_dup
73
- yield csv_field_builder
8
+ self.speaky_csv_config = speaky_csv_config.deep_dup
9
+ yield ConfigBuilder.new(config: speaky_csv_config)
74
10
  end
75
11
 
76
12
  # Return a new exporter instance
77
13
  def self.exporter(records_enumerator)
78
- Export.new csv_field_builder,
14
+ Export.new speaky_csv_config,
79
15
  records_enumerator
80
16
  end
81
17
 
@@ -98,8 +34,8 @@ module SpeakyCsv
98
34
  # end
99
35
  # end
100
36
  #
101
- def attr_importer(input_io)
102
- AttrImport.new self.class.csv_field_builder,
37
+ def self.attr_importer(input_io)
38
+ AttrImport.new speaky_csv_config,
103
39
  input_io
104
40
  end
105
41
 
@@ -125,9 +61,9 @@ module SpeakyCsv
125
61
  # Optionally an Enumerable instance can be passed instead of an IO
126
62
  # instance. The enumerable should return attr hashes. This may be helpful
127
63
  # for transforming or chaining Enumerables.
128
- def active_record_importer(input_io_or_enumerable, klass)
64
+ def self.active_record_importer(input_io_or_enumerable, klass)
129
65
  ActiveRecordImport.new \
130
- self.class.csv_field_builder,
66
+ speaky_csv_config,
131
67
  input_io_or_enumerable,
132
68
  klass
133
69
  end
@@ -0,0 +1,30 @@
1
+ module SpeakyCsv
2
+ # An instance of this class is yielded to the block passed to
3
+ # define_csv_fields. Used to configure speaky csv.
4
+ class Config
5
+ attr_accessor \
6
+ :export_only_fields,
7
+ :fields,
8
+ :has_manys,
9
+ :has_ones,
10
+ :primary_key,
11
+ :root
12
+
13
+ def initialize(root: true)
14
+ @root = root
15
+ @export_only_fields = []
16
+ @fields = []
17
+ @has_manys = {}
18
+ @has_ones = {}
19
+ @primary_key = :id
20
+ end
21
+
22
+ def dup
23
+ other = super
24
+ other.instance_variable_set '@has_manys', @has_manys.deep_dup
25
+ other.instance_variable_set '@has_ones', @has_ones.deep_dup
26
+
27
+ other
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,74 @@
1
+ module SpeakyCsv
2
+ # An instance of this class is yielded to the block passed to
3
+ # define_csv_fields. Used to configure speaky csv.
4
+ class ConfigBuilder
5
+ attr_reader :config
6
+
7
+ def initialize(config: Config.new, root: true)
8
+ @config = config
9
+ @config.root = root
10
+ end
11
+
12
+ # Add one or many fields to the csv format.
13
+ #
14
+ # If options are passed, they apply to all given fields.
15
+ def field(*fields, export_only: false)
16
+ @config.fields += fields.map(&:to_sym)
17
+ @config.fields.uniq!
18
+
19
+ if export_only
20
+ @config.export_only_fields += fields.map(&:to_sym)
21
+ @config.export_only_fields.uniq!
22
+ end
23
+
24
+ nil
25
+ end
26
+
27
+ # Define a custom primary key. By default an `id` column as used.
28
+ #
29
+ # Accepts the same options as #field
30
+ def primary_key=(name, options = {})
31
+ field name, options
32
+ @config.primary_key = name.to_sym
33
+ end
34
+
35
+ # Define a one to one association. This is also aliased as `belongs_to`. Expects a name and a block to
36
+ # define the fields on associated record.
37
+ #
38
+ # For example:
39
+ #
40
+ # define_csv_fields do |c|
41
+ # has_many 'publisher' do |p|
42
+ # p.field :id, :name, :_destroy
43
+ # end
44
+ # end
45
+ #
46
+ def has_one(name)
47
+ @config.root or raise NotImplementedError, "nested associations are not supported"
48
+ @config.has_ones[name.to_sym] ||= Config.new
49
+ yield self.class.new config: @config.has_ones[name.to_sym], root: false
50
+
51
+ nil
52
+ end
53
+ alias :belongs_to :has_one
54
+
55
+ # Define a one to many association. Expect a name and a block to
56
+ # define the fields on associated records.
57
+ #
58
+ # For example:
59
+ #
60
+ # define_csv_fields do |c|
61
+ # has_many 'reviews' do |r|
62
+ # r.field :id, :name, :_destroy
63
+ # end
64
+ # end
65
+ #
66
+ def has_many(name)
67
+ @config.root or raise NotImplementedError, "nested associations are not supported"
68
+ @config.has_manys[name.to_sym] ||= Config.new
69
+ yield self.class.new config: @config.has_manys[name.to_sym], root: false
70
+
71
+ nil
72
+ end
73
+ end
74
+ end
@@ -32,6 +32,7 @@ module SpeakyCsv
32
32
 
33
33
  def valid_field?(record, field, prefix: nil)
34
34
  return true if record.respond_to? field
35
+ return false if field == :_destroy
35
36
 
36
37
  error_name = prefix ? "#{prefix}_#{field}" : field
37
38
  logger.error "#{error_name} is not a method for class #{record.class}"
@@ -43,8 +44,13 @@ module SpeakyCsv
43
44
  return @enumerator if defined? @enumerator
44
45
 
45
46
  @enumerator = Enumerator.new do |yielder|
47
+ columns = @config.fields
48
+ columns += @config.has_ones.flat_map do |name, config|
49
+ config.fields.map {|f| "#{name}_#{f}" }
50
+ end
51
+
46
52
  # header row
47
- yielder << CSV::Row.new(@config.fields, @config.fields, true).to_csv
53
+ yielder << CSV::Row.new(columns, columns, true).to_csv
48
54
 
49
55
  @records_enumerator.each do |record|
50
56
  values = @config.fields
@@ -62,6 +68,13 @@ module SpeakyCsv
62
68
  end
63
69
  end
64
70
 
71
+ @config.has_ones.select { |a| valid_field? record, a }.each do |name, config|
72
+ has_one_record = record.send name
73
+ config.fields.select { |f| valid_field? has_one_record, f, prefix: name }.each do |field|
74
+ row << has_one_record.send(field)
75
+ end
76
+ end
77
+
65
78
  yielder << row.to_csv
66
79
  end
67
80
  end
@@ -1,3 +1,3 @@
1
1
  module SpeakyCsv
2
- VERSION = '0.0.3'
2
+ VERSION = '0.0.5'
3
3
  end
@@ -5,7 +5,7 @@ describe SpeakyCsv::ActiveRecordImport, :db do
5
5
  let(:presenter_klass) { Class.new SpeakyCsv::Base }
6
6
 
7
7
  let(:io) { StringIO.new }
8
- subject { presenter_klass.new.active_record_importer io, Book }
8
+ subject { presenter_klass.active_record_importer io, Book }
9
9
 
10
10
  def record
11
11
  unless defined? @record
@@ -119,7 +119,7 @@ Big Fiction,Sneed
119
119
  before do
120
120
  presenter_klass.class_eval do
121
121
  define_csv_fields do |d|
122
- d.field :id
122
+ d.field :id, :name, :author
123
123
  end
124
124
  end
125
125
  end
@@ -170,7 +170,7 @@ id,name,whats_this
170
170
  end
171
171
  end
172
172
 
173
- context 'with has_many field' do
173
+ context 'with has_many association' do
174
174
  before do
175
175
  presenter_klass.class_eval do
176
176
  define_csv_fields do |d|
@@ -255,6 +255,80 @@ id
255
255
  end
256
256
  end
257
257
 
258
+ context 'with has_one association' do
259
+ before do
260
+ presenter_klass.class_eval do
261
+ define_csv_fields do |d|
262
+ d.field :id
263
+ d.has_one 'publisher' do |r|
264
+ r.field :id, :name, :_destroy
265
+ end
266
+ end
267
+ end
268
+ end
269
+
270
+ let!(:book) { Book.create! id: 1 }
271
+
272
+ context 'and csv has new associated record' do
273
+ let(:io) do
274
+ StringIO.new <<-CSV
275
+ id,publisher_id,publisher_name
276
+ 1,,Dan Blam
277
+ CSV
278
+ end
279
+
280
+ it 'builds new record' do
281
+ expect(record.publisher.attributes).to include('id' => nil, 'name' => 'Dan Blam')
282
+ expect(record.publisher).to be_new_record
283
+ end
284
+ end
285
+
286
+ #context 'and csv has unchanged associated record' do
287
+ #let(:io) do
288
+ #StringIO.new <<-CSV
289
+ #id
290
+ #1,review_0_id,1,review_0_tomatoes,99,review_0_publication,Post
291
+ #CSV
292
+ #end
293
+
294
+ #let!(:review) { Review.create! id: 1, book: book, tomatoes: 99, publication: 'Post' }
295
+
296
+ #it 'returns clean record' do
297
+ #expect(actual_review).to_not be_changed
298
+ #end
299
+ #end
300
+
301
+ #context 'and csv changes associated record' do
302
+ #let(:io) do
303
+ #StringIO.new <<-CSV
304
+ #id
305
+ #1,review_0_id,1,review_0_tomatoes,80,review_0_publication,Post
306
+ #CSV
307
+ #end
308
+
309
+ #let!(:review) { Review.create! id: 1, book: book, tomatoes: 99, publication: 'Post' }
310
+
311
+ #it 'returns clean record' do
312
+ #expect(actual_review.tomatoes).to eq 80
313
+ #expect(actual_review).to be_changed
314
+ #end
315
+ #end
316
+ #context 'and csv destroys associated record' do
317
+ #let(:io) do
318
+ #StringIO.new <<-CSV
319
+ #id
320
+ #1,review_0_id,1,review_0__destroy,true
321
+ #CSV
322
+ #end
323
+
324
+ #let!(:review) { Review.create! id: 1, book: book, tomatoes: 99, publication: 'Post' }
325
+
326
+ #it 'marks record for destruction' do
327
+ #expect(actual_review).to be_marked_for_destruction
328
+ #end
329
+ #end
330
+ end
331
+
258
332
  it 'should fail when variable columns not pair up correctly'
259
333
 
260
334
  describe 'batch behavior' do
@@ -368,7 +442,7 @@ hihihi
368
442
  end
369
443
 
370
444
  context 'with enumerator' do
371
- subject { presenter_klass.new.active_record_importer enumerator, Book }
445
+ subject { presenter_klass.active_record_importer enumerator, Book }
372
446
 
373
447
  before do
374
448
  presenter_klass.class_eval do
@@ -4,7 +4,7 @@ describe SpeakyCsv::AttrImport do
4
4
  let(:presenter_klass) { Class.new SpeakyCsv::Base }
5
5
 
6
6
  let(:io) { StringIO.new }
7
- subject { presenter_klass.new.attr_importer io }
7
+ subject { presenter_klass.attr_importer io }
8
8
 
9
9
  context 'with fields' do
10
10
  before do
@@ -29,6 +29,40 @@ True story,Honest Abe
29
29
  { 'name' => 'True story', 'author' => 'Honest Abe' }
30
30
  ])
31
31
  end
32
+
33
+ context "when csv cell is empty" do
34
+ let(:io) do
35
+ StringIO.new <<-CSV
36
+ name,author
37
+ ,Sneed
38
+ True story,
39
+ CSV
40
+ end
41
+
42
+ it 'should return nil values' do
43
+ expect(subject.to_a).to eq([
44
+ { 'name' => nil, 'author' => 'Sneed' },
45
+ { 'name' => 'True story', 'author' => nil }
46
+ ])
47
+ end
48
+ end
49
+
50
+ context "when a column isn't present" do
51
+ let(:io) do
52
+ StringIO.new <<-CSV
53
+ name
54
+ Big Fiction
55
+ True story
56
+ CSV
57
+ end
58
+
59
+ it 'should not include that field in output' do
60
+ expect(subject.to_a).to eq([
61
+ { 'name' => 'Big Fiction' },
62
+ { 'name' => 'True story' }
63
+ ])
64
+ end
65
+ end
32
66
  end
33
67
 
34
68
  context 'with export_only field' do
@@ -58,7 +92,7 @@ id,name
58
92
  end
59
93
  end
60
94
 
61
- context 'with unknown field' do
95
+ context 'with unknown columns' do
62
96
  before do
63
97
  presenter_klass.class_eval do
64
98
  define_csv_fields do |d|
@@ -127,6 +161,130 @@ True story,Honest Abe,review_0_tomatoes,50,review_0_publication,Daily
127
161
  }
128
162
  ])
129
163
  end
164
+
165
+ context 'when csv cell is empty' do
166
+ let(:io) do
167
+ StringIO.new <<-CSV
168
+ name,author,,,,,,,,
169
+ Big Fiction,Sneed,review_0_tomatoes,,review_0_publication,,review_1_tomatoes,,review_1_publication
170
+ True story,Honest Abe,review_0_tomatoes,,review_0_publication,,
171
+ CSV
172
+ end
173
+
174
+ it 'should return nil values' do
175
+ expect(subject.to_a).to eq([
176
+ {
177
+ 'name' => 'Big Fiction',
178
+ 'author' => 'Sneed',
179
+ 'reviews' => [
180
+ { 'tomatoes' => nil, 'publication' => nil },
181
+ { 'tomatoes' => nil, 'publication' => nil }
182
+ ]
183
+ },
184
+ {
185
+ 'name' => 'True story',
186
+ 'author' => 'Honest Abe',
187
+ 'reviews' => [
188
+ { 'tomatoes' => nil, 'publication' => nil }
189
+ ]
190
+ }
191
+ ])
192
+ end
193
+ end
194
+ end
195
+
196
+ context 'with has_one fields' do
197
+ before do
198
+ presenter_klass.class_eval do
199
+ define_csv_fields do |d|
200
+ d.field 'name', 'author'
201
+ d.has_one 'publisher' do |r|
202
+ r.field :id, :name
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ let(:io) do
209
+ StringIO.new <<-CSV
210
+ name,author,publisher_id,publisher_name,
211
+ Big Fiction,Sneed,3,Dan Blam
212
+ True story,Honest Abe,50,Burt Bacharach
213
+ CSV
214
+ end
215
+
216
+ it 'should return attrs' do
217
+ expect(subject.to_a).to eq([
218
+ {
219
+ 'name' => 'Big Fiction',
220
+ 'author' => 'Sneed',
221
+ 'publisher' => {
222
+ 'id' => '3', 'name' => 'Dan Blam'
223
+ },
224
+ },
225
+ {
226
+ 'name' => 'True story',
227
+ 'author' => 'Honest Abe',
228
+ 'publisher' => {
229
+ 'id' => '50', 'name' => 'Burt Bacharach'
230
+ }
231
+ }
232
+ ])
233
+ end
234
+
235
+ context 'when csv cell is empty' do
236
+ let(:io) do
237
+ StringIO.new <<-CSV
238
+ name,author,publisher_id,publisher_name,
239
+ Big Fiction,Sneed,,Dan Blam
240
+ True story,Honest Abe,50,
241
+ CSV
242
+ end
243
+
244
+ it 'should return nil values' do
245
+ expect(subject.to_a).to eq([
246
+ {
247
+ 'name' => 'Big Fiction',
248
+ 'author' => 'Sneed',
249
+ 'publisher' => {
250
+ 'id' => nil, 'name' => 'Dan Blam'
251
+ },
252
+ },
253
+ {
254
+ 'name' => 'True story',
255
+ 'author' => 'Honest Abe',
256
+ 'publisher' => {
257
+ 'id' => '50', 'name' => nil
258
+ }
259
+ }
260
+ ])
261
+ end
262
+ end
263
+
264
+ context "when a column isn't present" do
265
+ let(:io) do
266
+ StringIO.new <<-CSV
267
+ name,author,publisher_id
268
+ Big Fiction,Sneed,3
269
+ True story,Honest Abe,50
270
+ CSV
271
+ end
272
+
273
+ it 'should not include that field in output' do
274
+ expect(subject.to_a).to eq([
275
+ {
276
+ 'name' => 'Big Fiction',
277
+ 'author' => 'Sneed',
278
+ 'publisher' => { 'id' => '3' },
279
+ },
280
+ {
281
+ 'name' => 'True story',
282
+ 'author' => 'Honest Abe',
283
+ 'publisher' => { 'id' => '50' }
284
+ }
285
+ ])
286
+ end
287
+ end
130
288
  end
131
289
 
132
290
  context 'with export_only has_many field' do