csv_row_model 0.4.1 → 1.0.0.beta1

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +162 -162
  3. data/csv_row_model.gemspec +2 -1
  4. data/lib/csv_row_model/concerns/{invalid_options.rb → check_options.rb} +4 -6
  5. data/lib/csv_row_model/export/attributes.rb +11 -20
  6. data/lib/csv_row_model/export/base.rb +3 -3
  7. data/lib/csv_row_model/export/cell.rb +24 -0
  8. data/lib/csv_row_model/export/dynamic_column_cell.rb +29 -0
  9. data/lib/csv_row_model/export/dynamic_columns.rb +11 -13
  10. data/lib/csv_row_model/export/file.rb +7 -7
  11. data/lib/csv_row_model/export/file_model.rb +2 -2
  12. data/lib/csv_row_model/export.rb +0 -3
  13. data/lib/csv_row_model/import/attributes.rb +18 -81
  14. data/lib/csv_row_model/import/base.rb +26 -69
  15. data/lib/csv_row_model/import/cell.rb +77 -0
  16. data/lib/csv_row_model/import/csv.rb +23 -60
  17. data/lib/csv_row_model/import/csv_string_model.rb +65 -0
  18. data/lib/csv_row_model/import/dynamic_column_cell.rb +37 -0
  19. data/lib/csv_row_model/import/dynamic_columns.rb +20 -37
  20. data/lib/csv_row_model/import/file/validations.rb +5 -1
  21. data/lib/csv_row_model/import/file.rb +8 -3
  22. data/lib/csv_row_model/import/file_model.rb +5 -4
  23. data/lib/csv_row_model/import/representation.rb +60 -0
  24. data/lib/csv_row_model/import/represents.rb +85 -0
  25. data/lib/csv_row_model/import.rb +4 -3
  26. data/lib/csv_row_model/model/base.rb +5 -15
  27. data/lib/csv_row_model/model/children.rb +2 -1
  28. data/lib/csv_row_model/model/columns.rb +19 -16
  29. data/lib/csv_row_model/model/comparison.rb +1 -1
  30. data/lib/csv_row_model/model/dynamic_column_cell.rb +44 -0
  31. data/lib/csv_row_model/model/dynamic_columns.rb +26 -11
  32. data/lib/csv_row_model/model.rb +4 -3
  33. data/lib/csv_row_model/version.rb +1 -1
  34. data/lib/csv_row_model.rb +3 -1
  35. metadata +29 -10
  36. data/lib/csv_row_model/concerns/inherited_class_var.rb +0 -121
  37. data/lib/csv_row_model/import/presenter.rb +0 -153
  38. data/lib/csv_row_model/model/csv_string_model.rb +0 -7
@@ -1,16 +1,16 @@
1
1
  module CsvRowModel
2
2
  module Export
3
3
  class File
4
- attr_reader :export_model_class, :csv, :file, :context
4
+ attr_reader :row_model_class, :csv, :file, :context
5
5
 
6
6
  # @param [Export] export_model export model class
7
- def initialize(export_model_class, context={})
8
- @export_model_class = export_model_class
7
+ def initialize(row_model_class, context={})
8
+ @row_model_class = row_model_class
9
9
  @context = context.to_h.symbolize_keys
10
10
  end
11
11
 
12
12
  def headers
13
- export_model_class.headers(self.context)
13
+ row_model_class.headers(self.context)
14
14
  end
15
15
 
16
16
  # Add a row_model to the
@@ -18,7 +18,7 @@ module CsvRowModel
18
18
  # @param [Hash] context the extra context given to the instance of the row model
19
19
  # @return [CsvRowModel::Export] the row model appended
20
20
  def append_model(source_model, context={})
21
- row_model = export_model_class.new(source_model, context.reverse_merge(self.context))
21
+ row_model = row_model_class.new(source_model, context.reverse_merge(self.context))
22
22
  row_model.to_rows.each do |row|
23
23
  csv << row
24
24
  end
@@ -34,10 +34,10 @@ module CsvRowModel
34
34
  # Open a block to generate a file
35
35
  # @param [Boolean] with_headers adds the header to the file if true
36
36
  def generate(with_headers: true)
