README.md in csv_row_model-0.1.0 vs README.md in csv_row_model-0.1.1
- old
+ new
@@ -1,9 +1,68 @@
# 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)
Import and export your custom CSVs with a intuitive shared Ruby interface.
+First define your schema:
+
+```ruby
+class ProjectRowModel
+ include CsvRowModel::Model
+
+ column :id
+ column :name
+end
+```
+
+To export, define your export model like [`ActiveModel::Serializer`](https://github.com/rails-api/active_model_serializers)
+and generate the file:
+
+```ruby
+class ProjectExportRowModel < ProjectRowModel
+ include CsvRowModel::Export
+
+ # this is an override with the default implementation
+ def id
+ source_model.id
+ end
+end
+
+export_file = CsvRowModel::Export::File.new(ProjectExportRowModel)
+export_file.generate { |csv| csv << project }
+export_file.file # => <Tempfile>
+export_file.to_s # => export_file.file.read
+```
+
+To import, define your import model, which works like [`ActiveRecord`](http://guides.rubyonrails.org/active_record_querying.html),
+and iterate through a file:
+
+```ruby
+class ProjectImportRowModel < ProjectRowModel
+ include CsvRowModel::Import
+
+ # this is an override with the default implementation
+ def id
+ original_attribute(:id)
+ end
+end
+
+import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
+row_model = import_file.next
+
+row_model.header # => ["id", "name"]
+
+row_model.source_row # => ["1", "Some Project Name"]
+row_model.mapped_row # => { id: "1", name: "Some Project Name" }, this is `source_row` mapped to `column_names`
+row_model.attributes # => { id: "1", name: "Some Project Name" }, this is final attribute values mapped to `column_names`
+
+row_model.id # => 1
+row_model.name # => "Some Project Name"
+
+row_model.previous # => <ProjectImportRowModel instance>
+row_model.previous.previous # => nil, save memory by avoiding a linked list
+```
+
## Installation
Add this line to your application's Gemfile:
```ruby
@@ -16,72 +75,217 @@
Or install it yourself as:
$ gem install csv_row_model
-## RowModel
+## Export
-Define your `RowModel`.
+### Header Value
+To generate a header value, the following pseudocode is executed:
+```ruby
+def header(column_name)
+ # 1. Header Option
+ header = options(column_name)[:header]
+ # 2. format_header
+ header || format_header(column_name)
+end
+```
+
+#### Header Option
+Specify the header manually:
```ruby
class ProjectRowModel
include CsvRowModel::Model
+ column :name, header: "NAME"
+end
+```
- # column indices are tracked with each call
- column :id
- column :name
- column :owner_id, header: 'Project Manager' # optional header String, that allows to modify the header of the colmnun
+#### Format Header
+Override the `format_header` method to format column header names:
+```ruby
+class ProjectExportRowModel < ProjectRowModel
+ include CsvRowModel::Export
+ class << self
+ def format_header(column_name)
+ column_name.to_s.titleize
+ end
+ end
end
```
## Import
-Automagically maps each column of a CSV row to an attribute of the `RowModel`.
+### Attribute Values
+To generate a attribute value, the following pseudocode is executed:
```ruby
+def original_attribute(column_name)
+ # 1. Get the raw CSV string value for the column
+ value = mapped_row[column_name]
+
+ # 2. Clean or format each cell
+ value = self.class.format_cell(value)
+
+ if value.present?
+ # 3a. Parse the cell value (which does nothing if no parsing is specified)
+ parse(value)
+ elsif default_exists?
+ # 3b. Set the default
+ default_for_column(column_name)
+ end
+end
+
+def original_attributes; @original_attributes ||= { id: original_attribute(:id) } end
+
+def id; original_attribute[:id] end
+```
+
+#### Format Cell
+Override the `format_cell` method to clean/format every cell:
+```ruby
class ProjectImportRowModel < ProjectRowModel
include CsvRowModel::Import
+ class << self
+ def format_cell(cell, column_name, column_index)
+ cell = cell.strip
+ cell.blank? ? nil : cell
+ end
+ end
+end
+```
- def name
- # mapped_row is raw
- # the calculated original_attribute[:name] is accessible as well
- mapped_row[:name].upcase
+#### Type
+Automatic type parsing.
+
+```ruby
+class ProjectImportRowModel
+ include CsvRowModel::Model
+ include CsvRowModel::Import
+
+ column :id, type: Integer
+ column :name, parse: ->(original_string) { parse(original_string) }
+
+ def parse(original_string)
+ "#{id} - #{original_string}"
end
end
```
-And to import:
+There are validators for different types: `Boolean`, `Date`, `Float`, `Integer`. See [Validations](#validations) for more.
+#### Default
+Sets the default value of the cell:
```ruby
+class ProjectImportRowModel
+ include CsvRowModel::Model
+ include CsvRowModel::Import
+
+ column :id, default: 1
+ column :name, default: -> { get_name }
+
+ def get_name; "John Doe" end
+end
+row_model = ProjectImportRowModel.new(["", ""])
+row_model.id # => 1
+row_model.name # => "John Doe"
+row_model.default_changes # => { id: ["", 1], name: ["", "John Doe"] }
+```
+
+`DefaultChangeValidator` is provided to allows to add warnings when defaults are set. See [Validations](#default-changes) for more.
+
+## Advanced Import
+
+### Children
+
+Child `RowModel` relationships can also be defined:
+
+```ruby
+class UserImportRowModel
+ include CsvRowModel::Model
+ include CsvRowModel::Import
+
+ column :id, type: Integer
+ column :name
+ column :email
+
+ # uses ProjectImportRowModel#valid? to detect the child row
+ has_many :projects, ProjectImportRowModel
+end
+
+import_file = CsvRowModel::Import::File.new(file_path, UserImportRowModel)
+row_model = import_file.next
+row_model.projects # => [<ProjectImportRowModel>, ...]
+```
+
+### Layers
+For complex `RowModel`s there are different layers you can work with:
+```ruby
import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
row_model = import_file.next
-row_model.header # => ["id", "name"]
+# the three layers:
+# 1. csv_string_model - represents the row BEFORE parsing (attributes are always strings)
+row_model.csv_string_model
-row_model.source_row # => ["1", "Some Project Name"]
-row_model.mapped_row # => { id: "1", name: "Some Project Name" }
+# 2. RowModel - represents the row AFTER parsing
+row_model
-row_model.id # => 1
-row_model.name # => "SOME PROJECT NAME"
+# 3. Presenter - an abstraction of a row
+row_model.presenter
```
-`Import::File` also provides the `RowModel` with the previous `RowModel` instance:
+#### CsvStringModel
+The `CsvStringModel` represents a row before parsing to add parsing validations.
```ruby
-row_model.previous # => <ProjectImportRowModel instance>
-row_model.previous.previous # => nil, save memory by avoiding a linked list
+class ProjectImportRowModel
+ include CsvRowModel::Model
+ include CsvRowModel::Import
+
+ # Note the type definition here for parsing
+ column :id, type: Integer
+
+ # this is applied to the parsed CSV on the model
+ validates :id, numericality: { greater_than: 0 }
+
+ csv_string_model do
+ # define your csv_string_model here
+
+ # this is applied BEFORE the parsed CSV on csv_string_model
+ validates :id, presense: true
+
+ def random_method; "Hihi" end
+ end
+end
+
+# Applied to the String
+ProjectImportRowModel.new([""])
+csv_string_model = row_model.csv_string_model
+csv_string_model.random_method => "Hihi"
+csv_string_model.valid? => false
+csv_string_model.errors.full_messages # => ["Id can't be blank'"]
+
+# Errors are propagated for simplicity
+row_model.valid? # => false
+row_model.errors.full_messages # => ["Id can't be blank'"]
+
+# Applied to the parsed Integer
+row_model = ProjectRowModel.new(["-1"])
+row_model.valid? # => false
+row_model.errors.full_messages # => ["Id must be greater than 0"]
```
-## Presenter
+Note that `CsvStringModel` validations are calculated after [Format Cell](#format-cell).
+
+#### Presenter
For complex rows, you can wrap your `RowModel` with a presenter:
```ruby
class ProjectImportRowModel < ProjectRowModel
include CsvRowModel::Import
- # same as above
-
presenter do
# define your presenter here
# this is shorthand for the psuedo_code:
# def project
@@ -113,11 +317,11 @@
import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
row_model = import_file.next
presenter = row_model.presenter
presenter.row_model # gets the row model underneath
-import_mapper.project.name == presenter.row_model.name # => "SOME PROJECT NAME"
+presenter.project.name == presenter.row_model.name # => "Some Project Name"
```
The presenters are designed for another layer of validation---such as with the database.
Also, the `attribute` defines a dynamic `#project` method that:
@@ -125,245 +329,103 @@
1. Memoizes by default, turn off with `memoize: false` option
2. All errors of `row_model` are propagated to the presenter when calling `presenter.valid?`
3. Handles dependencies. When any of the dependencies are `invalid?`:
- The attribute block is not called and the attribute returns `nil`.
- `presenter.errors` for dependencies are cleaned. For the example above, if `row_model.id/name` are `invalid?`, then
-the `:project` key is removed from the errors, so: `import_mapper.errors.keys # => [:id, :name]`
+the `:project` key is removed from the errors, so: `presenter.errors.keys # => [:id, :name]`
-## Children
+## Import Validations
-Child `RowModel` relationships can also be defined:
+Use [`ActiveModel::Validations`](http://api.rubyonrails.org/classes/ActiveModel/Validations.html) the `RowModel`'s [Layers](#layers).
+Please read [Layers](#layers) for more information.
-```ruby
-class UserImportRowModel
- include CsvRowModel::Model
- include CsvRowModel::Import
+Included is [`ActiveWarnings`](https://github.com/s12chung/active_warnings) on `Model` and `Presenter` for warnings.
- column :id
- column :name
- column :email
- # uses ProjectImportRowModel#valid? to detect the child row
- has_many :projects, ProjectImportRowModel
-end
+### Type Format
+Notice that there are validators given for different types: `Boolean`, `Date`, `Float`, `Integer`:
-import_file = CsvRowModel::Import::File.new(file_path, UserImportRowModel)
-row_model = import_file.next
-row_model.projects # => [<ProjectImportRowModel>, ...]
-```
-
-## Column Options
-### Default Attributes
-For `Import`, `default_attributes` are calculated as thus:
-- `format_cell`
-- if `value_from_format_cell.blank?`, `default_lambda.call` or nil
-- otherwise, `parse_lambda.call`
-
-#### Format Cell
-Override the `format_cell` method to clean/format every cell:
```ruby
-class ProjectImportRowModel < ProjectRowModel
+class ProjectImportRowModel
+ include CsvRowModel::Model
include CsvRowModel::Import
- class << self
- def format_cell(cell, column_name, column_index)
- cell = cell.strip
- cell.to_i.to_s == cell ? cell.to_i : cell
- end
- end
-end
-```
-#### Default
-Called when `format_cell` is `value_from_format_cell.blank?`, it sets the default value of the cell:
-```ruby
-class ProjectImportRowModel < ProjectRowModel
- include CsvRowModel::Import
+ column :id, type: Integer, validate_type: true
- column :id, default: 1
- column :name, default: -> { get_name }
-
- def get_name; "John Doe" end
+ # the :validate_type option is the same as:
+ # csv_string_model do
+ # validates :id, integer_format: true, allow_blank: true
+ # end
end
-row_model = ProjectImportRowModel.new(["", ""])
-row_model.id # => 1
-row_model.name # => "John Doe"
-row_model.default_changes # => { id: ["", 1], name: ["", "John Doe"] }
+ProjectRowModel.new(["not_a_number"])
+row_model.valid? # => false
+row_model.errors.full_messages # => ["Id is not a Integer format"]
```
-`DefaultChangeValidator` is provided to allows to add warnings when defaults or set:
+### Default Changes
+[Default Changes](#default) are tracked within [`ActiveWarnings`](https://github.com/s12chung/active_warnings).
```ruby
class ProjectImportRowModel
include CsvRowModel::Model
include CsvRowModel::Input
column :id, default: 1
warnings do
validates :id, default_change: true
- # validates :id, presence: true, works too. See ActiveWarnings gem for more.
end
end
row_model = ProjectImportRowModel.new([""])
row_model.unsafe? # => true
row_model.has_warnings? # => true, same as `#unsafe?`
row_model.warnings.full_messages # => ["Id changed by default"]
+row_model.default_changes # => { id: ["", 1] }
```
-See [Validations](#validations) for more.
+### Skip and Abort
+You can iterate through a file with the `#each` method, which calls `#next` internally.
+`#next` will always return the next `RowModel` in the file. However, you can implement skips and
+abort logic:
-#### Type
-Automatic type parsing.
-
```ruby
-class ProjectImportRowModel < ProjectRowModel
- include CsvRowModel::Import
-
- column :id, type: Integer
- column :name, parse: ->(original_string) { parse(original_string) }
-
- def parse(original_string)
- "#{id} - #{original_string}"
+class ProjectImportRowModel
+ # always skip
+ def skip?
+ true # original implementation: !valid? || presenter.skip?
end
end
-```
-## Validations
-
-Use [`ActiveModel::Validations`](http://api.rubyonrails.org/classes/ActiveModel/Validations.html)
-on your `RowModel` or `Mapper`.
-
-Included is [`ActiveWarnings`](https://github.com/s12chung/active_warnings) on `Model` and `Mapper` for warnings
-(such as setting defaults), but not errors (which by default results in a skip).
-
-`RowModel` has two validation layers on the `csv_string_model` (a model of `#mapped_row` with `::format_cell` applied) and itself:
-
-```ruby
-class ProjectRowModel
- include CsvRowModel::Model
- include CsvRowModel::Import
-
- column :id, type: Integer
-
- # this is applied to the parsed CSV on the model
- validates :id, numericality: { greater_than: 0 }
-
- csv_string_model do
- # this is applied before the parsed CSV on csv_string_model
- validates :id, integer_format: true, allow_blank: true
- end
+CsvRowModel::Import::File.new(file_path, ProjectImportRowModel).each do |project_import_model|
+ # never yields here
end
-
-# Applied to the String
-ProjectRowModel.new(["not_a_number"])
-row_model.valid? # => false
-row_model.errors.full_messages # => ["Id is not a Integer format"]
-
-# Applied to the parsed Integer
-row_model = ProjectRowModel.new(["-1"])
-row_model.valid? # => false
-row_model.errors.full_messages # => ["Id must be greater than 0"]
```
-Notice that there are validators given for different types: `Boolean`, `Date`, `Float`, `Integer`:
-
-```ruby
-class ProjectRowModel
- include CsvRowModel::Model
-
- # the :validate_type option does the commented code below.
- column :id, type: Integer, validate_type: true
-
- # csv_string_model do
- # validates :id, integer_format: true, allow_blank: true
- # end
-end
-```
-
-
-## Callbacks
+### Import Callbacks
`CsvRowModel::Import::File` can be subclassed to access
[`ActiveModel::Callbacks`](http://api.rubyonrails.org/classes/ActiveModel/Callbacks.html).
-You can iterate through a file with the `#each` method, which calls `#next` internally:
-
-```ruby
-CsvRowModel::Import::File.new(file_path, ProjectImportRowModel).each do |project_import_model|
-end
-```
-
-Within `#each`, **Skips** and **Aborts** will be done via the `skip?` or `abort?` method on the row model,
-allowing the following callbacks:
-
-* yield - `before`, `around`, or `after` the iteration yield
+* each_iteration - `before`, `around`, or `after` the an iteration on `#each`.
+Use this to handle exceptions. `return` and `break` may be called within the callback for
+skips and aborts.
+* next - `before`, `around`, or `after` each change in `current_row_model`
* skip - `before`
* abort - `before`
and implement the callbacks:
```ruby
class ImportFile < CsvRowModel::Import::File
- around_yield :logger_track
+ around_each_iteration :logger_track
before_skip :track_skip
def logger_track(&block)
...
end
def track_skip
...
- end
-end
-```
-
-### Export RowModel
-
-Maps each attribute of the `RowModel` to a column of a CSV row.
-
-```ruby
-class ProjectExportRowModel < ProjectRowModel
- include CsvRowModel::Export
-
- # Optionally it's possible to override the attribute method, by default it
- # does source_model.public_send(attribute)
- def name
- "#{source_model.id} - #{source_model.name}"
- end
-end
-```
-
-### Export SingleModel
-
-Maps each attribute of the `RowModel` to a row on the CSV.
-
-```ruby
-class ProjectExportRowModel < ProjectRowModel
- include CsvRowModel::Export
- include CsvRowModel::Export::SingleModel
-
-
-end
-```
-
-And to export:
-
-```ruby
-export_csv = CsvRowModel::Export::Csv.new(ProjectExportRowModel)
-csv_string = export_csv.generate do |csv|
- csv.append_model(project) #optional you can pass a context
- end
-```
-
-#### Format Header
-Override the `format_header` method to format column header names:
-```ruby
-class ProjectExportRowModel < ProjectRowModel
- include CsvRowModel::Export
- class << self
- def format_header(column_name)
- column_name.to_s.titleize
- end
end
end
```
\ No newline at end of file