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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +27 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +119 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +72 -0
- data/Rakefile +6 -0
- data/lib/speaky_csv/active_record_import.rb +97 -0
- data/lib/speaky_csv/attr_import.rb +71 -0
- data/lib/speaky_csv/base.rb +95 -0
- data/lib/speaky_csv/export.rb +64 -0
- data/lib/speaky_csv/version.rb +3 -0
- data/lib/speaky_csv.rb +11 -0
- data/speaky_csv.gemspec +38 -0
- data/spec/active_record_import_spec.rb +338 -0
- data/spec/attr_import_spec.rb +205 -0
- data/spec/base_spec.rb +56 -0
- data/spec/export_spec.rb +174 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/active_record.rb +45 -0
- metadata +269 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: c9f27598188d01a1f54fc451cd37a7bf147030b8
|
|
4
|
+
data.tar.gz: b140a75185fbe65047c91b60dc041f11bcbe7f08
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bd17153298a4ace51d67c96698fc5454c5e27f93652ac651cdb3241779bb4347e177fc5385e044d08d14ecab326568db5f9fc4235791343e03ea6fa7a28d348f
|
|
7
|
+
data.tar.gz: ad3848f06899fca0b59bb0adb3e5534c4ea18886fec66cfd10e2ac07241a299fa0dae6807f774b2826281437a68102cb80230c8e3efb09268206198e8661c309
|
data/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spec/active_record.log
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# uncomment to ignore pre-existing warnings
|
|
2
|
+
# inherit_from: .rubocop_todo.yml
|
|
3
|
+
|
|
4
|
+
AllCops:
|
|
5
|
+
Exclude:
|
|
6
|
+
- 'bin/**/*'
|
|
7
|
+
- 'db/**/*'
|
|
8
|
+
- 'config/**/*'
|
|
9
|
+
- 'features/**/*'
|
|
10
|
+
- 'lib/**/*'
|
|
11
|
+
- 'script/**/*'
|
|
12
|
+
- 'spec/**/*'
|
|
13
|
+
- 'vendor/**/*'
|
|
14
|
+
|
|
15
|
+
RunRailsCops: true
|
|
16
|
+
|
|
17
|
+
# Allow 120 character line lengths
|
|
18
|
+
LineLength:
|
|
19
|
+
Max: 120
|
|
20
|
+
|
|
21
|
+
# Allow tabbed alignment of method args
|
|
22
|
+
SingleSpaceBeforeFirstArg:
|
|
23
|
+
Enabled: false
|
|
24
|
+
|
|
25
|
+
# Don't require top-level documenation
|
|
26
|
+
Documentation:
|
|
27
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
speaky_csv (0.0.1)
|
|
5
|
+
activemodel
|
|
6
|
+
activerecord
|
|
7
|
+
activesupport
|
|
8
|
+
|
|
9
|
+
GEM
|
|
10
|
+
remote: https://rubygems.org/
|
|
11
|
+
specs:
|
|
12
|
+
activemodel (4.2.5)
|
|
13
|
+
activesupport (= 4.2.5)
|
|
14
|
+
builder (~> 3.1)
|
|
15
|
+
activerecord (4.2.5)
|
|
16
|
+
activemodel (= 4.2.5)
|
|
17
|
+
activesupport (= 4.2.5)
|
|
18
|
+
arel (~> 6.0)
|
|
19
|
+
activesupport (4.2.5)
|
|
20
|
+
i18n (~> 0.7)
|
|
21
|
+
json (~> 1.7, >= 1.7.7)
|
|
22
|
+
minitest (~> 5.1)
|
|
23
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
|
24
|
+
tzinfo (~> 1.1)
|
|
25
|
+
arel (6.0.3)
|
|
26
|
+
ast (2.1.0)
|
|
27
|
+
astrolabe (1.3.1)
|
|
28
|
+
parser (~> 2.2)
|
|
29
|
+
builder (3.2.2)
|
|
30
|
+
coderay (1.1.0)
|
|
31
|
+
database_cleaner (1.5.1)
|
|
32
|
+
diff-lcs (1.2.5)
|
|
33
|
+
ffi (1.9.10)
|
|
34
|
+
formatador (0.2.5)
|
|
35
|
+
guard (2.13.0)
|
|
36
|
+
formatador (>= 0.2.4)
|
|
37
|
+
listen (>= 2.7, <= 4.0)
|
|
38
|
+
lumberjack (~> 1.0)
|
|
39
|
+
nenv (~> 0.1)
|
|
40
|
+
notiffany (~> 0.0)
|
|
41
|
+
pry (>= 0.9.12)
|
|
42
|
+
shellany (~> 0.0)
|
|
43
|
+
thor (>= 0.18.1)
|
|
44
|
+
guard-compat (1.2.1)
|
|
45
|
+
guard-rspec (4.6.4)
|
|
46
|
+
guard (~> 2.1)
|
|
47
|
+
guard-compat (~> 1.1)
|
|
48
|
+
rspec (>= 2.99.0, < 4.0)
|
|
49
|
+
i18n (0.7.0)
|
|
50
|
+
json (1.8.3)
|
|
51
|
+
listen (3.0.4)
|
|
52
|
+
rb-fsevent (>= 0.9.3)
|
|
53
|
+
rb-inotify (>= 0.9)
|
|
54
|
+
lumberjack (1.0.9)
|
|
55
|
+
method_source (0.8.2)
|
|
56
|
+
minitest (5.8.2)
|
|
57
|
+
nenv (0.2.0)
|
|
58
|
+
notiffany (0.0.8)
|
|
59
|
+
nenv (~> 0.1)
|
|
60
|
+
shellany (~> 0.0)
|
|
61
|
+
parser (2.2.3.0)
|
|
62
|
+
ast (>= 1.1, < 3.0)
|
|
63
|
+
powerpack (0.1.1)
|
|
64
|
+
pry (0.10.3)
|
|
65
|
+
coderay (~> 1.1.0)
|
|
66
|
+
method_source (~> 0.8.1)
|
|
67
|
+
slop (~> 3.4)
|
|
68
|
+
rainbow (2.0.0)
|
|
69
|
+
rake (10.4.2)
|
|
70
|
+
rb-fsevent (0.9.6)
|
|
71
|
+
rb-inotify (0.9.5)
|
|
72
|
+
ffi (>= 0.5.0)
|
|
73
|
+
rspec (3.3.0)
|
|
74
|
+
rspec-core (~> 3.3.0)
|
|
75
|
+
rspec-expectations (~> 3.3.0)
|
|
76
|
+
rspec-mocks (~> 3.3.0)
|
|
77
|
+
rspec-core (3.3.2)
|
|
78
|
+
rspec-support (~> 3.3.0)
|
|
79
|
+
rspec-expectations (3.3.1)
|
|
80
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
81
|
+
rspec-support (~> 3.3.0)
|
|
82
|
+
rspec-mocks (3.3.2)
|
|
83
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
84
|
+
rspec-support (~> 3.3.0)
|
|
85
|
+
rspec-support (3.3.0)
|
|
86
|
+
rubocop (0.35.0)
|
|
87
|
+
astrolabe (~> 1.3)
|
|
88
|
+
parser (>= 2.2.3.0, < 3.0)
|
|
89
|
+
powerpack (~> 0.1)
|
|
90
|
+
rainbow (>= 1.99.1, < 3.0)
|
|
91
|
+
ruby-progressbar (~> 1.7)
|
|
92
|
+
ruby-progressbar (1.7.5)
|
|
93
|
+
ruby_gntp (0.3.4)
|
|
94
|
+
shellany (0.0.1)
|
|
95
|
+
slop (3.6.0)
|
|
96
|
+
sqlite3 (1.3.11)
|
|
97
|
+
thor (0.19.1)
|
|
98
|
+
thread_safe (0.3.5)
|
|
99
|
+
tzinfo (1.2.2)
|
|
100
|
+
thread_safe (~> 0.1)
|
|
101
|
+
|
|
102
|
+
PLATFORMS
|
|
103
|
+
ruby
|
|
104
|
+
|
|
105
|
+
DEPENDENCIES
|
|
106
|
+
bundler (> 1.5)
|
|
107
|
+
database_cleaner
|
|
108
|
+
guard-rspec
|
|
109
|
+
rake
|
|
110
|
+
rb-fsevent
|
|
111
|
+
rb-inotify
|
|
112
|
+
rspec (> 2.14.0)
|
|
113
|
+
rubocop
|
|
114
|
+
ruby_gntp
|
|
115
|
+
speaky_csv!
|
|
116
|
+
sqlite3
|
|
117
|
+
|
|
118
|
+
BUNDLED WITH
|
|
119
|
+
1.10.3
|
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2014 Andrew Hartford
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Speaky CSV
|
|
2
|
+
|
|
3
|
+
CSV exporting and importing for ActiveRecord and ActiveModel records.
|
|
4
|
+
|
|
5
|
+
Speaky lets the format of csv files to be customized, but it does
|
|
6
|
+
require certain conventions to be followed. At a high level, the csv
|
|
7
|
+
ends up looking similar to the way active record data gets serialized
|
|
8
|
+
into form parameters which will be familiar to many rails developers.
|
|
9
|
+
The advantage of this approach is that associated records be imported
|
|
10
|
+
and exported.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add this line to your application's Gemfile:
|
|
15
|
+
|
|
16
|
+
gem 'speaky_csv'
|
|
17
|
+
|
|
18
|
+
And then execute:
|
|
19
|
+
|
|
20
|
+
$ bundle
|
|
21
|
+
|
|
22
|
+
Or install it yourself as:
|
|
23
|
+
|
|
24
|
+
$ gem install speaky_csv
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Subclass SpeakyCsv::Base and define a csv format for an active
|
|
29
|
+
record class. For example:
|
|
30
|
+
|
|
31
|
+
# in app/csv/user_csv.rb
|
|
32
|
+
class UserCsv < SpeakyCsv::Base
|
|
33
|
+
define_csv_fields do |config|
|
|
34
|
+
config.field :id, :first_name, :last_name, :email
|
|
35
|
+
|
|
36
|
+
config.has_many :roles do |r|
|
|
37
|
+
r.field :role_name
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
See the rdoc for more details on how to configure the format.
|
|
43
|
+
|
|
44
|
+
Once the format is defined records can be exported like this:
|
|
45
|
+
|
|
46
|
+
$ exporter = UserCsv.new.exporter(User.all)
|
|
47
|
+
$ File.open('users.csv', 'w') { |io| exporter.each { |row| io.write row } }
|
|
48
|
+
|
|
49
|
+
## Recommendations
|
|
50
|
+
|
|
51
|
+
* Add `id` and `_destroy` fields for active record models
|
|
52
|
+
* For associations, use `nested_attributes_for` and add `id` and
|
|
53
|
+
`_destroy` fields
|
|
54
|
+
* Use optimistic locking and add `lock_version` to csv
|
|
55
|
+
|
|
56
|
+
## TODO
|
|
57
|
+
|
|
58
|
+
* [x] export only fields
|
|
59
|
+
* [x] configurable id field (key off an `external_id` for example)
|
|
60
|
+
* [x] export validations
|
|
61
|
+
* [x] attr import validations
|
|
62
|
+
* [x] active record import validations
|
|
63
|
+
* [ ] `has_one` associations
|
|
64
|
+
* [ ] required fields (make `lock_version` required for example)
|
|
65
|
+
|
|
66
|
+
## Contributing
|
|
67
|
+
|
|
68
|
+
1. Fork it ( http://github.com/ajh/speaky_csv/fork )
|
|
69
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
70
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
71
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
72
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require 'csv'
|
|
2
|
+
require 'active_record'
|
|
3
|
+
|
|
4
|
+
module SpeakyCsv
|
|
5
|
+
# Imports a csv file as unsaved active record instances
|
|
6
|
+
class ActiveRecordImport
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
QUERY_BATCH_SIZE = 20
|
|
10
|
+
TRUE_VALUES = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES
|
|
11
|
+
|
|
12
|
+
attr_accessor :errors
|
|
13
|
+
|
|
14
|
+
def initialize(config, input_io, klass)
|
|
15
|
+
@config = config
|
|
16
|
+
@errors = ActiveModel::Errors.new(self)
|
|
17
|
+
@klass = klass
|
|
18
|
+
|
|
19
|
+
@attr_import = AttrImport.new @config, input_io
|
|
20
|
+
@attr_import.errors = @errors
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def each
|
|
24
|
+
errors.clear
|
|
25
|
+
block_given? ? enumerator.each { |a| yield a } : enumerator
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def enumerator
|
|
31
|
+
Enumerator.new do |yielder|
|
|
32
|
+
attr_enumerator = @attr_import.each
|
|
33
|
+
done = false
|
|
34
|
+
|
|
35
|
+
row_index = 1
|
|
36
|
+
|
|
37
|
+
while done == false
|
|
38
|
+
rows = []
|
|
39
|
+
|
|
40
|
+
QUERY_BATCH_SIZE.times do
|
|
41
|
+
begin
|
|
42
|
+
rows << attr_enumerator.next
|
|
43
|
+
rescue StopIteration
|
|
44
|
+
done = true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
keys = rows.map { |attrs| attrs[@config.primary_key.to_s] }
|
|
49
|
+
records = @klass.includes(@config.has_manys.keys)
|
|
50
|
+
.where(@config.primary_key => keys)
|
|
51
|
+
.inject({}) { |a, e| a[e.send(@config.primary_key).to_s] = e; a }
|
|
52
|
+
|
|
53
|
+
rows.each do |attrs|
|
|
54
|
+
row_index += 1
|
|
55
|
+
|
|
56
|
+
record = if attrs[@config.primary_key.to_s].present?
|
|
57
|
+
records[attrs[@config.primary_key.to_s]]
|
|
58
|
+
else
|
|
59
|
+
@klass.new
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
unless record
|
|
63
|
+
errors.add "row_#{row_index}", "record not found with primary key #{attrs[@config.primary_key]}"
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if @config.fields.include?(:_destroy)
|
|
68
|
+
if TRUE_VALUES.include?(attrs['_destroy'])
|
|
69
|
+
record.mark_for_destruction
|
|
70
|
+
yielder << record
|
|
71
|
+
next
|
|
72
|
+
|
|
73
|
+
else
|
|
74
|
+
attrs.delete '_destroy'
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@config.has_manys.keys.each do |name|
|
|
79
|
+
if attrs.key?(name.to_s)
|
|
80
|
+
# assume nested attributes feature is used
|
|
81
|
+
attrs["#{name}_attributes"] = attrs.delete name.to_s
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
record.attributes = attrs
|
|
87
|
+
rescue ActiveRecord::UnknownAttributeError
|
|
88
|
+
errors.add "row_#{row_index}", "record doesn't respond to some configured fields: #{$!.message}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
yielder << record
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require 'csv'
|
|
2
|
+
|
|
3
|
+
module SpeakyCsv
|
|
4
|
+
# Imports a csv file as attribute hashes.
|
|
5
|
+
class AttrImport
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
attr_accessor :errors
|
|
9
|
+
|
|
10
|
+
def initialize(config, input_io)
|
|
11
|
+
@config = config
|
|
12
|
+
@input_io = input_io
|
|
13
|
+
@errors = ActiveModel::Errors.new(self)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# yields successive
|
|
17
|
+
def each
|
|
18
|
+
errors.clear
|
|
19
|
+
block_given? ? enumerator.each { |a| yield a } : enumerator
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def enumerator
|
|
25
|
+
Enumerator.new do |yielder|
|
|
26
|
+
begin
|
|
27
|
+
csv = CSV.new @input_io, headers: true
|
|
28
|
+
|
|
29
|
+
csv.each do |row|
|
|
30
|
+
attrs = {}
|
|
31
|
+
|
|
32
|
+
row.headers.compact.each do |h|
|
|
33
|
+
next unless @config.fields.include?(h.to_sym)
|
|
34
|
+
next if @config.export_only_fields.include?(h.to_sym)
|
|
35
|
+
attrs[h] = row.field h
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
headers_length = row.headers.compact.length
|
|
39
|
+
pairs_start_on_evens = headers_length.even?
|
|
40
|
+
(headers_length..row.fields.length).each do |i|
|
|
41
|
+
i.send(pairs_start_on_evens ? :even? : :odd?) || next
|
|
42
|
+
row[i] || next
|
|
43
|
+
|
|
44
|
+
m = row[i].match(/^(\w+)_(\d+)_(\w+)$/)
|
|
45
|
+
m || next
|
|
46
|
+
has_many_name = m[1].pluralize
|
|
47
|
+
has_many_index = m[2].to_i
|
|
48
|
+
has_many_field = m[3]
|
|
49
|
+
has_many_value = row[i + 1]
|
|
50
|
+
|
|
51
|
+
has_many_config = @config.has_manys[has_many_name.to_sym]
|
|
52
|
+
|
|
53
|
+
next unless has_many_config
|
|
54
|
+
next unless has_many_config.fields.include?(has_many_field.to_sym)
|
|
55
|
+
next if has_many_config.export_only_fields.include?(has_many_field.to_sym)
|
|
56
|
+
|
|
57
|
+
attrs[has_many_name] ||= []
|
|
58
|
+
attrs[has_many_name][has_many_index] ||= {}
|
|
59
|
+
attrs[has_many_name][has_many_index][has_many_field] = has_many_value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
yielder << attrs
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
rescue CSV::MalformedCSVError
|
|
66
|
+
errors.add :csv, "is malformed: #{$!.message}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module SpeakyCsv
|
|
2
|
+
|
|
3
|
+
# An instance of this class is yielded to the block passed to
|
|
4
|
+
# define_csv_fields. Used to configure speaky csv.
|
|
5
|
+
class Builder
|
|
6
|
+
attr_reader \
|
|
7
|
+
:export_only_fields,
|
|
8
|
+
:fields,
|
|
9
|
+
:has_manys,
|
|
10
|
+
:has_ones,
|
|
11
|
+
:primary_key
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@export_only_fields = []
|
|
15
|
+
@fields = []
|
|
16
|
+
@has_manys = {}
|
|
17
|
+
@has_ones = {}
|
|
18
|
+
@primary_key = :id
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Add one or many fields to the csv format.
|
|
22
|
+
#
|
|
23
|
+
# If options are passed, they apply to all given fields.
|
|
24
|
+
def field(*fields, export_only: false)
|
|
25
|
+
@fields += fields.map(&:to_sym)
|
|
26
|
+
@fields.uniq!
|
|
27
|
+
|
|
28
|
+
if export_only
|
|
29
|
+
@export_only_fields += fields.map(&:to_sym)
|
|
30
|
+
@export_only_fields.uniq!
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Define a custom primary key. By default an `id` column as used.
|
|
37
|
+
#
|
|
38
|
+
# Accepts the same options as #field
|
|
39
|
+
def primary_key=(name, options={})
|
|
40
|
+
field name, options
|
|
41
|
+
@primary_key = name.to_sym
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def has_one(name)
|
|
45
|
+
@has_ones[name.to_sym] ||= self.class.new
|
|
46
|
+
yield @has_ones[name.to_sym]
|
|
47
|
+
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def has_many(name)
|
|
52
|
+
@has_manys[name.to_sym] ||= self.class.new
|
|
53
|
+
yield @has_manys[name.to_sym]
|
|
54
|
+
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def dup
|
|
59
|
+
other = super
|
|
60
|
+
other.instance_variable_set '@has_manys', @has_manys.deep_dup
|
|
61
|
+
other.instance_variable_set '@has_ones', @has_ones.deep_dup
|
|
62
|
+
|
|
63
|
+
other
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Inherit from this class when using SpeakyCsv
|
|
68
|
+
class Base
|
|
69
|
+
class_attribute :csv_field_builder
|
|
70
|
+
self.csv_field_builder = Builder.new
|
|
71
|
+
|
|
72
|
+
def self.define_csv_fields
|
|
73
|
+
self.csv_field_builder = csv_field_builder.deep_dup
|
|
74
|
+
yield csv_field_builder
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Return a new exporter instance
|
|
78
|
+
def exporter(records_enumerator)
|
|
79
|
+
Export.new self.class.csv_field_builder,
|
|
80
|
+
records_enumerator
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def attr_importer(input_io)
|
|
84
|
+
AttrImport.new self.class.csv_field_builder,
|
|
85
|
+
input_io
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def active_record_importer(input_io, klass)
|
|
89
|
+
ActiveRecordImport.new \
|
|
90
|
+
self.class.csv_field_builder,
|
|
91
|
+
input_io,
|
|
92
|
+
klass
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require 'csv'
|
|
2
|
+
require 'active_model'
|
|
3
|
+
|
|
4
|
+
module SpeakyCsv
|
|
5
|
+
# Exports records as csv. Will write a csv to the given IO object
|
|
6
|
+
class Export
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
def initialize(config, records_enumerator)
|
|
10
|
+
@config = config
|
|
11
|
+
@records_enumerator = records_enumerator
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Writes csv string to io
|
|
15
|
+
def each
|
|
16
|
+
errors.clear
|
|
17
|
+
block_given? ? enumerator.each { |a| yield a } : enumerator
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def errors
|
|
21
|
+
@errors ||= ActiveModel::Errors.new(self)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def valid_field?(record, field, prefix: nil)
|
|
27
|
+
return true if record.respond_to? field
|
|
28
|
+
|
|
29
|
+
error_name = prefix ? "#{prefix}_#{field}" : field
|
|
30
|
+
|
|
31
|
+
if errors[error_name].blank?
|
|
32
|
+
errors.add error_name, "is not a method for class #{record.class}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def enumerator
|
|
39
|
+
Enumerator.new do |yielder|
|
|
40
|
+
# header row
|
|
41
|
+
yielder << CSV::Row.new(@config.fields, @config.fields, true).to_csv
|
|
42
|
+
|
|
43
|
+
@records_enumerator.each do |record|
|
|
44
|
+
values = @config.fields
|
|
45
|
+
.select { |f| valid_field? record, f }
|
|
46
|
+
.map { |f| record.send f }
|
|
47
|
+
|
|
48
|
+
row = CSV::Row.new @config.fields, values
|
|
49
|
+
|
|
50
|
+
@config.has_manys.select { |a| valid_field? record, a }.each do |name, config|
|
|
51
|
+
record.send(name).each_with_index do |has_many_record, index|
|
|
52
|
+
config.fields.select { |f| valid_field? has_many_record, f, prefix: name }.each do |field|
|
|
53
|
+
row << "#{name.to_s.singularize}_#{index}_#{field}"
|
|
54
|
+
row << has_many_record.send(field)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
yielder << row.to_csv
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/speaky_csv.rb
ADDED
data/speaky_csv.gemspec
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'speaky_csv/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = 'speaky_csv'
|
|
8
|
+
spec.version = SpeakyCsv::VERSION
|
|
9
|
+
spec.authors = ['Andy Hartford']
|
|
10
|
+
spec.email = ['andy.hartford@cohealo.com']
|
|
11
|
+
spec.summary = 'CSV importing and exporting for ActiveRecord and ActiveModel'
|
|
12
|
+
spec.description = 'CSV importing and exporting for ActiveRecord and ActiveModel with a Enumerator flavor'
|
|
13
|
+
spec.homepage = 'https://github.com/ajh/speaky_csv'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
|
+
spec.require_paths = ['lib']
|
|
20
|
+
|
|
21
|
+
spec.add_runtime_dependency 'activemodel', '~> 4.2'
|
|
22
|
+
spec.add_runtime_dependency 'activerecord', '~> 4.2'
|
|
23
|
+
spec.add_runtime_dependency 'activesupport', '~> 4.2'
|
|
24
|
+
|
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.10'
|
|
26
|
+
spec.add_development_dependency 'database_cleaner', '~> 1.5'
|
|
27
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
|
28
|
+
spec.add_development_dependency 'rspec', '~> 3'
|
|
29
|
+
spec.add_development_dependency 'rspec-its', '~> 1'
|
|
30
|
+
spec.add_development_dependency 'rubocop', '~> 0.35'
|
|
31
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
|
32
|
+
|
|
33
|
+
# guard stuff
|
|
34
|
+
spec.add_development_dependency 'guard-rspec', '~> 4.6'
|
|
35
|
+
spec.add_development_dependency 'rb-fsevent', '~> 0.9'
|
|
36
|
+
spec.add_development_dependency 'rb-inotify', '~> 0.9'
|
|
37
|
+
spec.add_development_dependency 'ruby_gntp', '~> 0.3'
|
|
38
|
+
end
|