csv_row_model 0.1.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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/.yardopts +10 -0
- data/CONTRIBUTING.md +7 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +369 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/csv_row_model.gemspec +23 -0
- data/lib/csv_row_model.rb +46 -0
- data/lib/csv_row_model/concerns/deep_class_var.rb +86 -0
- data/lib/csv_row_model/concerns/inspect.rb +10 -0
- data/lib/csv_row_model/engine.rb +5 -0
- data/lib/csv_row_model/export.rb +70 -0
- data/lib/csv_row_model/export/csv.rb +43 -0
- data/lib/csv_row_model/export/single_model.rb +47 -0
- data/lib/csv_row_model/import.rb +125 -0
- data/lib/csv_row_model/import/attributes.rb +105 -0
- data/lib/csv_row_model/import/csv.rb +136 -0
- data/lib/csv_row_model/import/csv/row.rb +17 -0
- data/lib/csv_row_model/import/file.rb +133 -0
- data/lib/csv_row_model/import/file/callbacks.rb +16 -0
- data/lib/csv_row_model/import/file/validations.rb +39 -0
- data/lib/csv_row_model/import/presenter.rb +160 -0
- data/lib/csv_row_model/import/single_model.rb +41 -0
- data/lib/csv_row_model/model.rb +68 -0
- data/lib/csv_row_model/model/children.rb +79 -0
- data/lib/csv_row_model/model/columns.rb +73 -0
- data/lib/csv_row_model/model/csv_string_model.rb +16 -0
- data/lib/csv_row_model/model/single_model.rb +16 -0
- data/lib/csv_row_model/validators/boolean_format.rb +11 -0
- data/lib/csv_row_model/validators/date_format.rb +9 -0
- data/lib/csv_row_model/validators/default_change.rb +6 -0
- data/lib/csv_row_model/validators/float_format.rb +9 -0
- data/lib/csv_row_model/validators/integer_format.rb +9 -0
- data/lib/csv_row_model/validators/number_validator.rb +16 -0
- data/lib/csv_row_model/validators/validate_attributes.rb +27 -0
- data/lib/csv_row_model/version.rb +3 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fc063f24b8e2038004c3542d79510f7ace7c0c1e
|
4
|
+
data.tar.gz: 109db80efb39dbad55894fdaefe2f0e83c2920c3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fecf7a046226eb066808d1773129f0a5487c658fcc07b9428317a1309b05304349c3ac2fc365fae8ca6753d96665b145d757814d22d9a8f829ec0a5be1f37a7c
|
7
|
+
data.tar.gz: 1e5b55b8784e2407d78e89a5f0b9d09fbcec78b80942e961d87c553e251e008ad17c149f9e006b85982f09d43f372cc4498bd73cc2a9f066b88080bcfd1e4452
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
csv_row_model
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.2
|
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
1. Fork it ( https://github.com/[my-github-username]/csv_row_model/fork )
|
4
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
5
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
6
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
7
|
+
5. Create a new Pull Request
|
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in csv_row_model.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem "bundler", "> 1.3"
|
7
|
+
gem "rake", "~> 10.0"
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem "rspec"
|
11
|
+
gem "codeclimate-test-reporter"
|
12
|
+
gem 'pry-byebug'
|
13
|
+
end
|
14
|
+
|
15
|
+
group :development do
|
16
|
+
gem "yard"
|
17
|
+
gem "redcarpet"
|
18
|
+
gem "yard-activesupport-concern"
|
19
|
+
gem "yard-delegate"
|
20
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Steve Chung
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,369 @@
|
|
1
|
+
# CsvRowModel [](https://travis-ci.org/FinalCAD/csv_row_model) [](https://codeclimate.com/github/FinalCAD/csv_row_model) [](https://codeclimate.com/github/FinalCAD/csv_row_model/coverage)
|
2
|
+
|
3
|
+
Import and export your custom CSVs with a intuitive shared Ruby interface.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'csv_row_model'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install csv_row_model
|
20
|
+
|
21
|
+
## RowModel
|
22
|
+
|
23
|
+
Define your `RowModel`.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class ProjectRowModel
|
27
|
+
include CsvRowModel::Model
|
28
|
+
|
29
|
+
# column indices are tracked with each call
|
30
|
+
column :id
|
31
|
+
column :name
|
32
|
+
column :owner_id, header: 'Project Manager' # optional header String, that allows to modify the header of the colmnun
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
## Import
|
37
|
+
|
38
|
+
Automagically maps each column of a CSV row to an attribute of the `RowModel`.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class ProjectImportRowModel < ProjectRowModel
|
42
|
+
include CsvRowModel::Import
|
43
|
+
|
44
|
+
def name
|
45
|
+
# mapped_row is raw
|
46
|
+
# the calculated original_attribute[:name] is accessible as well
|
47
|
+
mapped_row[:name].upcase
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
And to import:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
|
56
|
+
row_model = import_file.next
|
57
|
+
|
58
|
+
row_model.header # => ["id", "name"]
|
59
|
+
|
60
|
+
row_model.source_row # => ["1", "Some Project Name"]
|
61
|
+
row_model.mapped_row # => { id: "1", name: "Some Project Name" }
|
62
|
+
|
63
|
+
row_model.id # => 1
|
64
|
+
row_model.name # => "SOME PROJECT NAME"
|
65
|
+
```
|
66
|
+
|
67
|
+
`Import::File` also provides the `RowModel` with the previous `RowModel` instance:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
row_model.previous # => <ProjectImportRowModel instance>
|
71
|
+
row_model.previous.previous # => nil, save memory by avoiding a linked list
|
72
|
+
```
|
73
|
+
|
74
|
+
## Presenter
|
75
|
+
For complex rows, you can wrap your `RowModel` with a presenter:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class ProjectImportRowModel < ProjectRowModel
|
79
|
+
include CsvRowModel::Import
|
80
|
+
|
81
|
+
# same as above
|
82
|
+
|
83
|
+
presenter do
|
84
|
+
# define your presenter here
|
85
|
+
|
86
|
+
# this is shorthand for the psuedo_code:
|
87
|
+
# def project
|
88
|
+
# return if row_model.id.invalid? || row_model.name.invalid?
|
89
|
+
#
|
90
|
+
# # turn off memoziation with `memoize: false` option
|
91
|
+
# @project ||= __the_code_inside_the_block__
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# and the psuedo_code:
|
95
|
+
# def valid?
|
96
|
+
# super # calls ActiveModel::Errors code
|
97
|
+
# errors.delete(:project) if row_model.id.invalid? || row_model.name.invalid?
|
98
|
+
# errors.empty?
|
99
|
+
# end
|
100
|
+
attribute :project, dependencies: [:id, :name] do
|
101
|
+
project = Project.where(id: row_model.id).first
|
102
|
+
|
103
|
+
# project not found, invalid.
|
104
|
+
return unless project
|
105
|
+
|
106
|
+
project.name = row_model.name
|
107
|
+
project
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Importing is the same
|
113
|
+
import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
|
114
|
+
row_model = import_file.next
|
115
|
+
presenter = row_model.presenter
|
116
|
+
|
117
|
+
presenter.row_model # gets the row model underneath
|
118
|
+
import_mapper.project.name == presenter.row_model.name # => "SOME PROJECT NAME"
|
119
|
+
```
|
120
|
+
|
121
|
+
The presenters are designed for another layer of validation---such as with the database.
|
122
|
+
|
123
|
+
Also, the `attribute` defines a dynamic `#project` method that:
|
124
|
+
|
125
|
+
1. Memoizes by default, turn off with `memoize: false` option
|
126
|
+
2. All errors of `row_model` are propagated to the presenter when calling `presenter.valid?`
|
127
|
+
3. Handles dependencies. When any of the dependencies are `invalid?`:
|
128
|
+
- The attribute block is not called and the attribute returns `nil`.
|
129
|
+
- `presenter.errors` for dependencies are cleaned. For the example above, if `row_model.id/name` are `invalid?`, then
|
130
|
+
the `:project` key is removed from the errors, so: `import_mapper.errors.keys # => [:id, :name]`
|
131
|
+
|
132
|
+
## Children
|
133
|
+
|
134
|
+
Child `RowModel` relationships can also be defined:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
class UserImportRowModel
|
138
|
+
include CsvRowModel::Model
|
139
|
+
include CsvRowModel::Import
|
140
|
+
|
141
|
+
column :id
|
142
|
+
column :name
|
143
|
+
column :email
|
144
|
+
|
145
|
+
# uses ProjectImportRowModel#valid? to detect the child row
|
146
|
+
has_many :projects, ProjectImportRowModel
|
147
|
+
end
|
148
|
+
|
149
|
+
import_file = CsvRowModel::Import::File.new(file_path, UserImportRowModel)
|
150
|
+
row_model = import_file.next
|
151
|
+
row_model.projects # => [<ProjectImportRowModel>, ...]
|
152
|
+
```
|
153
|
+
|
154
|
+
## Column Options
|
155
|
+
### Default Attributes
|
156
|
+
For `Import`, `default_attributes` are calculated as thus:
|
157
|
+
- `format_cell`
|
158
|
+
- if `value_from_format_cell.blank?`, `default_lambda.call` or nil
|
159
|
+
- otherwise, `parse_lambda.call`
|
160
|
+
|
161
|
+
#### Format Cell
|
162
|
+
Override the `format_cell` method to clean/format every cell:
|
163
|
+
```ruby
|
164
|
+
class ProjectImportRowModel < ProjectRowModel
|
165
|
+
include CsvRowModel::Import
|
166
|
+
class << self
|
167
|
+
def format_cell(cell, column_name, column_index)
|
168
|
+
cell = cell.strip
|
169
|
+
cell.to_i.to_s == cell ? cell.to_i : cell
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
#### Default
|
176
|
+
Called when `format_cell` is `value_from_format_cell.blank?`, it sets the default value of the cell:
|
177
|
+
```ruby
|
178
|
+
class ProjectImportRowModel < ProjectRowModel
|
179
|
+
include CsvRowModel::Import
|
180
|
+
|
181
|
+
column :id, default: 1
|
182
|
+
column :name, default: -> { get_name }
|
183
|
+
|
184
|
+
def get_name; "John Doe" end
|
185
|
+
end
|
186
|
+
row_model = ProjectImportRowModel.new(["", ""])
|
187
|
+
row_model.id # => 1
|
188
|
+
row_model.name # => "John Doe"
|
189
|
+
row_model.default_changes # => { id: ["", 1], name: ["", "John Doe"] }
|
190
|
+
|
191
|
+
```
|
192
|
+
|
193
|
+
`DefaultChangeValidator` is provided to allows to add warnings when defaults or set:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
class ProjectImportRowModel
|
197
|
+
include CsvRowModel::Model
|
198
|
+
include CsvRowModel::Input
|
199
|
+
|
200
|
+
column :id, default: 1
|
201
|
+
|
202
|
+
warnings do
|
203
|
+
validates :id, default_change: true
|
204
|
+
# validates :id, presence: true, works too. See ActiveWarnings gem for more.
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
row_model = ProjectImportRowModel.new([""])
|
209
|
+
|
210
|
+
row_model.unsafe? # => true
|
211
|
+
row_model.has_warnings? # => true, same as `#unsafe?`
|
212
|
+
row_model.warnings.full_messages # => ["Id changed by default"]
|
213
|
+
```
|
214
|
+
|
215
|
+
See [Validations](#validations) for more.
|
216
|
+
|
217
|
+
#### Type
|
218
|
+
Automatic type parsing.
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
class ProjectImportRowModel < ProjectRowModel
|
222
|
+
include CsvRowModel::Import
|
223
|
+
|
224
|
+
column :id, type: Integer
|
225
|
+
column :name, parse: ->(original_string) { parse(original_string) }
|
226
|
+
|
227
|
+
def parse(original_string)
|
228
|
+
"#{id} - #{original_string}"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
```
|
232
|
+
|
233
|
+
## Validations
|
234
|
+
|
235
|
+
Use [`ActiveModel::Validations`](http://api.rubyonrails.org/classes/ActiveModel/Validations.html)
|
236
|
+
on your `RowModel` or `Mapper`.
|
237
|
+
|
238
|
+
Included is [`ActiveWarnings`](https://github.com/s12chung/active_warnings) on `Model` and `Mapper` for warnings
|
239
|
+
(such as setting defaults), but not errors (which by default results in a skip).
|
240
|
+
|
241
|
+
`RowModel` has two validation layers on the `csv_string_model` (a model of `#mapped_row` with `::format_cell` applied) and itself:
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
class ProjectRowModel
|
245
|
+
include CsvRowModel::Model
|
246
|
+
include CsvRowModel::Import
|
247
|
+
|
248
|
+
column :id, type: Integer
|
249
|
+
|
250
|
+
# this is applied to the parsed CSV on the model
|
251
|
+
validates :id, numericality: { greater_than: 0 }
|
252
|
+
|
253
|
+
csv_string_model do
|
254
|
+
# this is applied before the parsed CSV on csv_string_model
|
255
|
+
validates :id, integer_format: true, allow_blank: true
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Applied to the String
|
260
|
+
ProjectRowModel.new(["not_a_number"])
|
261
|
+
row_model.valid? # => false
|
262
|
+
row_model.errors.full_messages # => ["Id is not a Integer format"]
|
263
|
+
|
264
|
+
# Applied to the parsed Integer
|
265
|
+
row_model = ProjectRowModel.new(["-1"])
|
266
|
+
row_model.valid? # => false
|
267
|
+
row_model.errors.full_messages # => ["Id must be greater than 0"]
|
268
|
+
```
|
269
|
+
|
270
|
+
Notice that there are validators given for different types: `Boolean`, `Date`, `Float`, `Integer`:
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
class ProjectRowModel
|
274
|
+
include CsvRowModel::Model
|
275
|
+
|
276
|
+
# the :validate_type option does the commented code below.
|
277
|
+
column :id, type: Integer, validate_type: true
|
278
|
+
|
279
|
+
# csv_string_model do
|
280
|
+
# validates :id, integer_format: true, allow_blank: true
|
281
|
+
# end
|
282
|
+
end
|
283
|
+
```
|
284
|
+
|
285
|
+
|
286
|
+
## Callbacks
|
287
|
+
`CsvRowModel::Import::File` can be subclassed to access
|
288
|
+
[`ActiveModel::Callbacks`](http://api.rubyonrails.org/classes/ActiveModel/Callbacks.html).
|
289
|
+
|
290
|
+
You can iterate through a file with the `#each` method, which calls `#next` internally:
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
CsvRowModel::Import::File.new(file_path, ProjectImportRowModel).each do |project_import_model|
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
297
|
+
Within `#each`, **Skips** and **Aborts** will be done via the `skip?` or `abort?` method on the row model,
|
298
|
+
allowing the following callbacks:
|
299
|
+
|
300
|
+
* yield - `before`, `around`, or `after` the iteration yield
|
301
|
+
* skip - `before`
|
302
|
+
* abort - `before`
|
303
|
+
|
304
|
+
and implement the callbacks:
|
305
|
+
```ruby
|
306
|
+
class ImportFile < CsvRowModel::Import::File
|
307
|
+
around_yield :logger_track
|
308
|
+
before_skip :track_skip
|
309
|
+
|
310
|
+
def logger_track(&block)
|
311
|
+
...
|
312
|
+
end
|
313
|
+
|
314
|
+
def track_skip
|
315
|
+
...
|
316
|
+
end
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
320
|
+
### Export RowModel
|
321
|
+
|
322
|
+
Maps each attribute of the `RowModel` to a column of a CSV row.
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
class ProjectExportRowModel < ProjectRowModel
|
326
|
+
include CsvRowModel::Export
|
327
|
+
|
328
|
+
# Optionally it's possible to override the attribute method, by default it
|
329
|
+
# does source_model.public_send(attribute)
|
330
|
+
def name
|
331
|
+
"#{source_model.id} - #{source_model.name}"
|
332
|
+
end
|
333
|
+
end
|
334
|
+
```
|
335
|
+
|
336
|
+
### Export SingleModel
|
337
|
+
|
338
|
+
Maps each attribute of the `RowModel` to a row on the CSV.
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
class ProjectExportRowModel < ProjectRowModel
|
342
|
+
include CsvRowModel::Export
|
343
|
+
include CsvRowModel::Export::SingleModel
|
344
|
+
|
345
|
+
|
346
|
+
end
|
347
|
+
```
|
348
|
+
|
349
|
+
And to export:
|
350
|
+
|
351
|
+
```ruby
|
352
|
+
export_csv = CsvRowModel::Export::Csv.new(ProjectExportRowModel)
|
353
|
+
csv_string = export_csv.generate do |csv|
|
354
|
+
csv.append_model(project) #optional you can pass a context
|
355
|
+
end
|
356
|
+
```
|
357
|
+
|
358
|
+
#### Format Header
|
359
|
+
Override the `format_header` method to format column header names:
|
360
|
+
```ruby
|
361
|
+
class ProjectExportRowModel < ProjectRowModel
|
362
|
+
include CsvRowModel::Export
|
363
|
+
class << self
|
364
|
+
def format_header(column_name)
|
365
|
+
column_name.to_s.titleize
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
```
|