37
- @file = Tempfile.new([export_model_class.name, ".csv"])
37
+ @file = Tempfile.new([row_model_class.name, ".csv"])
38
38
  CSV.open(file.path, "wb") do |csv|
39
39
  @csv = csv
40
- export_model_class.setup(csv, context, with_headers: with_headers)
40
+ row_model_class.setup(csv, context, with_headers: with_headers)
41
41
  yield Proxy.new(self)
42
42
  end
43
43
  ensure
@@ -6,11 +6,11 @@ module CsvRowModel
6
6
  # @return [Array] an array of rows, where if cell is row_name, it's parsed into the header_match
7
7
  # and everything else is return as is.
8
8
  def to_rows
9
- rows_template.map do |row|
9
+ rows_template.map.with_index do |row, index|
10
10
  [].tap do |result|
11
11
  row.each do |cell|
12
12
  if header? cell
13
- result << self.class.format_header(cell, context)
13
+ result << self.class.format_header(cell, index, context)
14
14
  else
15
15
  result << cell.to_s
16
16
  end
@@ -1,7 +1,6 @@
1
1
  require 'csv_row_model/export/base'
2
2
  require 'csv_row_model/export/dynamic_columns'
3
3
  require 'csv_row_model/export/attributes'
4
- require 'csv_row_model/model/comparison'
5
4
 
6
5
  module CsvRowModel
7
6
  # Include this to with {Model} to have a RowModel for exporting to CSVs.
@@ -11,7 +10,5 @@ module CsvRowModel
11
10
  include Base
12
11
  include Attributes
13
12
  include DynamicColumns
14
-
15
- include Model::Comparison # can't be added on Model module because Model does not have attributes implemented
16
13
  end
17
14
  end
@@ -1,4 +1,4 @@
1
- require 'csv_row_model/validators/boolean_format'
1
+ require 'csv_row_model/import/cell'
2
2
 
3
3
  module CsvRowModel
4
4
  module Import
@@ -9,86 +9,37 @@ module CsvRowModel
9
9
  self.column_names.each { |*args| define_attribute_method(*args) }
10
10
  end
11
11
 
12
- # Classes with a validations associated with them in csv_row_model/validators
13
- PARSE_VALIDATION_CLASSES = [Boolean, Integer, Float, Date, DateTime].freeze
14
-
15
- # Mapping of column type classes to a parsing lambda. These are applied after {Import.format_cell}.
16
- # Can pass custom Proc with :parse option.
17
- CLASS_TO_PARSE_LAMBDA = {
18
- nil => ->(s) { s },
19
- Boolean => ->(s) { s =~ BooleanFormatValidator::FALSE_BOOLEAN_REGEX ? false : true },
20
- String => ->(s) { s },
21
- Integer => ->(s) { s.to_i },
22
- Float => ->(s) { s.to_f },
23
- DateTime => ->(s) { s.present? ? DateTime.parse(s) : s },
24
- Date => ->(s) { s.present? ? Date.parse(s) : s }
25
- }.freeze
12
+ def cell_objects
13
+ @cell_objects ||= begin
14
+ csv_string_model.valid?
15
+ _cell_objects(csv_string_model.errors)
16
+ end
17
+ end
26
18
 
27
19
  # @return [Hash] a map of `column_name => original_attribute(column_name)`
28
20
  def original_attributes
29
- self.class.column_names.each { |column_name| original_attribute(column_name) }
30
- @original_attributes
21
+ array_to_block_hash(self.class.column_names) { |column_name| original_attribute(column_name) }
31
22
  end
32
23
 
33
24
  # @return [Object] the column's attribute before override
34
25
  def original_attribute(column_name)
35
- return @original_attributes[column_name] if original_attribute_memoized? column_name
36
-
37
- csv_string_model.valid?
38
- return nil unless csv_string_model.errors[column_name].blank?
39
-
40
- value = self.class.format_cell(mapped_row[column_name], column_name, self.class.index(column_name), context)
41
- if value.present?
42
- value = instance_exec(value, &self.class.parse_lambda(column_name))
43
- elsif self.class.options(column_name)[:default]
44
- original_value = value
45
- value = instance_exec(value, &self.class.default_lambda(column_name))
46
- @default_changes[column_name] = [original_value, value]
47
- end
48
- @original_attributes[column_name] = value
26
+ cell_objects[column_name].try(:value)
49
27
  end
