csv_row_model 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +10 -0
  7. data/.yardopts +10 -0
  8. data/CONTRIBUTING.md +7 -0
  9. data/Gemfile +20 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +369 -0
  12. data/Rakefile +1 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +7 -0
  15. data/csv_row_model.gemspec +23 -0
  16. data/lib/csv_row_model.rb +46 -0
  17. data/lib/csv_row_model/concerns/deep_class_var.rb +86 -0
  18. data/lib/csv_row_model/concerns/inspect.rb +10 -0
  19. data/lib/csv_row_model/engine.rb +5 -0
  20. data/lib/csv_row_model/export.rb +70 -0
  21. data/lib/csv_row_model/export/csv.rb +43 -0
  22. data/lib/csv_row_model/export/single_model.rb +47 -0
  23. data/lib/csv_row_model/import.rb +125 -0
  24. data/lib/csv_row_model/import/attributes.rb +105 -0
  25. data/lib/csv_row_model/import/csv.rb +136 -0
  26. data/lib/csv_row_model/import/csv/row.rb +17 -0
  27. data/lib/csv_row_model/import/file.rb +133 -0
  28. data/lib/csv_row_model/import/file/callbacks.rb +16 -0
  29. data/lib/csv_row_model/import/file/validations.rb +39 -0
  30. data/lib/csv_row_model/import/presenter.rb +160 -0
  31. data/lib/csv_row_model/import/single_model.rb +41 -0
  32. data/lib/csv_row_model/model.rb +68 -0
  33. data/lib/csv_row_model/model/children.rb +79 -0
  34. data/lib/csv_row_model/model/columns.rb +73 -0
  35. data/lib/csv_row_model/model/csv_string_model.rb +16 -0
  36. data/lib/csv_row_model/model/single_model.rb +16 -0
  37. data/lib/csv_row_model/validators/boolean_format.rb +11 -0
  38. data/lib/csv_row_model/validators/date_format.rb +9 -0
  39. data/lib/csv_row_model/validators/default_change.rb +6 -0
  40. data/lib/csv_row_model/validators/float_format.rb +9 -0
  41. data/lib/csv_row_model/validators/integer_format.rb +9 -0
  42. data/lib/csv_row_model/validators/number_validator.rb +16 -0
  43. data/lib/csv_row_model/validators/validate_attributes.rb +27 -0
  44. data/lib/csv_row_model/version.rb +3 -0
  45. metadata +116 -0
@@ -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
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .idea/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1 @@
1
+ csv_row_model
@@ -0,0 +1 @@
1
+ 2.2.2
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ script: bundle exec rspec
3
+ rvm:
4
+ - 2.1.0
5
+ - ruby-head
6
+ matrix:
7
+ allow_failures:
8
+ - rvm: ruby-head
9
+ env:
10
+ - CODECLIMATE_REPO_TOKEN=6b3e0fb12a56a7aba4b43ca8662d8a5383c831d98b92fc818ea731f4e70e73cd bundle exec rake
@@ -0,0 +1,10 @@
1
+ --no-cache
2
+ --verbose
3
+ --markup markdown
4
+ --markup-provider redcarpet
5
+
6
+ --plugin activesupport-concern
7
+ --plugin yard-delegate
8
+
9
+ --protected
10
+ --private
@@ -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
@@ -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.
@@ -0,0 +1,369 @@
1
+ # CsvRowModel [![Build Status](https://travis-ci.org/FinalCAD/csv_row_model.svg?branch=master)](https://travis-ci.org/FinalCAD/csv_row_model) [![Code Climate](https://codeclimate.com/github/FinalCAD/csv_row_model/badges/gpa.svg)](https://codeclimate.com/github/FinalCAD/csv_row_model) [![Test Coverage](https://codeclimate.com/github/FinalCAD/csv_row_model/badges/coverage.svg)](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
+ ```