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 ```