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