README.md in csv_row_model-0.4.1 vs README.md in csv_row_model-1.0.0.beta1
- old
+ new
@@ -27,11 +27,11 @@
source_model.id
end
end
export_file = CsvRowModel::Export::File.new(ProjectExportRowModel)
-export_file.generate { |csv| csv << project }
+export_file.generate { |csv| csv << project } # `project` is the `source_model` in `ProjectExportRowModel`
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),
@@ -84,14 +84,14 @@
### 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]
+ header = options_for(column_name)[:header]
# 2. format_header
- header || format_header(column_name)
+ header || format_header(column_name, column_index, context)
end
```
#### Header Option
Specify the header manually:
@@ -106,11 +106,11 @@
Override the `format_header` method to format column header names:
```ruby
class ProjectExportRowModel < ProjectRowModel
include CsvRowModel::Export
class << self
- def format_header(column_name)
+ def format_header(column_name, column_index, context)
column_name.to_s.titleize
end
end
end
```
@@ -135,22 +135,21 @@
# 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
+def 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, context={})
+ def format_cell(cell, column_name, column_index, context)
cell = cell.strip
cell.blank? ? nil : cell
end
end
end
@@ -171,11 +170,11 @@
"#{id} - #{original_string}"
end
end
```
-There are validators for different types: `Boolean`, `Date`, `DateTime`, `Float`, `Integer`. See [Validations](#validations) for more.
+There are validators for different types: `Boolean`, `Date`, `DateTime`, `Float`, `Integer`. See [Type Format](#type-format) for more.
#### Default
Sets the default value of the cell:
```ruby
class ProjectImportRowModel
@@ -191,161 +190,41 @@
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.
+`DefaultChangeValidator` is provided to allows to add warnings when defaults are set. See [Default Changes](#default-changes) for more.
-## Advanced Import
+### Validations
-### Children
+[`ActiveModel::Validations`](http://api.rubyonrails.org/classes/ActiveModel/Validations.html) and [`ActiveWarnings`](https://github.com/s12chung/active_warnings)
+are included for errors and warnings.
-Child `RowModel` relationships can also be defined:
+There are layers to validations.
```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
-
-# the three layers:
-# 1. csv_string_model - represents the row BEFORE parsing (attributes are always strings)
-row_model.csv_string_model
-
-# 2. RowModel - represents the row AFTER parsing
-row_model
-
-# 3. Presenter - an abstraction of a row
-row_model.presenter
-```
-
-#### CsvStringModel
-The `CsvStringModel` represents a row before parsing to add parsing validations.
-
-```ruby
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 }
-
+
+ # Errors - by default, an Error will make the row skip
+ validates :id, numericality: { greater_than: 0 } # ActiveModel::Validations
+
+ # Warnings - a message you want the user to see, but will not make the row skip
+ warnings do # ActiveWarnings, see: https://github.com/s12chung/active_warnings
+ validates :some_custom_string, presence: true
+ end
+
+ # This is for validation of the strings before parsing. See: https://github.com/FinalCAD/csv_row_model#csvstringmodel
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
+ validates :id, presence: true
+ # can do warnings too
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"]
```
-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
-
- presenter do
- # define your presenter here
-
- # this is shorthand for the psuedo_code:
- # def project
- # return if row_model.id.blank? || row_model.name.blank?
- #
- # # turn off memoziation with `memoize: false` option
- # @project ||= __the_code_inside_the_block__
- # end
- #
- # and the psuedo_code:
- # def valid?
- # super # calls ActiveModel::Errors code
- # errors.delete(:project) if row_model.id.invalid? || row_model.name.invalid?
- # errors.empty?
- # end
- attribute :project, dependencies: [:id, :name] do
- project = Project.where(id: row_model.id).first
-
- # project not found, invalid.
- return unless project
-
- project.name = row_model.name
- project
- end
- end
-end
-
-# Importing is the same
-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
-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:
-
-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 `blank?`, the attribute block is not called and the attribute returns `nil`.
- - When any of the dependencies are `invalid?`, `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: `presenter.errors.keys # => [:id, :name]`
-
-## Import Validations
-
-Use [`ActiveModel::Validations`](http://api.rubyonrails.org/classes/ActiveModel/Validations.html) the `RowModel`'s [Layers](#layers).
-Please read [Layers](#layers) for more information.
-
-Included is [`ActiveWarnings`](https://github.com/s12chung/active_warnings) on `Model` and `Presenter` for warnings.
-
-
-### Type Format
+#### Type Format
Notice that there are validators given for different types: `Boolean`, `Date`, `DateTime`, `Float`, `Integer`:
```ruby
class ProjectImportRowModel
include CsvRowModel::Model
@@ -362,30 +241,26 @@
ProjectRowModel.new(["not_a_number"])
row_model.valid? # => false
row_model.errors.full_messages # => ["Id is not a Integer format"]
```
-### Default Changes
-[Default Changes](#default) are tracked within [`ActiveWarnings`](https://github.com/s12chung/active_warnings).
+#### Default Changes
+A custom validator for [Default Changes](#default).
```ruby
class ProjectImportRowModel
include CsvRowModel::Model
include CsvRowModel::Input
column :id, default: 1
-
- warnings do
- validates :id, default_change: true
- end
+ validates :id, default_change: true
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.valid? # => false
+row_model.errors.full_messages # => ["Id changed by default"]
row_model.default_changes # => { id: ["", 1] }
```
### Skip and Abort
You can iterate through a file with the `#each` method, which calls `#next` internally.
@@ -394,17 +269,17 @@
```ruby
class ProjectImportRowModel
# always skip
def skip?
- true # original implementation: !valid? || presenter.skip?
+ true # original implementation: !valid?
end
end
-CsvRowModel::Import::File.new(file_path, ProjectImportRowModel).each do |project_import_model|
- # never yields here
-end
+import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
+import_file.each { |project_import_model| puts "does not yield here" }
+import_file.next # does not skip or abort
```
### Import Callbacks
`CsvRowModel::Import::File` can be subclassed to access
[`ActiveModel::Callbacks`](http://api.rubyonrails.org/classes/ActiveModel/Callbacks.html).
@@ -430,10 +305,132 @@
...
end
end
```
+## Advanced Import
+
+### CsvStringModel
+The `CsvStringModel` represents a row BEFORE parsing to add validations.
+
+```ruby
+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, presence: 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"]
+```
+
+Note that `CsvStringModel` validations are calculated after [Format Cell](#format-cell).
+
+### Represents
+A CSV is often a representation of database model(s), much like how JSON parameters represents models in requests.
+However, CSVs schemas are **flat** and **static** and JSON parameters are **tree structured** and **dynamic** (but often static).
+Because CSVs are flat, `RowModel`s are also flat, but they can represent various models. The `represents` interface attempts to simplify this for importing.
+
+```ruby
+class ProjectImportRowModel < ProjectRowModel
+ include CsvRowModel::Import
+
+ # this is shorthand for the psuedo_code:
+ # def project
+ # return if id.blank? || name.blank?
+ #
+ # # turn off memoziation with `memoize: false` option
+ # @project ||= __the_code_inside_the_block__
+ # end
+ #
+ # and the psuedo_code:
+ # def valid?
+ # super # calls ActiveModel::Errors code
+ # errors.delete(:project) if id.invalid? || name.invalid?
+ # errors.empty?
+ # end
+ represents_one :project, dependencies: [:id, :name] do
+ project = Project.where(id: id).first
+
+ # project not found, invalid.
+ return unless project
+
+ project.name = name
+ project
+ end
+
+ # same as above, but: returns [] if name.blank?
+ represents_many :projects, dependencies: [:name] do
+ Project.where(name: name)
+ end
+end
+
+# Importing is the same
+import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
+row_model = import_file.next
+row_model.project.name # => "Some Project Name"
+```
+
+The `represents_one` method defines a dynamic `#project` method that:
+
+1. Memoizes by default, turn off with `memoize: false` option
+2. Handles dependencies:
+ - When any of the dependencies are `blank?`, the attribute block is not called and the representation returns `nil`.
+ - When any of the dependencies are `invalid?`, `row_model.errors` for dependencies are cleaned. For the example above, if `id/name` are `invalid?`, then
+the `:project` key is removed from the errors, so: `row_model.errors.keys # => [:id, :name]` (applies to warnings as well)
+
+`represents_many` is also available, except it returns `[]` when any of the dependencies are `blank?`.
+
+### 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>, ...]
+```
+
## Dynamic columns
Dynamic columns are columns that can expand to many columns. Currently, we can only one dynamic column after all other standard columns.
The following:
```ruby
@@ -454,10 +451,13 @@
| John | Doe | No | Yes |
| Mario | Super | Yes | No |
| Mike | Jackson | Yes | Yes |
+The `format_dynamic_column_header(header_model, column_name, dynamic_column_index, index_of_column, context)` can
+be used to defined like `format_header`. Defined in both import and export due to headers being used for both.
+
### Export
Dynamic column attributes are arrays, but each item in the array is defined via singular attribute method like
normal columns:
```ruby
@@ -492,11 +492,11 @@
class << self
# Clean/format every dynamic_column attribute array
#
# this is an override with the default implementation
- def format_dynamic_column_cells(cells, column_name)
+ def format_dynamic_column_cells(cells, column_name, column_index, context)
cells
end
end
end
row_model = CsvRowModel::Import::File.new(file_path, DynamicColumnImportModel).next
@@ -516,10 +516,10 @@
include CsvRowModel::Model::FileModel
row :string1
row :string2, header: 'String 2'
- def self.format_header(column_name, context={})
+ def self.format_header(column_name, column_index, context)
":: - #{column_name} - ::"
end
end
```