bumblebee 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +8 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +89 -0
- data/Guardfile +16 -0
- data/LICENSE +7 -0
- data/README.md +220 -0
- data/bumblebee.gemspec +30 -0
- data/lib/bumblebee/bumblebee.rb +30 -0
- data/lib/bumblebee/column.rb +96 -0
- data/lib/bumblebee/template.rb +58 -0
- data/lib/bumblebee/version.rb +12 -0
- data/lib/bumblebee.rb +10 -0
- data/spec/bumblebee/bumblebee_spec.rb +52 -0
- data/spec/bumblebee/column_spec.rb +187 -0
- data/spec/spec_helper.rb +10 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 981b25ba3c2fc7438e3c0207297a6e084ae70f30
|
4
|
+
data.tar.gz: f96a80682fdd1533f8ea017f72696bb8f47750b6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 92fdf21e7dbaadc2e9836e07de7cfa9e9113ff94ecb74756ecd0ce97b99bde0eb599764df2306f2e36813e0706acec923441ec4491ce19e43fc69c95040c083c
|
7
|
+
data.tar.gz: 27e2f192458decd10ca7834245bbcf9f913fc850d9ac9161c4346d78deb671ae6e5cff377a832758de9e26520fc4ccaad149470f3eb9624725541ee79c236496
|
data/.editorconfig
ADDED
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.7
|
data/.travis.yml
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.3.1
|
4
|
+
- 2.3.7
|
5
|
+
cache: bundler
|
6
|
+
script:
|
7
|
+
- bundle exec rubocop
|
8
|
+
- bundle exec rspec
|
9
|
+
notifications:
|
10
|
+
hipchat:
|
11
|
+
rooms:
|
12
|
+
secure: i6oRaPbul9by4YkFdGOROctO6Clgsn1Q5eTrHGn9SJeKtOtBm79ezCS3w0UBLKGcE1Cso3gMNUjzMmi/i2je9rUUIZFXUaT3COyh1LLHlPU4y6j3ZQReWb/m43AEKL0akW6eiAUnmddc96WyTL6x5zplqHKweZs/kHutAor36x+sPTNumHm5yM795HVFlmUqpLnJu3OAIEC+1EhIHxX6xG5lOveAQ7/8KnyIRJWFQI2sHwZUPyD2YM6o+kYzGTfiXixpuG7sbyN17+u3YuQc8kPWgXkk/5+VF7B0OIuEymm9rnnisGxIU0B3nrDESumdNnhbSMGrItgsmhssICZ1Dr1pa3uBr2CFT3ZbQszuQvPfq1Pavytw5+jTjAqqhq2DQCXQRZT16Mnu1eFvHyu9yWeqFECJGQ8ligDcsckvQe2GU432dAt6Dj6zEnL4CsOi/NhaGkfW+b1hnD2L9LjCV3TGoT1YHuBxP2gxd2mN7ihCaPtaXMwojhzmPClqbdxmu/v8aZv7nF9SiYzXOMG/Hj5/wtQA6dE64LufQIkn2XYxDWeh7pnjye1u0np9LHp0Qw0R8KT8uGm7kL6BRUaN2xynl6O13r8juRk2xDHGry6tse8+/zooOZkB84akZZunKGki9jh1APjza0tylTr0C139N1/VAyeB4cA+GFFRWxY=
|
data/CHANGELOG.md
ADDED
File without changes
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
bumblebee (1.0.0)
|
5
|
+
acts_as_hashable (~> 1.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
acts_as_hashable (1.0.3)
|
11
|
+
ast (2.4.0)
|
12
|
+
coderay (1.1.2)
|
13
|
+
diff-lcs (1.3)
|
14
|
+
ffi (1.9.25)
|
15
|
+
formatador (0.2.5)
|
16
|
+
guard (2.15.0)
|
17
|
+
formatador (>= 0.2.4)
|
18
|
+
listen (>= 2.7, < 4.0)
|
19
|
+
lumberjack (>= 1.0.12, < 2.0)
|
20
|
+
nenv (~> 0.1)
|
21
|
+
notiffany (~> 0.0)
|
22
|
+
pry (>= 0.9.12)
|
23
|
+
shellany (~> 0.0)
|
24
|
+
thor (>= 0.18.1)
|
25
|
+
guard-compat (1.2.1)
|
26
|
+
guard-rspec (4.7.3)
|
27
|
+
guard (~> 2.1)
|
28
|
+
guard-compat (~> 1.1)
|
29
|
+
rspec (>= 2.99.0, < 4.0)
|
30
|
+
jaro_winkler (1.5.1)
|
31
|
+
listen (3.1.5)
|
32
|
+
rb-fsevent (~> 0.9, >= 0.9.4)
|
33
|
+
rb-inotify (~> 0.9, >= 0.9.7)
|
34
|
+
ruby_dep (~> 1.2)
|
35
|
+
lumberjack (1.0.13)
|
36
|
+
method_source (0.9.2)
|
37
|
+
nenv (0.3.0)
|
38
|
+
notiffany (0.1.1)
|
39
|
+
nenv (~> 0.1)
|
40
|
+
shellany (~> 0.0)
|
41
|
+
parallel (1.12.1)
|
42
|
+
parser (2.5.3.0)
|
43
|
+
ast (~> 2.4.0)
|
44
|
+
powerpack (0.1.2)
|
45
|
+
pry (0.12.2)
|
46
|
+
coderay (~> 1.1.0)
|
47
|
+
method_source (~> 0.9.0)
|
48
|
+
rainbow (3.0.0)
|
49
|
+
rb-fsevent (0.10.3)
|
50
|
+
rb-inotify (0.9.10)
|
51
|
+
ffi (>= 0.5.0, < 2)
|
52
|
+
rspec (3.8.0)
|
53
|
+
rspec-core (~> 3.8.0)
|
54
|
+
rspec-expectations (~> 3.8.0)
|
55
|
+
rspec-mocks (~> 3.8.0)
|
56
|
+
rspec-core (3.8.0)
|
57
|
+
rspec-support (~> 3.8.0)
|
58
|
+
rspec-expectations (3.8.2)
|
59
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
60
|
+
rspec-support (~> 3.8.0)
|
61
|
+
rspec-mocks (3.8.0)
|
62
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
63
|
+
rspec-support (~> 3.8.0)
|
64
|
+
rspec-support (3.8.0)
|
65
|
+
rubocop (0.59.2)
|
66
|
+
jaro_winkler (~> 1.5.1)
|
67
|
+
parallel (~> 1.10)
|
68
|
+
parser (>= 2.5, != 2.5.1.1)
|
69
|
+
powerpack (~> 0.1)
|
70
|
+
rainbow (>= 2.2.2, < 4.0)
|
71
|
+
ruby-progressbar (~> 1.7)
|
72
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
73
|
+
ruby-progressbar (1.10.0)
|
74
|
+
ruby_dep (1.5.0)
|
75
|
+
shellany (0.0.1)
|
76
|
+
thor (0.20.3)
|
77
|
+
unicode-display_width (1.4.0)
|
78
|
+
|
79
|
+
PLATFORMS
|
80
|
+
ruby
|
81
|
+
|
82
|
+
DEPENDENCIES
|
83
|
+
bumblebee!
|
84
|
+
guard-rspec (~> 4.7)
|
85
|
+
rspec (~> 3.8)
|
86
|
+
rubocop (~> 0.59)
|
87
|
+
|
88
|
+
BUNDLED WITH
|
89
|
+
1.17.1
|
data/Guardfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
4
|
+
require 'guard/rspec/dsl'
|
5
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
6
|
+
|
7
|
+
# RSpec files
|
8
|
+
rspec = dsl.rspec
|
9
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
10
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
11
|
+
watch(rspec.spec_files)
|
12
|
+
|
13
|
+
# Ruby files
|
14
|
+
ruby = dsl.ruby
|
15
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
16
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright 2018 Blue Marble Payroll, LLC
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
# Bumblebee
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/bluemarblepayroll/bumblebee.svg?branch=master)](https://travis-ci.org/bluemarblepayroll/bumblebee)
|
4
|
+
|
5
|
+
Higher level languages, such as Ruby, make interacting with CSVs trivial. Even so, this library provides a very simple object/csv mapper that allows you to fully interact with CSVs in a declarative way. Locking in common patterns, even in higher level languages, is important in large codebases. Using a library such as this will help ensure standardization around CSV interaction.
|
6
|
+
|
7
|
+
There are situations where this may not be appropriate. This library is not meant to be extremely performant given large files and/or datasets. This library shines with CSV and/or data-sets of less than 100,000 records (approx.)
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
To install through Rubygems:
|
12
|
+
|
13
|
+
````
|
14
|
+
gem install install bumblebee
|
15
|
+
````
|
16
|
+
|
17
|
+
You can also add this to your Gemfile:
|
18
|
+
|
19
|
+
````
|
20
|
+
bundle add bumblebee
|
21
|
+
````
|
22
|
+
|
23
|
+
## Examples
|
24
|
+
|
25
|
+
### A Simple 1:1 Example
|
26
|
+
|
27
|
+
Imagine the following CSV:
|
28
|
+
|
29
|
+
id | name | dob | phone
|
30
|
+
-- | ---- | ---------- | ------------
|
31
|
+
1 | Matt | 1901-02-03 | 555-555-5555
|
32
|
+
2 | Nick | 1921-09-03 | 444-444-4444
|
33
|
+
3 | Sam | 1932-12-12 | 333-333-3333
|
34
|
+
|
35
|
+
Using the following column configuration:
|
36
|
+
|
37
|
+
````
|
38
|
+
columns = [
|
39
|
+
{ field: :id },
|
40
|
+
{ field: :name },
|
41
|
+
{ field: :dob },
|
42
|
+
{ field: :phone }
|
43
|
+
]
|
44
|
+
````
|
45
|
+
|
46
|
+
We could parse this data and turn it into hashes:
|
47
|
+
|
48
|
+
````
|
49
|
+
objects = ::Bumblebee.parse_csv(columns, data)
|
50
|
+
````
|
51
|
+
|
52
|
+
*Note: Data, in this case, would be the read CSV file contents in string format.*
|
53
|
+
|
54
|
+
The variable `objects` would now be an array of hash objects.
|
55
|
+
|
56
|
+
### Custom Headers
|
57
|
+
|
58
|
+
If our headers are not a perfect 1:1 match to our object, such as:
|
59
|
+
|
60
|
+
ID # | First Name | Date of Birth | Phone #
|
61
|
+
---- | ---------- | ------------- | ------------
|
62
|
+
1 | Matt | 1901-02-03 | 555-555-5555
|
63
|
+
2 | Nick | 1921-09-03 | 444-444-4444
|
64
|
+
3 | Sam | 1932-12-12 | 333-333-3333
|
65
|
+
|
66
|
+
Then we can explicitly map those as:
|
67
|
+
|
68
|
+
````
|
69
|
+
columns = [
|
70
|
+
{ field: :id, header: 'ID #' },
|
71
|
+
{ field: :name, header: 'First Name' },
|
72
|
+
{ field: :dob, header: 'Date of Birth' },
|
73
|
+
{ field: :phone, header: 'Phone #' }
|
74
|
+
]
|
75
|
+
````
|
76
|
+
|
77
|
+
### Nested Objects
|
78
|
+
|
79
|
+
Let's say we have the following data which we want to create a CSV from:
|
80
|
+
|
81
|
+
````
|
82
|
+
objects = [
|
83
|
+
{
|
84
|
+
id: 1,
|
85
|
+
name: { first: 'Matt' },
|
86
|
+
demo: { dob: '1901-02-03' },
|
87
|
+
contact: { phone: '555-555-5555' }
|
88
|
+
},
|
89
|
+
{
|
90
|
+
id: 2,
|
91
|
+
name: { first: 'Nick' },
|
92
|
+
demo: { dob: '1921-09-03' },
|
93
|
+
contact: { phone: '444-444-4444' }
|
94
|
+
},
|
95
|
+
{
|
96
|
+
id: 3,
|
97
|
+
name: { first: 'Sam' },
|
98
|
+
demo: { dob: '1932-12-12' },
|
99
|
+
contact: { phone: '333-333-3333' }
|
100
|
+
}
|
101
|
+
]
|
102
|
+
````
|
103
|
+
|
104
|
+
We could create a flat-file CSV:
|
105
|
+
|
106
|
+
ID # | First Name | Date of Birth | Phone #
|
107
|
+
---- | ---------- | ------------- | ------------
|
108
|
+
1 | Matt | 1901-02-03 | 555-555-5555
|
109
|
+
2 | Nick | 1921-09-03 | 444-444-4444
|
110
|
+
3 | Sam | 1932-12-12 | 333-333-3333
|
111
|
+
|
112
|
+
Using the following column config:
|
113
|
+
|
114
|
+
````
|
115
|
+
columns = [
|
116
|
+
{ field: :id, header: 'ID #' },
|
117
|
+
{ field: [:name, :first], header: 'First Name' },
|
118
|
+
{ field: [:demo, :dob], header: 'Date of Birth' },
|
119
|
+
{ field: [:contact, :phone], header: 'Phone #' }
|
120
|
+
]
|
121
|
+
````
|
122
|
+
|
123
|
+
And executing the following:
|
124
|
+
|
125
|
+
````
|
126
|
+
csv = ::Bumblebee.generate_csv(columns, objects)
|
127
|
+
````
|
128
|
+
|
129
|
+
The above columns config would work both ways, so if we received the CSV, we could parse it to an array of nested hashes. Unfortunately, for now, we cannot do better than an array of nested hashes.
|
130
|
+
|
131
|
+
### Custom Formatting
|
132
|
+
|
133
|
+
You can also pass in functions that can do the value formatting. For example:
|
134
|
+
|
135
|
+
````
|
136
|
+
columns = [
|
137
|
+
{
|
138
|
+
field: :id,
|
139
|
+
header: 'ID #'
|
140
|
+
},
|
141
|
+
{
|
142
|
+
field: :name,
|
143
|
+
header: 'First Name',
|
144
|
+
to_csv: [:name, :first, ->(o) { o.to_s.upcase }]
|
145
|
+
},
|
146
|
+
{
|
147
|
+
field: :dob,
|
148
|
+
header: 'Date of Birth',
|
149
|
+
to_csv: [:demo, :dob]
|
150
|
+
},
|
151
|
+
{
|
152
|
+
field: :phone,
|
153
|
+
header: 'Phone #',
|
154
|
+
to_csv: [:contact, :phone]
|
155
|
+
}
|
156
|
+
]
|
157
|
+
````
|
158
|
+
|
159
|
+
would ensure the CSV has only upper-case `First Name` values.
|
160
|
+
|
161
|
+
#### Further CSV Customization
|
162
|
+
|
163
|
+
The two main methods:
|
164
|
+
|
165
|
+
* generate_csv
|
166
|
+
* parse_csv
|
167
|
+
|
168
|
+
also accept custom options that [Ruby's CSV::new](https://ruby-doc.org/stdlib-2.6/libdoc/csv/rdoc/CSV.html#method-c-new) accepts. The only caveat is that Bumblebee needs headers for its mapping, so it overrides the header options.
|
169
|
+
|
170
|
+
## Contributing
|
171
|
+
|
172
|
+
### Development Environment Configuration
|
173
|
+
|
174
|
+
Basic steps to take to get this repository compiling:
|
175
|
+
|
176
|
+
1. Install [Ruby](https://www.ruby-lang.org/en/documentation/installation/) (check bumblebee.gemspec for versions supported)
|
177
|
+
2. Install bundler (gem install bundler)
|
178
|
+
3. Clone the repository (git clone git@github.com:bluemarblepayroll/bumblebee.git)
|
179
|
+
4. Navigate to the root folder (cd bumblebee)
|
180
|
+
5. Install dependencies (bundle)
|
181
|
+
|
182
|
+
### Running Tests
|
183
|
+
|
184
|
+
To execute the test suite run:
|
185
|
+
|
186
|
+
````
|
187
|
+
bundle exec rspec spec --format documentation
|
188
|
+
````
|
189
|
+
|
190
|
+
Alternatively, you can have Guard watch for changes:
|
191
|
+
|
192
|
+
````
|
193
|
+
bundle exec guard
|
194
|
+
````
|
195
|
+
|
196
|
+
Also, do not forget to run Rubocop:
|
197
|
+
|
198
|
+
````
|
199
|
+
bundle exec rubocop
|
200
|
+
````
|
201
|
+
|
202
|
+
### Publishing
|
203
|
+
|
204
|
+
Note: ensure you have proper authorization before trying to publish new versions.
|
205
|
+
|
206
|
+
After code changes have successfully gone through the Pull Request review process then the following steps should be followed for publishing new versions:
|
207
|
+
|
208
|
+
1. Merge Pull Request into master
|
209
|
+
2. Update [lib/bumblebee/version.rb](https://github.com/bluemarblepayroll/bumblebee/blob/master/lib/bumblebee/version.rb) [version number](https://semver.org/)
|
210
|
+
3. Bundle
|
211
|
+
4. Update CHANGELOG.md
|
212
|
+
5. Commit & Push master to remote and ensure CI builds master successfully
|
213
|
+
6. Build the project locally: `gem build bumblebee`
|
214
|
+
7. Publish package to NPM: `gem push bumblebee-X.gem` where X is the version to push
|
215
|
+
8. Tag master with new version: `git tag <version>`
|
216
|
+
9. Push tags remotely: `git push origin --tags`
|
217
|
+
|
218
|
+
## License
|
219
|
+
|
220
|
+
This project is MIT Licensed.
|
data/bumblebee.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require './lib/bumblebee/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'bumblebee'
|
7
|
+
s.version = Bumblebee::VERSION
|
8
|
+
s.summary = 'Object/CSV Mapper'
|
9
|
+
|
10
|
+
s.description = <<-DESCRIPTION
|
11
|
+
Higher level languages, such as Ruby, make interacting with CSVs trivial.
|
12
|
+
Even so, this library provides a very simple object/csv mapper that allows you to fully interact with CSVs in a declarative way.
|
13
|
+
DESCRIPTION
|
14
|
+
|
15
|
+
s.authors = ['Matthew Ruggio']
|
16
|
+
s.email = ['mruggio@bluemarblepayroll.com']
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
20
|
+
s.homepage = 'https://github.com/bluemarblepayroll/bumblebee'
|
21
|
+
s.license = 'MIT'
|
22
|
+
|
23
|
+
s.required_ruby_version = '>= 2.3.1'
|
24
|
+
|
25
|
+
s.add_dependency('acts_as_hashable', '~>1.0')
|
26
|
+
|
27
|
+
s.add_development_dependency('guard-rspec', '~>4.7')
|
28
|
+
s.add_development_dependency('rspec', '~> 3.8')
|
29
|
+
s.add_development_dependency('rubocop', '~> 0.59')
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'csv'
|
11
|
+
require 'acts_as_hashable'
|
12
|
+
require 'ostruct'
|
13
|
+
|
14
|
+
require_relative 'column'
|
15
|
+
require_relative 'template'
|
16
|
+
|
17
|
+
# The top-level module provides the two main methods for convenience.
|
18
|
+
# You can also consume these in a more OOP way using the Template class or a more
|
19
|
+
# procedural way using these.
|
20
|
+
module Bumblebee
|
21
|
+
class << self
|
22
|
+
def generate_csv(columns, objects, options = {})
|
23
|
+
::Bumblebee::Template.new(columns).generate_csv(objects, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_csv(columns, string, options = {})
|
27
|
+
::Bumblebee::Template.new(columns).parse_csv(string, options)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
module Bumblebee
|
11
|
+
# This is main piece of logic that defines a column which can go from objects to csv cell values
|
12
|
+
# and csv rows to objects.
|
13
|
+
class Column
|
14
|
+
acts_as_hashable
|
15
|
+
|
16
|
+
attr_reader :field,
|
17
|
+
:to_object,
|
18
|
+
:header,
|
19
|
+
:to_csv
|
20
|
+
|
21
|
+
def initialize(field:, header: nil, to_csv: nil, to_object: nil)
|
22
|
+
raise ArgumentError, 'field is required' unless field
|
23
|
+
|
24
|
+
@field = field
|
25
|
+
@header = make_header(header || field)
|
26
|
+
@to_csv = Array(to_csv || field)
|
27
|
+
@to_object = Array(to_object || field)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Take a object and convert to a value.
|
31
|
+
def object_to_csv(object)
|
32
|
+
val = object
|
33
|
+
|
34
|
+
# Iterate over keys until we reach a nil or the end of keys.
|
35
|
+
to_csv.each do |f|
|
36
|
+
# short-circuit out of the extract method
|
37
|
+
return nil unless val
|
38
|
+
|
39
|
+
val = single_extract(val, f)
|
40
|
+
end
|
41
|
+
|
42
|
+
val
|
43
|
+
end
|
44
|
+
|
45
|
+
def csv_to_object(csv_hash)
|
46
|
+
return nil unless csv_hash
|
47
|
+
|
48
|
+
value = csv_hash[header]
|
49
|
+
pointer = hash = {}
|
50
|
+
|
51
|
+
to_object[0..-2].each do |f|
|
52
|
+
if f.is_a?(Proc)
|
53
|
+
value = f.call(value)
|
54
|
+
else
|
55
|
+
pointer = pointer[f] = {}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
pointer[to_object[-1]] = value
|
60
|
+
|
61
|
+
hash
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# Loop through all values, contcatenating their string equivalent with an underscore.
|
67
|
+
# Since we allow procs, but we want deterministic headers, we will simply transform
|
68
|
+
# theem to _proc_. This is not perfect and could create naming clashes, but it really
|
69
|
+
# should not be. You should really leave proc's out of header and field.
|
70
|
+
def make_header(value)
|
71
|
+
Array(value).map do |v|
|
72
|
+
if v.is_a?(Proc)
|
73
|
+
'proc'
|
74
|
+
else
|
75
|
+
v.to_s
|
76
|
+
end
|
77
|
+
end.join('_')
|
78
|
+
end
|
79
|
+
|
80
|
+
# Take an object and attempt to extract a value from it.
|
81
|
+
# First, see if the key is a proc, if so, then delegate to the proc.
|
82
|
+
# Next, see if the object responds to the key, if so, then call it.
|
83
|
+
# Lastly, see if it responds to brackets, if so, then call the brackets method sending
|
84
|
+
# in the key.
|
85
|
+
# Finally if all else fails, return nil.
|
86
|
+
def single_extract(object, key)
|
87
|
+
if key.is_a?(Proc)
|
88
|
+
key.call(object)
|
89
|
+
elsif object.respond_to?(key)
|
90
|
+
object.send(key)
|
91
|
+
elsif object.respond_to?(:[])
|
92
|
+
object[key]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
module Bumblebee
|
11
|
+
# Wraps up columns and provides to main methods:
|
12
|
+
# generate_csv: take in an array of objects and return a string (CSV contents)
|
13
|
+
# parse_csv: take in a string and return an array of OpenStruct objects
|
14
|
+
class Template
|
15
|
+
attr_reader :columns
|
16
|
+
|
17
|
+
def initialize(columns = [])
|
18
|
+
@columns = ::Bumblebee::Column.array(columns)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Return array of strings (headers)
|
22
|
+
def headers
|
23
|
+
columns.map(&:header)
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_csv(objects, options = {})
|
27
|
+
objects = objects.is_a?(Hash) ? [objects] : Array(objects)
|
28
|
+
|
29
|
+
CSV.generate(make_options(options)) do |csv|
|
30
|
+
objects.each do |object|
|
31
|
+
row = columns.map { |column| column.object_to_csv(object) }
|
32
|
+
|
33
|
+
csv << row
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_csv(string, options = {})
|
39
|
+
csv = CSV.new(string, make_options(options))
|
40
|
+
|
41
|
+
# Drop the first record, it is the header record
|
42
|
+
csv.to_a[1..-1].map do |row|
|
43
|
+
# Build up a hash using the column one at a time
|
44
|
+
extracted_hash = columns.inject({}) do |hash, column|
|
45
|
+
hash.merge(column.csv_to_object(row))
|
46
|
+
end
|
47
|
+
|
48
|
+
extracted_hash
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def make_options(options = {})
|
55
|
+
options.merge(headers: headers, write_headers: true)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
module Bumblebee
|
11
|
+
VERSION = '1.0.0'
|
12
|
+
end
|
data/lib/bumblebee.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
require_relative 'bumblebee/bumblebee'
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
require './spec/spec_helper'
|
11
|
+
|
12
|
+
describe ::Bumblebee do
|
13
|
+
let(:columns) do
|
14
|
+
[
|
15
|
+
{ field: :name },
|
16
|
+
{ field: :dob }
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:people) do
|
21
|
+
[
|
22
|
+
{ name: 'Matt', dob: '1901-01-03' },
|
23
|
+
{ name: 'Nathan', dob: '1931-09-03' }
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
let(:csv) { "name,dob\nMatt,1901-01-03\nNathan,1931-09-03\n" }
|
28
|
+
|
29
|
+
let(:quoted_csv) { "\"name\",\"dob\"\n\"Matt\",\"1901-01-03\"\n\"Nathan\",\"1931-09-03\"\n" }
|
30
|
+
|
31
|
+
it 'should generate a csv' do
|
32
|
+
actual = ::Bumblebee.generate_csv(columns, people)
|
33
|
+
|
34
|
+
expect(actual).to eq(csv)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should generate a csv and accept options' do
|
38
|
+
options = {
|
39
|
+
force_quotes: true
|
40
|
+
}
|
41
|
+
|
42
|
+
actual = ::Bumblebee.generate_csv(columns, people, options)
|
43
|
+
|
44
|
+
expect(actual).to eq(quoted_csv)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should parse a csv' do
|
48
|
+
objects = ::Bumblebee.parse_csv(columns, csv)
|
49
|
+
|
50
|
+
expect(objects).to eq(people)
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2018-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'spec_helper'
|
11
|
+
|
12
|
+
describe ::Bumblebee::Column do
|
13
|
+
let(:object) do
|
14
|
+
OpenStruct.new(
|
15
|
+
name: 'Mattycakes',
|
16
|
+
dob: '1921-01-02',
|
17
|
+
pizza: 'Pepperoni',
|
18
|
+
license: OpenStruct.new(id: '123456')
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:hash) do
|
23
|
+
{
|
24
|
+
name: 'Mattycakes',
|
25
|
+
dob: '1921-01-02',
|
26
|
+
pizza: 'Pepperoni',
|
27
|
+
license: { id: '123456' }
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'initialization' do
|
32
|
+
it 'should error if field is nil' do
|
33
|
+
expect { ::Bumblebee::Column.new(field: nil) }.to raise_error(ArgumentError)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should initialize with just a field' do
|
37
|
+
field = :name
|
38
|
+
|
39
|
+
column = ::Bumblebee::Column.new(field: field)
|
40
|
+
|
41
|
+
expect(column.field).to eq(field)
|
42
|
+
expect(column.header).to eq(field.to_s)
|
43
|
+
expect(column.to_csv).to eq([field])
|
44
|
+
expect(column.to_object).to eq([field])
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'header computation' do
|
48
|
+
it 'should compute from single field' do
|
49
|
+
field = :name
|
50
|
+
|
51
|
+
column = ::Bumblebee::Column.new(field: field)
|
52
|
+
|
53
|
+
expect(column.header).to eq(field.to_s)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should compute from multiple fields' do
|
57
|
+
field = [:name, 'is', :too, 22.4]
|
58
|
+
|
59
|
+
column = ::Bumblebee::Column.new(field: field)
|
60
|
+
|
61
|
+
expect(column.header).to eq(field.map(&:to_s).join('_'))
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should compute from a lambda literal' do
|
65
|
+
column = ::Bumblebee::Column.new(field: ->(o) {})
|
66
|
+
expect(column.header).to eq('proc')
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should compute from a proc' do
|
70
|
+
column = ::Bumblebee::Column.new(field: proc {})
|
71
|
+
expect(column.header).to eq('proc')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#csv_to_object' do
|
77
|
+
it 'should correctly extract the value using field' do
|
78
|
+
record = {
|
79
|
+
'name' => 'Nathan'
|
80
|
+
}
|
81
|
+
|
82
|
+
column = ::Bumblebee::Column.new(field: 'name')
|
83
|
+
|
84
|
+
expect(column.csv_to_object(record)).to eq(record)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should correctly extract the value using header' do
|
88
|
+
csv_row = {
|
89
|
+
'First Name' => 'Nathan'
|
90
|
+
}
|
91
|
+
|
92
|
+
record = {
|
93
|
+
'name' => 'Nathan'
|
94
|
+
}
|
95
|
+
|
96
|
+
column = ::Bumblebee::Column.new(field: 'name', header: 'First Name')
|
97
|
+
|
98
|
+
expect(column.csv_to_object(csv_row)).to eq(record)
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should correctly extract the value using custom from_csv value' do
|
102
|
+
csv_row = {
|
103
|
+
'First Name' => 'Nathan'
|
104
|
+
}
|
105
|
+
|
106
|
+
record = {
|
107
|
+
'First' => 'Nathan'
|
108
|
+
}
|
109
|
+
|
110
|
+
column = ::Bumblebee::Column.new(
|
111
|
+
field: 'name',
|
112
|
+
header: 'First Name',
|
113
|
+
to_object: 'First'
|
114
|
+
)
|
115
|
+
|
116
|
+
expect(column.csv_to_object(csv_row)).to eq(record)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe '#object_to_csv' do
|
121
|
+
context 'using field' do
|
122
|
+
context 'for single values' do
|
123
|
+
it 'should get csv value correctly' do
|
124
|
+
column = ::Bumblebee::Column.new(field: :name)
|
125
|
+
|
126
|
+
expect(column.object_to_csv(object)).to eq(object.name)
|
127
|
+
expect(column.object_to_csv(hash)).to eq(hash[:name])
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'should return nil when does not exist' do
|
131
|
+
column = ::Bumblebee::Column.new(field: :doesnt_exist)
|
132
|
+
|
133
|
+
expect(column.object_to_csv(object)).to eq(nil)
|
134
|
+
expect(column.object_to_csv(hash)).to eq(nil)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'for arrays' do
|
139
|
+
it 'should get csv value correctly' do
|
140
|
+
column = ::Bumblebee::Column.new(field: %i[license id])
|
141
|
+
|
142
|
+
expect(column.object_to_csv(object)).to eq(object.license.id)
|
143
|
+
expect(column.object_to_csv(hash)).to eq(hash[:license][:id])
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'should return nil when it hits dead end at beginning' do
|
147
|
+
column = ::Bumblebee::Column.new(field: %i[something that does not exist])
|
148
|
+
|
149
|
+
expect(column.object_to_csv(object)).to eq(nil)
|
150
|
+
expect(column.object_to_csv(hash)).to eq(nil)
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'should return nil when it hits dead end in middle' do
|
154
|
+
column = ::Bumblebee::Column.new(field: %i[license doesnt_exist here])
|
155
|
+
|
156
|
+
expect(column.object_to_csv(object)).to eq(nil)
|
157
|
+
expect(column.object_to_csv(hash)).to eq(nil)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
context 'when mixing in procs' do
|
162
|
+
it 'should get csv value correctly when proc runs against end value' do
|
163
|
+
column = ::Bumblebee::Column.new(field: [:license, :id, ->(o) { "# #{o}" }])
|
164
|
+
|
165
|
+
expect(column.object_to_csv(object)).to eq("# #{object.license.id}")
|
166
|
+
expect(column.object_to_csv(hash)).to eq("# #{hash[:license][:id]}")
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'should get csv value correctly when proc runs against object-based value' do
|
170
|
+
column = ::Bumblebee::Column.new(field: [:license, ->(o) { "# #{o.id}" }])
|
171
|
+
expect(column.object_to_csv(object)).to eq("# #{object.license.id}")
|
172
|
+
|
173
|
+
column = ::Bumblebee::Column.new(field: [:license, ->(o) { "# #{o[:id]}" }])
|
174
|
+
expect(column.object_to_csv(hash)).to eq("# #{hash[:license][:id]}")
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'should not hit proc if ran against nil' do
|
178
|
+
column = ::Bumblebee::Column.new(field: [:doesnt_exist, ->(o) { "# #{o.id}" }])
|
179
|
+
expect(column.object_to_csv(object)).to eq(nil)
|
180
|
+
|
181
|
+
column = ::Bumblebee::Column.new(field: [:doesnt_exist, ->(o) { "# #{o[:id]}" }])
|
182
|
+
expect(column.object_to_csv(hash)).to eq(nil)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bumblebee
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthew Ruggio
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-12-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: acts_as_hashable
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: guard-rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.8'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.59'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.59'
|
69
|
+
description: |2
|
70
|
+
Higher level languages, such as Ruby, make interacting with CSVs trivial.
|
71
|
+
Even so, this library provides a very simple object/csv mapper that allows you to fully interact with CSVs in a declarative way.
|
72
|
+
email:
|
73
|
+
- mruggio@bluemarblepayroll.com
|
74
|
+
executables: []
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- ".editorconfig"
|
79
|
+
- ".gitignore"
|
80
|
+
- ".rubocop.yml"
|
81
|
+
- ".ruby-version"
|
82
|
+
- ".travis.yml"
|
83
|
+
- CHANGELOG.md
|
84
|
+
- Gemfile
|
85
|
+
- Gemfile.lock
|
86
|
+
- Guardfile
|
87
|
+
- LICENSE
|
88
|
+
- README.md
|
89
|
+
- bumblebee.gemspec
|
90
|
+
- lib/bumblebee.rb
|
91
|
+
- lib/bumblebee/bumblebee.rb
|
92
|
+
- lib/bumblebee/column.rb
|
93
|
+
- lib/bumblebee/template.rb
|
94
|
+
- lib/bumblebee/version.rb
|
95
|
+
- spec/bumblebee/bumblebee_spec.rb
|
96
|
+
- spec/bumblebee/column_spec.rb
|
97
|
+
- spec/spec_helper.rb
|
98
|
+
homepage: https://github.com/bluemarblepayroll/bumblebee
|
99
|
+
licenses:
|
100
|
+
- MIT
|
101
|
+
metadata: {}
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.3.1
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 2.5.2.3
|
119
|
+
signing_key:
|
120
|
+
specification_version: 4
|
121
|
+
summary: Object/CSV Mapper
|
122
|
+
test_files:
|
123
|
+
- spec/bumblebee/bumblebee_spec.rb
|
124
|
+
- spec/bumblebee/column_spec.rb
|
125
|
+
- spec/spec_helper.rb
|