csv-importer 0.1.3 → 0.2.0
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/CHANGELOG.md +47 -4
- data/README.md +53 -5
- data/lib/csv_importer.rb +11 -5
- data/lib/csv_importer/config.rb +6 -1
- data/lib/csv_importer/csv_reader.rb +2 -1
- data/lib/csv_importer/dsl.rb +8 -2
- data/lib/csv_importer/row.rb +7 -5
- data/lib/csv_importer/runner.rb +2 -0
- data/lib/csv_importer/version.rb +1 -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: 9de355b09e3f3610054a0e2ca5c7f27b6e333c1c
|
4
|
+
data.tar.gz: 759a79fb7217f50959c265cf433351460a21e7f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41d3f8ffff6fa2b92bf02a15c02e9713171d44c898efc929ff8daa79a6f7f9b6a9a666f586abb4874d4a7e738a58d2e2d72035fa447f2c80d5a62bf4ddefafd2
|
7
|
+
data.tar.gz: 721a1e4e3b86de3e633d5c62bb7cd5f602af14e82daa2257af1d9bb88bf3ef07e3ef3201ac327434a1310467210478cce1603600b4cb7c82fa88f4b47f15d721
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,41 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
## Unreleased
|
6
|
+
|
7
|
+
## [0.2.0] - 2015-07-24
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
* `after_save` callback.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
progress_bar = ProgressBar.new
|
15
|
+
|
16
|
+
UserImport.new(file: my_file) do
|
17
|
+
after_save do |user|
|
18
|
+
progress_bar.increment
|
19
|
+
end
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
* You can define a composite identifier made of multiple columns.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# Update records with matching company_id AND employee_id
|
27
|
+
identifier :company_id, :employee_id
|
28
|
+
```
|
29
|
+
|
30
|
+
* You can set a custom `quote_char` at runtime. ([#26][] by [@shvetsovdm][])
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
UserImport.new(file: csv_file, quote_char: "'")
|
34
|
+
```
|
35
|
+
|
36
|
+
## [0.1.3] - 2015-06-19
|
37
|
+
|
38
|
+
### Added
|
4
39
|
|
5
40
|
* You can now change the configuration at runtime. Example:
|
6
41
|
|
@@ -19,15 +54,23 @@ before saving it.
|
|
19
54
|
enables you to use `id` as an identifier and import new entries
|
20
55
|
without having to provide an `id`
|
21
56
|
|
22
|
-
|
57
|
+
## [0.1.2] - 2015-06-15
|
58
|
+
|
59
|
+
### Fixed
|
23
60
|
|
24
61
|
* `run!` was not *returning* a report object when the header was invalid.
|
25
62
|
|
26
|
-
|
63
|
+
## [0.1.1] - 2015-06-12
|
64
|
+
|
65
|
+
### Changed
|
27
66
|
|
28
67
|
* When calling `run!` on an import with invalid header we update the
|
29
68
|
report object instead of raising an exception.
|
30
69
|
|
31
|
-
|
70
|
+
## [0.1.0] - 2015-06-11
|
32
71
|
|
33
72
|
* Initial Release
|
73
|
+
|
74
|
+
<!--- The following link definition list is generated by PimpMyChangelog --->
|
75
|
+
[#26]: https://github.com/BrewhouseTeam/csv-importer/issues/26
|
76
|
+
[@shvetsovdm]: https://github.com/shvetsovdm
|
data/README.md
CHANGED
@@ -24,8 +24,7 @@ Reporting progress and errors to the end-user is also key for a good
|
|
24
24
|
experience.
|
25
25
|
|
26
26
|
I went through this many times so I decided to build CSV Importer to
|
27
|
-
save us
|
28
|
-
|
27
|
+
save us the trouble.
|
29
28
|
|
30
29
|
CSV Importer provides:
|
31
30
|
|
@@ -33,6 +32,9 @@ CSV Importer provides:
|
|
33
32
|
* good reporting to the end user
|
34
33
|
* support for wild encodings and CSV formats.
|
35
34
|
|
35
|
+
It is compatible with ActiveRecord 4+ and any ORM that implements
|
36
|
+
the class methods `transaction` and `find_by` and the instance method `save`.
|
37
|
+
|
36
38
|
## Usage tldr;
|
37
39
|
|
38
40
|
Define your CSVImporter:
|
@@ -48,7 +50,7 @@ class ImportUserCSV
|
|
48
50
|
column :last_name, as: [ /last.?name/i, "nom" ]
|
49
51
|
column :published, to: ->(published, user) { user.published_at = published ? Time.now : nil }
|
50
52
|
|
51
|
-
identifier :email # will
|
53
|
+
identifier :email # will update_or_create via :email
|
52
54
|
|
53
55
|
when_invalid :skip # or :abort
|
54
56
|
end
|
@@ -215,6 +217,13 @@ end
|
|
215
217
|
|
216
218
|
And yes, we'll look for an existing record using the downcased email. :)
|
217
219
|
|
220
|
+
You can also define a composite identifier:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
# Update records with matching company_id AND employee_id
|
224
|
+
identifier :company_id, :employee_id
|
225
|
+
```
|
226
|
+
|
218
227
|
### Skip or Abort on error
|
219
228
|
|
220
229
|
By default, we skip invalid records and report errors back to the user.
|
@@ -266,13 +275,12 @@ import = ImportUserCSV.new(file: my_file) do
|
|
266
275
|
end
|
267
276
|
```
|
268
277
|
|
269
|
-
###
|
278
|
+
### `after_build` and `after_save` callbacks
|
270
279
|
|
271
280
|
You can preset attributes (or perform any changes to the model) at
|
272
281
|
configuration or runtime using `after_build`
|
273
282
|
|
274
283
|
```ruby
|
275
|
-
|
276
284
|
class ImportUserCSV
|
277
285
|
model User
|
278
286
|
|
@@ -292,6 +300,20 @@ import = ImportUserCSV.new(file: my_file) do
|
|
292
300
|
end
|
293
301
|
```
|
294
302
|
|
303
|
+
The `after_save` callback is run after each call to the method `save` no
|
304
|
+
matter it fails or succeeds. It is quite handy to keep track of
|
305
|
+
progress.
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
progress_bar = ProgressBar.new
|
309
|
+
|
310
|
+
UserImport.new(file: my_file) do
|
311
|
+
after_save do |user|
|
312
|
+
progress_bar.increment
|
313
|
+
end
|
314
|
+
end
|
315
|
+
```
|
316
|
+
|
295
317
|
|
296
318
|
### Validate the header
|
297
319
|
|
@@ -337,6 +359,32 @@ INVALID_EMAIL,bob
|
|
337
359
|
|
338
360
|
The error returned should be: `{ "E-Mail" => "is invalid" }`
|
339
361
|
|
362
|
+
### Custom quote char
|
363
|
+
|
364
|
+
You can handle exotic quote chars with the `quote_char` option.
|
365
|
+
|
366
|
+
```csv
|
367
|
+
email,name
|
368
|
+
bob@example.com,'bob "elvis" wilson'
|
369
|
+
```
|
370
|
+
|
371
|
+
```ruby
|
372
|
+
import = ImportUserCSV.new(content: csv_content)
|
373
|
+
import.run!
|
374
|
+
import.report.status
|
375
|
+
# => :invalid_csv_file
|
376
|
+
import.report.messages
|
377
|
+
# => CSV::MalformedCSVError: Illegal quoting in line 2.
|
378
|
+
```
|
379
|
+
|
380
|
+
Let's provide a valid quote char:
|
381
|
+
|
382
|
+
```ruby
|
383
|
+
import = ImportUserCSV.new(content: csv_content, quote_char: "'")
|
384
|
+
import.run!
|
385
|
+
# => [ ["bob@example.com", "bob \"elvis\" wilson"] ]
|
386
|
+
```
|
387
|
+
|
340
388
|
## Development
|
341
389
|
|
342
390
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/csv_importer.rb
CHANGED
@@ -30,14 +30,20 @@ require "csv_importer/dsl"
|
|
30
30
|
module CSVImporter
|
31
31
|
class Error < StandardError; end
|
32
32
|
|
33
|
+
# Setup DSL and config object
|
33
34
|
def self.included(klass)
|
34
35
|
klass.extend(Dsl)
|
35
|
-
klass.include(Dsl)
|
36
36
|
klass.define_singleton_method(:config) do
|
37
37
|
@config ||= Config.new
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
# Instance level config will run against this configurator
|
42
|
+
class Configurator < Struct.new(:config)
|
43
|
+
include Dsl
|
44
|
+
end
|
45
|
+
|
46
|
+
|
41
47
|
# Defines the path, file or content of the csv file.
|
42
48
|
# Also allows you to overwrite the configuration at runtime.
|
43
49
|
#
|
@@ -51,7 +57,7 @@ module CSVImporter
|
|
51
57
|
@config = self.class.config.dup
|
52
58
|
@config.attributes = args.last
|
53
59
|
@report = Report.new
|
54
|
-
instance_exec(&block) if block
|
60
|
+
Configurator.new(@config).instance_exec(&block) if block
|
55
61
|
end
|
56
62
|
|
57
63
|
attr_reader :csv, :report, :config
|
@@ -65,7 +71,7 @@ module CSVImporter
|
|
65
71
|
def rows
|
66
72
|
csv.rows.map do |row_array|
|
67
73
|
Row.new(header: header, row_array: row_array, model_klass: config.model,
|
68
|
-
|
74
|
+
identifiers: config.identifiers, after_build_blocks: config.after_build_blocks)
|
69
75
|
end
|
70
76
|
end
|
71
77
|
|
@@ -84,7 +90,8 @@ module CSVImporter
|
|
84
90
|
# Run the import. Return a Report.
|
85
91
|
def run!
|
86
92
|
if valid_header?
|
87
|
-
@report = Runner.call(rows: rows, when_invalid: config.when_invalid
|
93
|
+
@report = Runner.call(rows: rows, when_invalid: config.when_invalid,
|
94
|
+
after_save_blocks: config.after_save_blocks)
|
88
95
|
else
|
89
96
|
@report
|
90
97
|
end
|
@@ -92,4 +99,3 @@ module CSVImporter
|
|
92
99
|
@report = Report.new(status: :invalid_csv_file, parser_error: e.message)
|
93
100
|
end
|
94
101
|
end
|
95
|
-
|
data/lib/csv_importer/config.rb
CHANGED
@@ -5,13 +5,18 @@ module CSVImporter
|
|
5
5
|
|
6
6
|
attribute :model
|
7
7
|
attribute :column_definitions, Array[ColumnDefinition], default: proc { [] }
|
8
|
-
attribute :
|
8
|
+
attribute :identifiers, Array[Symbol], default: []
|
9
9
|
attribute :when_invalid, Symbol, default: proc { :skip }
|
10
10
|
attribute :after_build_blocks, Array[Proc], default: []
|
11
|
+
attribute :after_save_blocks, Array[Proc], default: []
|
11
12
|
|
12
13
|
def after_build(block)
|
13
14
|
self.after_build_blocks << block
|
14
15
|
end
|
16
|
+
|
17
|
+
def after_save(block)
|
18
|
+
self.after_save_blocks << block
|
19
|
+
end
|
15
20
|
end
|
16
21
|
end
|
17
22
|
|
@@ -7,12 +7,13 @@ module CSVImporter
|
|
7
7
|
attribute :content, String
|
8
8
|
attribute :file # IO
|
9
9
|
attribute :path, String
|
10
|
+
attribute :quote_char, String, default: '"'
|
10
11
|
|
11
12
|
def csv_rows
|
12
13
|
@csv_rows ||= begin
|
13
14
|
sane_content = sanitize_content(read_content)
|
14
15
|
separator = detect_separator(sane_content)
|
15
|
-
cells = CSV.parse(sane_content, col_sep: separator)
|
16
|
+
cells = CSV.parse(sane_content, col_sep: separator, quote_char: quote_char)
|
16
17
|
sanitize_cells(cells)
|
17
18
|
end
|
18
19
|
end
|
data/lib/csv_importer/dsl.rb
CHANGED
@@ -10,10 +10,12 @@ module CSVImporter
|
|
10
10
|
config.column_definitions << options.merge(name: name)
|
11
11
|
end
|
12
12
|
|
13
|
-
def identifier(
|
14
|
-
config.
|
13
|
+
def identifier(*identifiers)
|
14
|
+
config.identifiers = identifiers
|
15
15
|
end
|
16
16
|
|
17
|
+
alias_method :identifiers, :identifier
|
18
|
+
|
17
19
|
def when_invalid(action)
|
18
20
|
config.when_invalid = action
|
19
21
|
end
|
@@ -21,5 +23,9 @@ module CSVImporter
|
|
21
23
|
def after_build(&block)
|
22
24
|
config.after_build(block)
|
23
25
|
end
|
26
|
+
|
27
|
+
def after_save(&block)
|
28
|
+
config.after_save(block)
|
29
|
+
end
|
24
30
|
end
|
25
31
|
end
|
data/lib/csv_importer/row.rb
CHANGED
@@ -9,13 +9,14 @@ module CSVImporter
|
|
9
9
|
attribute :header, Header
|
10
10
|
attribute :row_array, Array[String]
|
11
11
|
attribute :model_klass
|
12
|
-
attribute :
|
12
|
+
attribute :identifiers, Array[Symbol]
|
13
13
|
attribute :after_build_blocks, Array[Proc], default: []
|
14
14
|
|
15
15
|
# The model to be persisted
|
16
16
|
def model
|
17
17
|
@model ||= begin
|
18
18
|
model = find_or_build_model
|
19
|
+
|
19
20
|
set_attributes(model)
|
20
21
|
end
|
21
22
|
end
|
@@ -85,13 +86,14 @@ module CSVImporter
|
|
85
86
|
end
|
86
87
|
|
87
88
|
def find_model
|
88
|
-
return nil if
|
89
|
+
return nil if identifiers.empty?
|
89
90
|
|
90
91
|
model = build_model
|
91
92
|
set_attributes(model)
|
92
|
-
|
93
|
-
|
94
|
-
|
93
|
+
query = Hash[
|
94
|
+
identifiers.map { |identifier| [ identifier, model.public_send(identifier) ] }
|
95
|
+
]
|
96
|
+
model_klass.find_by(query)
|
95
97
|
end
|
96
98
|
|
97
99
|
def build_model
|
data/lib/csv_importer/runner.rb
CHANGED
@@ -11,6 +11,7 @@ module CSVImporter
|
|
11
11
|
|
12
12
|
attribute :rows, Array[Row]
|
13
13
|
attribute :when_invalid, Symbol
|
14
|
+
attribute :after_save_blocks, Array[Proc], default: []
|
14
15
|
|
15
16
|
attribute :report, Report, default: proc { Report.new }
|
16
17
|
|
@@ -58,6 +59,7 @@ module CSVImporter
|
|
58
59
|
end
|
59
60
|
|
60
61
|
add_to_report(row, tags)
|
62
|
+
after_save_blocks.each { |block| block.call(row.model) }
|
61
63
|
end
|
62
64
|
end
|
63
65
|
end
|
data/lib/csv_importer/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: csv-importer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Philippe Creux
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: virtus
|