50
28
 
51
29
  # return [Hash] a map changes from {.column}'s default option': `column_name -> [value_before_default, default_set]`
52
30
  def default_changes
53
- original_attributes
54
- @default_changes
31
+ array_to_block_hash(self.class.column_names) { |column_name| cell_objects[column_name].default_change }.delete_if {|k, v| v.blank? }
55
32
  end
56
33
 
57
34
  protected
58
- def original_attribute_memoized?(column_name)
59
- @original_attributes ||= {}
60
- @default_changes ||= {}
61
- @original_attributes.has_key? column_name
35
+ # to prevent circular dependency with csv_string_model
36
+ def _cell_objects(csv_string_model_errors={})
37
+ array_to_block_hash(self.class.column_names) do |column_name|
38
+ Cell.new(column_name, mapped_row[column_name], csv_string_model_errors[column_name], self)
39
+ end
62
40
  end
63
41
 
64
42
  class_methods do
65
- # Safe to override. Method applied to each cell by default
66
- #
67
- # @param cell [String] the cell's string
68
- # @param column_name [Symbol] the cell's column_name
69
- # @param column_index [Integer] the column_name's index
70
- def format_cell(cell, column_name, column_index, context={})
71
- cell
72
- end
73
-
74
- # @return [Lambda] returns a Lambda: ->(original_value) { default_exists? ? default : original_value }
75
- def default_lambda(column_name)
76
- default = options(column_name)[:default]
77
- default.is_a?(Proc) ? ->(s) { instance_exec(&default) } : ->(s) { default.nil? ? s : default }
78
- end
79
-
80
- # @return [Lambda, Proc] returns the Lambda/Proc given in the parse option or:
81
- # ->(original_value) { parse_proc_exists? ? parsed_value : original_value }
82
- def parse_lambda(column_name)
83
- options = options(column_name)
84
-
85
- raise ArgumentError.new("You need either :parse OR :type but not both of them") if options[:parse] && options[:type]
86
-
87
- parse_lambda = options[:parse] || CLASS_TO_PARSE_LAMBDA[options[:type]]
88
- return parse_lambda if parse_lambda
89
- raise ArgumentError.new("type must be #{CLASS_TO_PARSE_LAMBDA.keys.reject(:nil?).join(", ")}")
90
- end
91
-
92
43
  protected
93
44
  # See {Model#column}
94
45
  def column(column_name, options={})
@@ -97,8 +48,8 @@ module CsvRowModel
97
48
  end
98
49
 
99
50
  def merge_options(column_name, options={})
100
- original_options = options(column_name)
101
- add_type_validation(column_name) if !original_options[:validate_type] && options[:validate_type]
51
+ original_options = columns[column_name]
52
+ csv_string_model_class.add_type_validation(column_name, columns[column_name]) unless original_options[:validate_type]
102
53
  super
103
54
  end
104
55
 
@@ -106,23 +57,9 @@ module CsvRowModel
106
57
  # @param column_name [Symbol] the cell's column_name
107
58
  def define_attribute_method(column_name)
108
59
  return if method_defined? column_name
109
- add_type_validation(column_name)
60
+ csv_string_model_class.add_type_validation(column_name, columns[column_name])
110
61
  define_method(column_name) { original_attribute(column_name) }
111
62
  end
112
-
113
- # Adds the type validation based on :validate_type option
114
- def add_type_validation(column_name)
115
- options = options(column_name)
116
- validate_type = options[:validate_type]
117
-
118
- return unless validate_type
119
-
120
- type = options[:type]
121
- raise ArgumentError.new("invalid :type given for :validate_type for column") unless PARSE_VALIDATION_CLASSES.include? type
122
- validate_type = Proc.new { validates column_name, "#{type.name.underscore}_format".to_sym => true, allow_blank: true }
123
-
124
- csv_string_model(&validate_type)
125
- end
126
63
  end
127
64
  end
128
65
  end
@@ -1,31 +1,31 @@
1
- require 'csv_row_model/import/presenter'
2
-
3
1
  module CsvRowModel
