csv_row_model 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
  ```