csv_row_model 0.1.0 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fc063f24b8e2038004c3542d79510f7ace7c0c1e
4
- data.tar.gz: 109db80efb39dbad55894fdaefe2f0e83c2920c3
3
+ metadata.gz: 5498738789715db182f898e6d048cf27368bc5b9
4
+ data.tar.gz: f70ad9d361f804afd025e3c5214dc63c2734f603
5
5
  SHA512:
6
- metadata.gz: fecf7a046226eb066808d1773129f0a5487c658fcc07b9428317a1309b05304349c3ac2fc365fae8ca6753d96665b145d757814d22d9a8f829ec0a5be1f37a7c
7
- data.tar.gz: 1e5b55b8784e2407d78e89a5f0b9d09fbcec78b80942e961d87c553e251e008ad17c149f9e006b85982f09d43f372cc4498bd73cc2a9f066b88080bcfd1e4452
6
+ metadata.gz: 60d6189fe5d90a7efb8e5630080f8b2d259fcef21beab143d54781b695c512b567c5f1bef27d11a329de9ba83145a03e5684208d7dde30c55639b09c1bbb55b6
7
+ data.tar.gz: 266ba89a88b2df74e1fa75f33bd9651fb27bb012d5707ac41567fbcbc3cded3cd97e4536d45170c1140ddef0ea9bd35ad2a4d1d05f15e83f22c0d4abf8f4620c
data/README.md CHANGED
@@ -2,6 +2,65 @@
2
2
 
3
3
  Import and export your custom CSVs with a intuitive shared Ruby interface.
4
4
 
