speaky_csv 0.0.2 → 0.0.3
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 +4 -4
- data/README.md +54 -3
- data/lib/speaky_csv/active_record_import.rb +27 -10
- data/lib/speaky_csv/version.rb +1 -1
- data/spec/active_record_import_spec.rb +28 -5
- data/spec/support/active_record.rb +11 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7fad11aca11ab34b9b05459bbc6282ec467d060a
|
4
|
+
data.tar.gz: 0beefbd6193709798f83e1c083022514840d65f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 611f717ba2b8539ec43ca525699d92baef358b5950c11d5c6035adba41b2bcd94d3297a42b7a5fb22b92cfc24993e53921d0d0d5c02658416878ca0bb9b88d74
|
7
|
+
data.tar.gz: 3f2dfd7c80f985470eff75bc58745a88b54a43b27f4d214410f870468abc7c908a2c11fc7da88de37ed857452b231462e8b1cf8685de4a6d9f3e28875d269255
|
data/README.md
CHANGED
@@ -1,9 +1,51 @@
|
|
1
1
|
# Speaky CSV
|
2
2
|
|
3
|
-
CSV
|
3
|
+
CSV imports and exports for ActiveRecord.
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
## For example
|
6
|
+
|
7
|
+
Lets say there exists a User class:
|
8
|
+
|
9
|
+
# in app/models/user.rb
|
10
|
+
class User < ActiveRecord::Base
|
11
|
+
...
|
12
|
+
end
|
13
|
+
|
14
|
+
Speaky can be used to import and export user records. The definition of
|
15
|
+
the csv format could look like this:
|
16
|
+
|
17
|
+
# in app/csv/user_csv.rb
|
18
|
+
class UserCsv < SpeakyCsv::Base
|
19
|
+
define_csv_fields do |config|
|
20
|
+
config.field :id, :email, :roles
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
Now lets import some user records. An import csv will always have an
|
25
|
+
initial header row, with each following row representing a user record.
|
26
|
+
lets import the following csv file (whitespace for clarity):
|
27
|
+
|
28
|
+
# my_import.csv
|
29
|
+
id, email, roles
|
30
|
+
22, admin@example.test, admin
|
31
|
+
, newbie@user.test, user
|
32
|
+
|
33
|
+
This file can be imported like this:
|
34
|
+
|
35
|
+
File.open "my_import.csv", "r" do |io|
|
36
|
+
importer = UserCsv.new.active_record_importer io, User
|
37
|
+
importer.each { |user| user.save }
|
38
|
+
end
|
39
|
+
|
40
|
+
## Custom CSV formats
|
41
|
+
|
42
|
+
Speaky
|
43
|
+
|
44
|
+
|
45
|
+
Speaky allows customization of csv files to a degree, but some
|
46
|
+
conventions need to be followed.
|
47
|
+
|
48
|
+
At a high level, the csv
|
7
49
|
ends up looking similar to the way active record data gets serialized
|
8
50
|
into form parameters which will be familiar to many rails developers.
|
9
51
|
The advantage of this approach is that associated records be imported
|
@@ -46,6 +88,12 @@ Once the format is defined records can be exported like this:
|
|
46
88
|
$ exporter = UserCsv.new.exporter(User.all)
|
47
89
|
$ File.open('users.csv', 'w') { |io| exporter.each { |row| io.write row } }
|
48
90
|
|
91
|
+
TODO:
|
92
|
+
|
93
|
+
* describe importing to attribute list
|
94
|
+
* describe importing to to active records
|
95
|
+
* describe how to transform with an enumerator
|
96
|
+
|
49
97
|
## Recommendations
|
50
98
|
|
51
99
|
* Add `id` and `_destroy` fields for active record models
|
@@ -62,6 +110,9 @@ Once the format is defined records can be exported like this:
|
|
62
110
|
* [x] active record import validations
|
63
111
|
* [ ] `has_one` associations
|
64
112
|
* [ ] required fields (make `lock_version` required for example)
|
113
|
+
* [ ] transformations for values via accessors on class
|
114
|
+
* [ ] public stable api for csv format definition
|
115
|
+
* [ ] assign attrs one at a time so they don't all fail together
|
65
116
|
|
66
117
|
## Contributing
|
67
118
|
|
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'csv'
|
2
2
|
require 'active_record'
|
3
|
-
require 'English'
|
4
3
|
|
5
4
|
module SpeakyCsv
|
6
5
|
# Imports a csv file as unsaved active record instances
|
@@ -37,6 +36,19 @@ module SpeakyCsv
|
|
37
36
|
@log_output.string
|
38
37
|
end
|
39
38
|
|
39
|
+
# Add includes options which will be used when querying records.
|
40
|
+
#
|
41
|
+
# Useful to avoid N+1 type problems. Configured has_manys are automaticaly
|
42
|
+
# included and don't need to be specified here.
|
43
|
+
def includes(options)
|
44
|
+
@includes = options
|
45
|
+
end
|
46
|
+
|
47
|
+
# Add eager_load options which will be used when querying records.
|
48
|
+
def eager_load(options)
|
49
|
+
@eager_load = options
|
50
|
+
end
|
51
|
+
|
40
52
|
private
|
41
53
|
|
42
54
|
def enumerator
|
@@ -46,13 +58,14 @@ module SpeakyCsv
|
|
46
58
|
Enumerator.new do |yielder|
|
47
59
|
@rx.each_slice(QUERY_BATCH_SIZE) do |rows|
|
48
60
|
keys = rows.map { |attrs| attrs[@config.primary_key.to_s] }
|
49
|
-
|
50
|
-
|
51
|
-
|
61
|
+
query = @klass.includes(@config.has_manys.keys)
|
62
|
+
.where(@config.primary_key => keys)
|
63
|
+
query = query.includes(@includes) if @includes
|
64
|
+
query = query.eager_load(@eager_load) if @eager_load
|
52
65
|
|
53
|
-
|
54
|
-
row_index += 1
|
66
|
+
records = query.inject({}) { |a, e| a[e.send(@config.primary_key).to_s] = e; a }
|
55
67
|
|
68
|
+
rows.each do |attrs|
|
56
69
|
record = if attrs[@config.primary_key.to_s].present?
|
57
70
|
records[attrs[@config.primary_key.to_s]]
|
58
71
|
else
|
@@ -83,13 +96,17 @@ module SpeakyCsv
|
|
83
96
|
end
|
84
97
|
end
|
85
98
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
99
|
+
attrs.each do |attr, value|
|
100
|
+
writer_method = "#{attr}="
|
101
|
+
if record.respond_to? writer_method
|
102
|
+
record.send writer_method, value
|
103
|
+
else
|
104
|
+
logger.error "[row #{row_index}] record doesn't respond to #{attr.inspect}"
|
105
|
+
end
|
90
106
|
end
|
91
107
|
|
92
108
|
yielder << record
|
109
|
+
row_index += 1
|
93
110
|
end
|
94
111
|
end
|
95
112
|
end
|
data/lib/speaky_csv/version.rb
CHANGED
@@ -137,7 +137,7 @@ id,name,author
|
|
137
137
|
|
138
138
|
it 'returns an error' do
|
139
139
|
subject.to_a
|
140
|
-
expect(subject.log).to match(/\[row
|
140
|
+
expect(subject.log).to match(/\[row 1\]/)
|
141
141
|
end
|
142
142
|
end
|
143
143
|
|
@@ -145,7 +145,7 @@ id,name,author
|
|
145
145
|
before do
|
146
146
|
presenter_klass.class_eval do
|
147
147
|
define_csv_fields do |d|
|
148
|
-
d.field :id, :whats_this
|
148
|
+
d.field :id, :name, :whats_this
|
149
149
|
end
|
150
150
|
end
|
151
151
|
end
|
@@ -154,14 +154,19 @@ id,name,author
|
|
154
154
|
|
155
155
|
let(:io) do
|
156
156
|
StringIO.new <<-CSV
|
157
|
-
id,whats_this
|
158
|
-
1,unknown
|
157
|
+
id,name,whats_this
|
158
|
+
1,Huge Fiction,unknown
|
159
159
|
CSV
|
160
160
|
end
|
161
161
|
|
162
162
|
it 'adds an error' do
|
163
163
|
expect(record).to eq book
|
164
|
-
expect(subject.log).to match(/\[row
|
164
|
+
expect(subject.log).to match(/\[row 1\]/)
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'assigns other attributes' do
|
168
|
+
expect(record).to eq book
|
169
|
+
expect(record.name).to eq 'Huge Fiction'
|
165
170
|
end
|
166
171
|
end
|
167
172
|
|
@@ -423,4 +428,22 @@ hihihi
|
|
423
428
|
end
|
424
429
|
end
|
425
430
|
end
|
431
|
+
|
432
|
+
context "with includes options" do
|
433
|
+
before { subject.includes :publisher }
|
434
|
+
|
435
|
+
it "loads active records with includes option" do
|
436
|
+
# I don't know how to verify this so I'll just make sure it doesnt crash
|
437
|
+
expect { subject.to_a }.to_not raise_error
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
context "with includes options" do
|
442
|
+
before { subject.eager_load :publisher }
|
443
|
+
|
444
|
+
it "loads active records with eager_log option" do
|
445
|
+
# I don't know how to verify this so I'll just make sure it doesnt crash
|
446
|
+
expect { subject.to_a }.to_not raise_error
|
447
|
+
end
|
448
|
+
end
|
426
449
|
end
|
@@ -10,8 +10,9 @@ ActiveRecord::Base.establish_connection adapter: 'sqlite3',
|
|
10
10
|
|
11
11
|
ActiveRecord::Schema.define do
|
12
12
|
create_table :books do |table|
|
13
|
-
table.column :name, :string
|
14
13
|
table.column :author, :string
|
14
|
+
table.column :name, :string
|
15
|
+
table.column :publisher_id, :integer
|
15
16
|
end
|
16
17
|
|
17
18
|
create_table :reviews do |table|
|
@@ -19,10 +20,15 @@ ActiveRecord::Schema.define do
|
|
19
20
|
table.column :tomatoes, :integer
|
20
21
|
table.column :publication, :string
|
21
22
|
end
|
23
|
+
|
24
|
+
create_table :publishers do |table|
|
25
|
+
table.column :name, :string
|
26
|
+
end
|
22
27
|
end
|
23
28
|
|
24
29
|
class Book < ActiveRecord::Base
|
25
30
|
has_many :reviews, inverse_of: :book
|
31
|
+
belongs_to :publisher, inverse_of: :books
|
26
32
|
accepts_nested_attributes_for :reviews, allow_destroy: true
|
27
33
|
end
|
28
34
|
|
@@ -31,6 +37,10 @@ class Review < ActiveRecord::Base
|
|
31
37
|
accepts_nested_attributes_for :book, allow_destroy: true
|
32
38
|
end
|
33
39
|
|
40
|
+
class Publisher < ActiveRecord::Base
|
41
|
+
has_many :books, inverse_of: :publisher
|
42
|
+
end
|
43
|
+
|
34
44
|
RSpec.configure do |config|
|
35
45
|
config.before(:suite) do
|
36
46
|
DatabaseCleaner.strategy = :transaction
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: speaky_csv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Hartford
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-12-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|