4
2
  module Import
5
3
  module Base
6
4
  extend ActiveSupport::Concern
7
5
 
8
6
  included do
9
- attr_reader :source_header, :source_row, :context, :index, :previous
7
+ attr_reader :source_header, :source_row, :line_number, :index, :previous
10
8
 
11
- validates :source_row, presence: true
9
+ validate { errors.add(:csv, "has #{@csv_exception.message}") if @csv_exception }
12
10
  end
13
11
 
14
-
15
- # @param [Array] source_row the csv row
12
+ # @param [Array] source_row_or_exception the csv row
16
13
  # @param options [Hash]
17
- # @option options [Integer] :index index in the CSV file
18
- # @option options [Hash] :context extra data you want to work with the model
14
+ # @option options [Integer] :index 1st row_model is 0, 2nd is 1, 3rd is 2, etc.
15
+ # @option options [Integer] :line_number line_number in the CSV file
19
16
  # @option options [Array] :source_header the csv header row
20
17
  # @option options [CsvRowModel::Import] :previous the previous row model
21
18
  # @option options [CsvRowModel::Import] :parent if the instance is a child, pass the parent
22
- def initialize(source_row, options={})
23
- options = options.symbolize_keys.reverse_merge(context: {})
24
- @source_row, @context = source_row, OpenStruct.new(options[:context])
25
- @index, @source_header, @previous = options[:index], options[:source_header], options[:previous].try(:dup)
19
+ def initialize(source_row_or_exception=[], options={})
20
+ @source_row = source_row_or_exception
21
+ @csv_exception = source_row if source_row.kind_of? Exception
22
+ @source_row = [] if source_row_or_exception.class != Array
23
+
24
+ @line_number, @index, @source_header = options[:line_number], options[:index], options[:source_header]
26
25
 
26
+ @previous = options[:previous].try(:dup)
27
27
  previous.try(:free_previous)
28
- super(source_row, options)
28
+ super(options)
29
29
  end
30
30
 
31
31
  # @return [Hash] a map of `column_name => source_row[index_of_column_name]`
@@ -36,78 +36,45 @@ module CsvRowModel
36
36
 
37
37
  # Free `previous` from memory to avoid making a linked list
38
38
  def free_previous
39
+ attributes
39
40
  @previous = nil
40
41
  end
41
42
 
42
- # @return [Presenter] the presenter of self
43
- def presenter
44
- @presenter ||= self.class.presenter_class.new(self)
45
- end
46
-
47
- # @return [Model::CsvStringModel] a model with validations related to Model::csv_string_model (values are from format_cell)
48
- def csv_string_model
49
- @csv_string_model ||= begin
50
- if source_row
51
- column_names = self.class.column_names
52
- hash = column_names.zip(
53
- column_names.map.with_index do |column_name, index|
54
- self.class.format_cell(source_row[index], column_name, index, context)
55
- end
56
- ).to_h
57
- else
58
- hash = {}
59
- end
60
-
61
- self.class.csv_string_model_class.new(hash)
62
- end
63
- end
64
-
65
43
  # Safe to override.
66
44
  #
67
45
  # @return [Boolean] returns true, if this instance should be skipped
68
46
  def skip?
69
- !valid? || presenter.skip?
47
+ !valid?
70
48
  end
71
49
 
72
50
  # Safe to override.
73
51
  #
74
52
  # @return [Boolean] returns true, if the entire csv file should stop reading
75
53
  def abort?
76
- presenter.abort?
77
- end
78
-
79
- def valid?(*args)
80
- super
81
-
82
- proc = -> do
83
- csv_string_model.valid?(*args)
84
- errors.messages.merge!(csv_string_model.errors.messages.reject {|k, v| v.empty? })
85
- errors.empty?
86
- end
87
-
88
- if using_warnings?
89
- csv_string_model.using_warnings(&proc)
90
- else
91
- proc.call
92
- end
54
+ false
93
55
  end
94
56
 
95
57
  class_methods do
96
- # @param [Import::Csv] csv to read from
58
+ #
59
+ # Move to Import::File once FileModel is removed.
60
+ #
61
+ # @param [Import::File] file to read from
97
62
  # @param [Hash] context extra data you want to work with the model