5
+ First define your schema:
6
+
7
+ ```ruby
8
+ class ProjectRowModel
9
+ include CsvRowModel::Model
10
+
11
+ column :id
12
+ column :name
13
+ end
14
+ ```
15
+
16
+ To export, define your export model like [`ActiveModel::Serializer`](https://github.com/rails-api/active_model_serializers)
17
+ and generate the file:
18
+
19
+ ```ruby
20
+ class ProjectExportRowModel < ProjectRowModel
21
+ include CsvRowModel::Export
22
+
23
+ # this is an override with the default implementation
24
+ def id
25
+ source_model.id
26
+ end
27
+ end
28
+
29
+ export_file = CsvRowModel::Export::File.new(ProjectExportRowModel)
30
+ export_file.generate { |csv| csv << project }
31
+ export_file.file # => <Tempfile>
32
+ export_file.to_s # => export_file.file.read
33
+ ```
34
+
35
+ To import, define your import model, which works like [`ActiveRecord`](http://guides.rubyonrails.org/active_record_querying.html),
36
+ and iterate through a file:
37
+
38
+ ```ruby
39
+ class ProjectImportRowModel < ProjectRowModel
40
+ include CsvRowModel::Import
41
+
42
+ # this is an override with the default implementation
43
+ def id
44
+ original_attribute(:id)
45
+ end
46
+ end
47
+
48
+ import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
49
+ row_model = import_file.next
50
+
51
+ row_model.header # => ["id", "name"]
52
+
53
+ row_model.source_row # => ["1", "Some Project Name"]
54
+ row_model.mapped_row # => { id: "1", name: "Some Project Name" }, this is `source_row` mapped to `column_names`
55
+ row_model.attributes # => { id: "1", name: "Some Project Name" }, this is final attribute values mapped to `column_names`
56
+
57
+ row_model.id # => 1
58
+ row_model.name # => "Some Project Name"
59
+
60
+ row_model.previous # => <ProjectImportRowModel instance>
61
+ row_model.previous.previous # => nil, save memory by avoiding a linked list
62
+ ```
63
+
5
64
  ## Installation
6
65
 
7
66
  Add this line to your application's Gemfile:
@@ -18,68 +77,213 @@ Or install it yourself as:
18
77
 
19
78
  $ gem install csv_row_model
20
79
 
21
- ## RowModel
80
+ ## Export
81
+
82
+ ### Header Value
83
+ To generate a header value, the following pseudocode is executed:
84
+ ```ruby
85
+ def header(column_name)
86
+ # 1. Header Option
87
+ header = options(column_name)[:header]
22
88
 
23
- Define your `RowModel`.
89
+ # 2. format_header
90
+ header || format_header(column_name)
91
+ end
92
+ ```
24
93
 
94
+ #### Header Option
95
+ Specify the header manually:
25
96
  ```ruby
26
97
  class ProjectRowModel
27
98
  include CsvRowModel::Model
99
+ column :name, header: "NAME"
100
+ end
101
+ ```
28
102
 
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
103
+ #### Format Header
104
+ Override the `format_header` method to format column header names:
105
+ ```ruby
106
+ class ProjectExportRowModel < ProjectRowModel
107
+ include CsvRowModel::Export
108
+ class << self
109
+ def format_header(column_name)
110
+ column_name.to_s.titleize
111
+ end
112
+ end
33
113
  end
34
114
  ```
35
115
 
36
116
  ## Import
37
117
 
38
- Automagically maps each column of a CSV row to an attribute of the `RowModel`.
118
+ ### Attribute Values
119
+ To generate a attribute value, the following pseudocode is executed:
39
120
 
121
+ ```ruby
122
+ def original_attribute(column_name)
123
+ # 1. Get the raw CSV string value for the column
124
+ value = mapped_row[column_name]
125
+
126
+ # 2. Clean or format each cell
127
+ value = self.class.format_cell(value)
128
+
129
+ if value.present?
130
+ # 3a. Parse the cell value (which does nothing if no parsing is specified)
131
+ parse(value)
132
+ elsif default_exists?
133
+ # 3b. Set the default
134
+ default_for_column(column_name)
135
+ end
136
+ end
137
+
138
+ def original_attributes; @original_attributes ||= { id: original_attribute(:id) } end
139
+
140
+ def id; original_attribute[:id] end
141
+ ```
142
+
143
+ #### Format Cell
144
+ Override the `format_cell` method to clean/format every cell:
40
145
  ```ruby
41
146
  class ProjectImportRowModel < ProjectRowModel
42
147
  include CsvRowModel::Import
148
+ class << self
149
+ def format_cell(cell, column_name, column_index)
150
+ cell = cell.strip
151
+ cell.blank? ? nil : cell
152
+ end
153
+ end
154
+ end
155
+ ```
156
+
157
+ #### Type
158
+ Automatic type parsing.
43
159
 
44
- def name
45
- # mapped_row is raw
46
- # the calculated original_attribute[:name] is accessible as well
47
- mapped_row[:name].upcase
160
+ ```ruby
161
+ class ProjectImportRowModel
162
+ include CsvRowModel::Model
163
+ include CsvRowModel::Import
164
+
165
+ column :id, type: Integer
166
+ column :name, parse: ->(original_string) { parse(original_string) }
167
+
168
+ def parse(original_string)
169
+ "#{id} - #{original_string}"
48
170
  end
49
171
  end
50
172
  ```
51
173
 
52
- And to import:
174
+ There are validators for different types: `Boolean`, `Date`, `Float`, `Integer`. See [Validations](#validations) for more.
175
+
176
+ #### Default
177
+ Sets the default value of the cell:
178
+ ```ruby
179
+ class ProjectImportRowModel
180
+ include CsvRowModel::Model
181
+ include CsvRowModel::Import
182
+
183
+ column :id, default: 1
184
+ column :name, default: -> { get_name }
185
+
186
+ def get_name; "John Doe" end
187
+ end
188
+ row_model = ProjectImportRowModel.new(["", ""])
189
+ row_model.id # => 1
190
+ row_model.name # => "John Doe"
191
+ row_model.default_changes # => { id: ["", 1], name: ["", "John Doe"] }
192
+ ```
193
+
194
+ `DefaultChangeValidator` is provided to allows to add warnings when defaults are set. See [Validations](#default-changes) for more.
195
+
196
+ ## Advanced Import
197
+
198
+ ### Children
199
+
200
+ Child `RowModel` relationships can also be defined:
201
+
202
+ ```ruby
203
+ class UserImportRowModel
204
+ include CsvRowModel::Model
205
+ include CsvRowModel::Import
206
+
207
+ column :id, type: Integer
208
+ column :name
209
+ column :email
210
+
211
+ # uses ProjectImportRowModel#valid? to detect the child row
212
+ has_many :projects, ProjectImportRowModel
213
+ end
53
214
 
215
+ import_file = CsvRowModel::Import::File.new(file_path, UserImportRowModel)
216
+ row_model = import_file.next
217
+ row_model.projects # => [<ProjectImportRowModel>, ...]
218
+ ```
219
+
220
+ ### Layers
221
+ For complex `RowModel`s there are different layers you can work with:
54
222
  ```ruby
55
223
  import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
56
224
  row_model = import_file.next
57
225
 
58
- row_model.header # => ["id", "name"]
226
+ # the three layers:
227
+ # 1. csv_string_model - represents the row BEFORE parsing (attributes are always strings)
228
+ row_model.csv_string_model
59
229
 
60
- row_model.source_row # => ["1", "Some Project Name"]
61
- row_model.mapped_row # => { id: "1", name: "Some Project Name" }
230
+ # 2. RowModel - represents the row AFTER parsing
231
+ row_model
62
232
 
63
- row_model.id # => 1
64
- row_model.name # => "SOME PROJECT NAME"
233
+ # 3. Presenter - an abstraction of a row
234
+ row_model.presenter
65
235
  ```
66
236
 
67
- `Import::File` also provides the `RowModel` with the previous `RowModel` instance:
237
+ #### CsvStringModel
238
+ The `CsvStringModel` represents a row before parsing to add parsing validations.
68
239
 
69
240
  ```ruby
70
- row_model.previous # => <ProjectImportRowModel instance>
71
- row_model.previous.previous # => nil, save memory by avoiding a linked list
241
+ class ProjectImportRowModel
242
+ include CsvRowModel::Model
243
+ include CsvRowModel::Import
244
+
245
+ # Note the type definition here for parsing
246
+ column :id, type: Integer
247
+
248
+ # this is applied to the parsed CSV on the model
249
+ validates :id, numericality: { greater_than: 0 }
250
+
251
+ csv_string_model do
252
+ # define your csv_string_model here
253
+
254
+ # this is applied BEFORE the parsed CSV on csv_string_model
255
+ validates :id, presense: true
256
+
257
+ def random_method; "Hihi" end
258
+ end
259
+ end
260
+
261
+ # Applied to the String
262
+ ProjectImportRowModel.new([""])
263
+ csv_string_model = row_model.csv_string_model
264
+ csv_string_model.random_method => "Hihi"
265
+ csv_string_model.valid? => false
266
+ csv_string_model.errors.full_messages # => ["Id can't be blank'"]
267
+
268
+ # Errors are propagated for simplicity
269
+ row_model.valid? # => false
270
+ row_model.errors.full_messages # => ["Id can't be blank'"]
271
+
272
+ # Applied to the parsed Integer
273
+ row_model = ProjectRowModel.new(["-1"])
274
+ row_model.valid? # => false
275
+ row_model.errors.full_messages # => ["Id must be greater than 0"]
72
276
  ```
73
277
 
74
- ## Presenter
278
+ Note that `CsvStringModel` validations are calculated after [Format Cell](#format-cell).
279
+
280
+ #### Presenter
75
281
  For complex rows, you can wrap your `RowModel` with a presenter:
76
282
 
77
283
  ```ruby
78
284
  class ProjectImportRowModel < ProjectRowModel
79
285
  include CsvRowModel::Import
80
286
 
81
- # same as above
82
-
83
287
  presenter do
84
288
  # define your presenter here
85
289
 
@@ -115,7 +319,7 @@ row_model = import_file.next
115
319
  presenter = row_model.presenter
116
320
 
117
321
  presenter.row_model # gets the row model underneath
118
- import_mapper.project.name == presenter.row_model.name # => "SOME PROJECT NAME"
322
+ presenter.project.name == presenter.row_model.name # => "Some Project Name"
119
323
  ```
120
324
 
121
325
  The presenters are designed for another layer of validation---such as with the database.
@@ -127,70 +331,39 @@ Also, the `attribute` defines a dynamic `#project` method that:
127
331
  3. Handles dependencies. When any of the dependencies are `invalid?`:
128
332
  - The attribute block is not called and the attribute returns `nil`.
129
333
  - `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
334
+ the `:project` key is removed from the errors, so: `presenter.errors.keys # => [:id, :name]`
133
335
 
134
- Child `RowModel` relationships can also be defined:
336
+ ## Import Validations
135
337
 
136
- ```ruby
137
- class UserImportRowModel
138
- include CsvRowModel::Model
139
- include CsvRowModel::Import
338
+ Use [`ActiveModel::Validations`](http://api.rubyonrails.org/classes/ActiveModel/Validations.html) the `RowModel`'s [Layers](#layers).
339
+ Please read [Layers](#layers) for more information.
140
340
 
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
- ```
341
+ Included is [`ActiveWarnings`](https://github.com/s12chung/active_warnings) on `Model` and `Presenter` for warnings.
153
342
 
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
343
 
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
- ```
344
+ ### Type Format
345
+ Notice that there are validators given for different types: `Boolean`, `Date`, `Float`, `Integer`:
174
346
 
175
- #### Default
176
- Called when `format_cell` is `value_from_format_cell.blank?`, it sets the default value of the cell:
177
347
  ```ruby
178
- class ProjectImportRowModel < ProjectRowModel
348
+ class ProjectImportRowModel
349
+ include CsvRowModel::Model
179
350
  include CsvRowModel::Import
180
351
 
181
- column :id, default: 1
182
- column :name, default: -> { get_name }
352
+ column :id, type: Integer, validate_type: true
183
353
 
184
- def get_name; "John Doe" end
354
+ # the :validate_type option is the same as:
355
+ # csv_string_model do
356
+ # validates :id, integer_format: true, allow_blank: true
357
+ # end
185
358
  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
359
 
360
+ ProjectRowModel.new(["not_a_number"])
361
+ row_model.valid? # => false
362
+ row_model.errors.full_messages # => ["Id is not a Integer format"]
191
363
  ```
192
364
 
193
- `DefaultChangeValidator` is provided to allows to add warnings when defaults or set:
365
+ ### Default Changes
366
+ [Default Changes](#default) are tracked within [`ActiveWarnings`](https://github.com/s12chung/active_warnings).
194
367
 
195
368
  ```ruby
196
369
  class ProjectImportRowModel
@@ -201,7 +374,6 @@ class ProjectImportRowModel
201
374
 
202
375
  warnings do
203
376
  validates :id, default_change: true
204
- # validates :id, presence: true, works too. See ActiveWarnings gem for more.
205
377
  end
206
378
  end
207
379
 
@@ -210,101 +382,42 @@ row_model = ProjectImportRowModel.new([""])
210
382
  row_model.unsafe? # => true
211
383
  row_model.has_warnings? # => true, same as `#unsafe?`
212
384
  row_model.warnings.full_messages # => ["Id changed by default"]
385
+ row_model.default_changes # => { id: ["", 1] }
213
386
  ```
214
387
 
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:
388
+ ### Skip and Abort
389
+ You can iterate through a file with the `#each` method, which calls `#next` internally.
390
+ `#next` will always return the next `RowModel` in the file. However, you can implement skips and
391
+ abort logic:
242
392
 
243
393
  ```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
394
+ class ProjectImportRowModel
395
+ # always skip
396
+ def skip?
397
+ true # original implementation: !valid? || presenter.skip?
256
398
  end
257
399
  end
258
400
 
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
401
+ CsvRowModel::Import::File.new(file_path, ProjectImportRowModel).each do |project_import_model|
402
+ # never yields here
282
403
  end
283
404
  ```
284
405
 
285
-
286
- ## Callbacks
406
+ ### Import Callbacks
287
407
  `CsvRowModel::Import::File` can be subclassed to access
288
408
  [`ActiveModel::Callbacks`](http://api.rubyonrails.org/classes/ActiveModel/Callbacks.html).
289
409
 
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
410
+ * each_iteration - `before`, `around`, or `after` the an iteration on `#each`.
411
+ Use this to handle exceptions. `return` and `break` may be called within the callback for
412
+ skips and aborts.
413
+ * next - `before`, `around`, or `after` each change in `current_row_model`
301
414
  * skip - `before`
302
415
  * abort - `before`
303
416
 
304
417
  and implement the callbacks:
305
418
  ```ruby
306
419
  class ImportFile < CsvRowModel::Import::File
307
- around_yield :logger_track
420
+ around_each_iteration :logger_track
308
421
  before_skip :track_skip
309
422
 
310
423
  def logger_track(&block)
@@ -315,55 +428,4 @@ class ImportFile < CsvRowModel::Import::File
315
428
  ...
316
429
  end
317
430
  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
431
  ```