speaky_csv 0.0.3 → 0.0.5

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