98
63
  # @param [Import] prevuous the previous row model
99
64
  # @return [Import] the next model instance from the csv
100
- def next(csv, source_header, context={}, previous=nil)
65
+ def next(file, context={})
66
+ csv = file.csv
101
67
  csv.skip_header
102
68
  row_model = nil
103
69
 
104
70
  loop do # loop until the next parent or end_of_file? (need to read children rows)
105
71
  csv.read_row
106
72
  row_model ||= new(csv.current_row,
107
- index: csv.index,
108
- source_header: source_header,
73
+ line_number: csv.line_number,
74
+ index: file.index,
75
+ source_header: csv.header,
109
76
  context: context,
110
- previous: previous)
77
+ previous: file.previous_row_model)
111
78
 
112
79
  return row_model if csv.end_of_file?
113
80
 
@@ -116,20 +83,10 @@ module CsvRowModel
116
83
  end
117
84
  end
118
85
 
119
- # @return [Class] the Class of the Presenter
120
- def presenter_class
121
- @presenter_class ||= inherited_custom_class(:presenter_class, Presenter)
122
- end
123
-
124
86
  protected
125
87
  def inspect_methods
126
88
  @inspect_methods ||= %i[mapped_row initialized_at parent context previous].freeze
127
89
  end
128
-
129
- # Call to define the presenter
130
- def presenter(&block)
131
- presenter_class.class_eval(&block)
132
- end
133
90
  end
134
91
  end
135
92
  end
@@ -0,0 +1,77 @@
1
+ require 'csv_row_model/validators/boolean_format'
2
+
3
+ module CsvRowModel
4
+ module Import
5
+ class Cell
6
+ attr_reader :column_name, :source_value, :csv_string_model_errors, :row_model
7
+
8
+ def initialize(column_name, source_value, csv_string_model_errors, row_model)
9
+ @column_name = column_name
10
+ @source_value = source_value
11
+ @csv_string_model_errors = csv_string_model_errors
12
+ @row_model = row_model
13
+ end
14
+
15
+ def value
16
+ @value ||= begin
17
+ return unless csv_string_model_errors.blank?
18
+ default? ? default_value : parsed_value
19
+ end
20
+ end
21
+
22
+ def formatted_value
23
+ @formatted_value ||= row_model.class.format_cell(source_value, column_name, row_model.class.index(column_name), row_model.context)
24
+ end
25
+
26
+ def parsed_value
27
+ @parsed_value ||= begin
28
+ value = formatted_value
29
+ value.present? ? row_model.instance_exec(formatted_value, &parse_lambda) : value
30
+ end
31
+ end
32
+
33
+ def default_value
34
+ @default_value ||= begin
35
+ default = options[:default]
36
+ default.is_a?(Proc) ? row_model.instance_exec(&default) : default
37
+ end
38
+ end
39
+
40
+ def default?
41
+ !!options[:default] && formatted_value.blank?
42
+ end
43
+
44
+ def default_change
45
+ [formatted_value, default_value] if default?
46
+ end
47
+
48
+ def options
49
+ row_model.class.columns[column_name]
50
+ end
51
+
52
+ protected
53
+
54
+ # Mapping of column type classes to a parsing lambda. These are applied after {Import.format_cell}.
55
+ # Can pass custom Proc with :parse option.
56
+ CLASS_TO_PARSE_LAMBDA = {
57
+ nil => ->(s) { s }, # no type given
58
+ Boolean => ->(s) { s =~ BooleanFormatValidator::FALSE_BOOLEAN_REGEX ? false : true },
59
+ String => ->(s) { s },
60
+ Integer => ->(s) { s.to_i },
61
+ Float => ->(s) { s.to_f },
62
+ DateTime => ->(s) { s.present? ? DateTime.parse(s) : s },
63
+ Date => ->(s) { s.present? ? Date.parse(s) : s }
64
+ }.freeze
65
+
66
+ # @return [Lambda, Proc] returns the Lambda/Proc given in the parse option or:
67
+ # ->(source_value) { parse_proc_exists? ? parsed_value : source_value }
68
+ def parse_lambda
69
+ raise ArgumentError.new("Use :parse OR :type option, but not both for: #{column_name}") if options[:parse] && options[:type]
70
+
71
+ parse_lambda = options[:parse] || CLASS_TO_PARSE_LAMBDA[options[:type]]
72
+ return parse_lambda if parse_lambda
73
+ raise ArgumentError.new("type must be #{CLASS_TO_PARSE_LAMBDA.keys.reject(:nil?).join(", ")} for: #{column_name}")
74
+ end
75
+ end
76
+ end
77
+ end
@@ -1,22 +1,17 @@
1
1
  module CsvRowModel
