csv_row_model 0.4.1 → 1.0.0.beta1

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