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.
- checksums.yaml +4 -4
- data/README.md +162 -162
- data/csv_row_model.gemspec +2 -1
- data/lib/csv_row_model/concerns/{invalid_options.rb → check_options.rb} +4 -6
- data/lib/csv_row_model/export/attributes.rb +11 -20
- data/lib/csv_row_model/export/base.rb +3 -3
- data/lib/csv_row_model/export/cell.rb +24 -0
- data/lib/csv_row_model/export/dynamic_column_cell.rb +29 -0
- data/lib/csv_row_model/export/dynamic_columns.rb +11 -13
- data/lib/csv_row_model/export/file.rb +7 -7
- data/lib/csv_row_model/export/file_model.rb +2 -2
- data/lib/csv_row_model/export.rb +0 -3
- data/lib/csv_row_model/import/attributes.rb +18 -81
- data/lib/csv_row_model/import/base.rb +26 -69
- data/lib/csv_row_model/import/cell.rb +77 -0
- data/lib/csv_row_model/import/csv.rb +23 -60
- data/lib/csv_row_model/import/csv_string_model.rb +65 -0
- data/lib/csv_row_model/import/dynamic_column_cell.rb +37 -0
- data/lib/csv_row_model/import/dynamic_columns.rb +20 -37
- data/lib/csv_row_model/import/file/validations.rb +5 -1
- data/lib/csv_row_model/import/file.rb +8 -3
- data/lib/csv_row_model/import/file_model.rb +5 -4
- data/lib/csv_row_model/import/representation.rb +60 -0
- data/lib/csv_row_model/import/represents.rb +85 -0
- data/lib/csv_row_model/import.rb +4 -3
- data/lib/csv_row_model/model/base.rb +5 -15
- data/lib/csv_row_model/model/children.rb +2 -1
- data/lib/csv_row_model/model/columns.rb +19 -16
- data/lib/csv_row_model/model/comparison.rb +1 -1
- data/lib/csv_row_model/model/dynamic_column_cell.rb +44 -0
- data/lib/csv_row_model/model/dynamic_columns.rb +26 -11
- data/lib/csv_row_model/model.rb +4 -3
- data/lib/csv_row_model/version.rb +1 -1
- data/lib/csv_row_model.rb +3 -1
- metadata +29 -10
- data/lib/csv_row_model/concerns/inherited_class_var.rb +0 -121
- data/lib/csv_row_model/import/presenter.rb +0 -153
- 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 :
|
4
|
+
attr_reader :row_model_class, :csv, :file, :context
|
5
5
|
|
6
6
|
# @param [Export] export_model export model class
|
7
|
-
def initialize(
|
8
|
-
@
|
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
|
-
|
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 =
|
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([
|
37
|
+
@file = Tempfile.new([row_model_class.name, ".csv"])
|
38
38
|
CSV.open(file.path, "wb") do |csv|
|
39
39
|
@csv = csv
|
40
|
-
|
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
|
data/lib/csv_row_model/export.rb
CHANGED
@@ -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/
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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 =
|
101
|
-
add_type_validation(column_name
|
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, :
|
7
|
+
attr_reader :source_header, :source_row, :line_number, :index, :previous
|
10
8
|
|
11
|
-
|
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
|
18
|
-
# @option options [
|
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(
|
23
|
-
|
24
|
-
@
|
25
|
-
@
|
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(
|
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?
|
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
|
-
|
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
|
-
#
|
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(
|
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
|
-
|
108
|
-
|
73
|
+
line_number: csv.line_number,
|
74
|
+
index: file.index,
|
75
|
+
source_header: csv.header,
|
109
76
|
context: context,
|
110
|
-
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
|
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
|
8
|
-
attr_reader :
|
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
|
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
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
@
|
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
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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(
|
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
|
-
|
114
|
-
|
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
|