2
2
  module Import
3
- # Abstraction of Ruby's CSV library. Keeps current row and index, skips empty rows, handles errors.
3
+ # Abstraction of Ruby's CSV library. Keeps current row and line_number, skips empty rows, handles errors.
4
4
  class Csv
5
5
  # @return [String] the file path of the CSV
6
6
  attr_reader :file_path
7
- # @return [Integer, nil] return `-1` at start of file, `0 to infinity` is index of row_model, `nil` is end of file (row is also `nil`)
8
- attr_reader :index
7
+ # @return [Integer, nil] return `0` at start of file, `1 to infinity` is line_number of row_model, `nil` is end of file (row is also `nil`)
8
+ attr_reader :line_number
9
9
  # @return [Array, nil] the current row, or nil at the beginning or end of file
10
10
  attr_reader :current_row
11
- # @return [Hash{Integer => Symbol}] hash of skipped rows from last change in position, `index => :reason`
12
- attr_reader :skipped_rows
13
11
 
14
12
  include ActiveModel::Validations
15
13
 
16
- validate do
17
- begin; _ruby_csv
18
- rescue => e; errors.add(:ruby_csv, e.message) end
19
- end
14
+ validate { begin; _ruby_csv; rescue => e; errors.add(:ruby_csv, e.message) end }
20
15
 
21
16
  def initialize(file_path)
22
17
  @file_path = file_path
@@ -40,93 +35,61 @@ module CsvRowModel
40
35
  def header
41
36
  return unless valid?
42
37
  return @header if @header
43
-
44
- ruby_csv = _ruby_csv
45
- @header = _read_row({}, 0, ruby_csv)
46
- ruby_csv.close
47
- @header
38
+ @header = next_row
48
39
  end
49
40
 
50
41
  # Resets the file to the start of file
51
42
  def reset
52
- return unless valid?
43
+ return false unless valid?
53
44
 
54
- @index = -1
45
+ @line_number = 0
55
46
  @current_row = @next_row = @skipped_rows = @next_skipped_rows = nil
47
+
48
+ @ruby_csv.try(:close)
56
49
  @ruby_csv = _ruby_csv
57
50
  true
58
51
  end
59
52
 
60
53
  # @return [Boolean] true, if the current position is at the start of the file
61
54
  def start_of_file?
62
- index == -1
55
+ line_number == 0
63
56
  end
64
57
 
65
58
  # @return [Boolean] true, if the current position is at the end of the file
66
59
  def end_of_file?
67
- index.nil?
60
+ line_number.nil?
68
61
  end
69
62
 
70
63
  # Returns the next row __without__ changing the position of the CSV
71
64
  # @return [Array, nil] the next row, or `nil` at the end of file
72
65
  def next_row
73
- @next_skipped_rows = {}
74
- @next_row ||= _read_row(@next_skipped_rows)
66
+ @next_row ||= _read_row
75
67
  end
76
68
 
77
69
  # Returns the next row, while changing the position of the CSV
78
70
  # @return [Array, nil] the changed current row, or `nil` at the end of file
79
71
  def read_row
80
- if @next_row
81
- @current_row, @skipped_rows = @next_row, @next_skipped_rows
82
- @next_row = nil
83
- increment_index(@current_row)
84
- else
85
- @skipped_rows = {}
86
- @current_row = _read_row(@skipped_rows) do |row|
87
- increment_index(row)
88
- end
89
- end
72
+ return if end_of_file?
73
+
74
+ @current_row = @next_row || _read_row
75
+ @line_number = current_row.nil? ? nil : @line_number + 1
76
+ @next_row = nil
77
+
90
78
  current_row
91
79
  end
92
80
 
93
81
  protected
94
- def set_end_of_file
95
- @current_row = @index = nil
96
- end
97
-
98
82
  def _ruby_csv
99
83
  CSV.open(file_path)
100
84
  end
101
85
 
102
- def _read_row(skipped_rows={}, index=@index, ruby_csv=@ruby_csv)
86
+ def _read_row(ruby_csv=@ruby_csv)
103
87
  return unless valid?
104
-
105
- row = ruby_csv.readline
106
- raise "empty?" if row.try(:empty?)
107
-
108
- index += 1 if index
109
-
110
- yield row if block_given?
111
- row
88
+ ruby_csv.readline
112
89
  rescue Exception => e
113
- index += 1 if index
114
- yield [] if block_given?
115
-
116
- # thanks to the ruby CSV library, quotes can still escape to the next line
117
- reason = case e.message
118
- when "empty?"; :empty
119
- when /Illegal quoting/i; :illegal_quote
120
- when /Unclosed quoted/i; :unclosed_quote
121
- end
122
-
123
- skipped_rows.merge!(index => reason)
124
-
125
- retry
126
- end
127
-
128
- def increment_index(current_row)
129
- current_row.nil? ? set_end_of_file : @index += 1
90
+ changed = e.exception(e.message.gsub(/line \d+\./, "line #{line_number + 1}.")) # line numbers are usually off
91
+ changed.set_backtrace(e.backtrace)
92
+ return changed
130
93
  end
131
94
  end
132
95
  end
@@ -0,0 +1,65 @@
1
+ module CsvRowModel
2
+ module Import
3
+ module CsvStringModel
4
+ extend ActiveSupport::Concern
5
+
6
+ def valid?(*args)
7
+ super
8
+ call_wrapper = using_warnings? ? csv_string_model.method(:using_warnings) : ->(&block) { block.call }
9
+ call_wrapper.call do
10
+ csv_string_model.valid?(*args)
11
+ errors.messages.merge!(csv_string_model.errors.messages.reject {|k, v| v.empty? })
12
+ errors.empty?
13
+ end
14
+ end
15
+
16
+ # @return [Import::CsvStringModel::Model] a model with validations related to csv_string_model (values are from format_cell)
17
+ # @return [Import::CsvStringModel::Model] a model with validations related to csv_string_model (values are from format_cell)
18
+ def csv_string_model
19
+ @csv_string_model ||= begin
20
+ cell_objects = _cell_objects
21
+ formatted_hash = array_to_block_hash(self.class.column_names) { |column_name| cell_objects[column_name].formatted_value }
22
+ self.class.csv_string_model_class.new(formatted_hash)
23
+ end
24
+ end
25
+
26
+ protected
27
+ def _original_attribute(column_name)
28
+ csv_string_model.valid?
29
+ return nil unless csv_string_model.errors[column_name].blank?
30
+ end
31
+
32
+ class_methods do
33
+ # @return [Class] the Class with validations of the csv_string_model
34
+ def csv_string_model_class
35
+ @csv_string_model_class ||= inherited_custom_class(:csv_string_model_class, Model)
36
+ end
37
+
38
+ protected
39
+ # Called to add validations to the csv_string_model_class
40
+ def csv_string_model(&block)
41
+ csv_string_model_class.class_eval(&block)
42
+ end
43
+ end
44
+
45
+ class Model < OpenStruct
46
+ include ActiveWarnings
47
+
48
+ # Classes with a validations associated with them in csv_row_model/validators
49
+ PARSE_VALIDATION_CLASSES = [Boolean, Integer, Float, Date, DateTime].freeze
50
+
51
+ class << self
52
+ # Adds the type validation based on :validate_type option
53
+ def add_type_validation(column_name, options)
54
+ return unless options[:validate_type]
55
+
56
+ type = options[:type]
57
+ raise ArgumentError.new("invalid :type given for :validate_type for: #{column_name}") unless PARSE_VALIDATION_CLASSES.include? type
58
+
59
+ class_eval { validates column_name, :"#{type.name.underscore}_format" => true, allow_blank: true }
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end