csv_row_model 0.1.0 → 0.1.1
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 +264 -202
- data/csv_row_model.gemspec +1 -1
- data/lib/csv_row_model.rb +7 -7
- data/lib/csv_row_model/concerns/{deep_class_var.rb → inherited_class_var.rb} +11 -11
- data/lib/csv_row_model/export.rb +3 -28
- data/lib/csv_row_model/export/file.rb +49 -0
- data/lib/csv_row_model/export/{single_model.rb → file_model.rb} +8 -17
- data/lib/csv_row_model/import.rb +19 -5
- data/lib/csv_row_model/import/attributes.rb +7 -6
- data/lib/csv_row_model/import/file.rb +24 -82
- data/lib/csv_row_model/import/file/callbacks.rb +2 -1
- data/lib/csv_row_model/import/{single_model.rb → file_model.rb} +20 -7
- data/lib/csv_row_model/import/presenter.rb +15 -9
- data/lib/csv_row_model/model.rb +1 -1
- data/lib/csv_row_model/model/children.rb +2 -2
- data/lib/csv_row_model/model/columns.rb +25 -2
- data/lib/csv_row_model/model/csv_string_model.rb +1 -10
- data/lib/csv_row_model/model/{single_model.rb → file_model.rb} +2 -1
- data/lib/csv_row_model/version.rb +1 -1
- metadata +8 -8
- data/lib/csv_row_model/export/csv.rb +0 -43
data/csv_row_model.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
|
|
7
7
|
spec.name = "csv_row_model"
|
8
8
|
spec.version = CsvRowModel::VERSION
|
9
9
|
spec.authors = ["Steve Chung"]
|
10
|
-
spec.email = ["
|
10
|
+
spec.email = ["hello@stevenchung.ca"]
|
11
11
|
|
12
12
|
spec.summary = "Import and export your custom CSVs with a intuitive shared Ruby interface."
|
13
13
|
spec.homepage = "https://github.com/s12chung/csv_row_model"
|
data/lib/csv_row_model.rb
CHANGED
@@ -3,6 +3,8 @@ autoload = false
|
|
3
3
|
|
4
4
|
class Boolean; end unless defined? Boolean
|
5
5
|
|
6
|
+
require 'csv_row_model/version'
|
7
|
+
|
6
8
|
require 'active_model'
|
7
9
|
require 'active_support/all'
|
8
10
|
require 'active_warnings'
|
@@ -11,25 +13,23 @@ require 'csv'
|
|
11
13
|
if autoload && defined?(Rails)
|
12
14
|
require 'csv_row_model/engine'
|
13
15
|
else
|
14
|
-
require 'csv_row_model/version'
|
15
|
-
|
16
16
|
require 'csv_row_model/concerns/inspect'
|
17
|
-
require 'csv_row_model/concerns/
|
17
|
+
require 'csv_row_model/concerns/inherited_class_var'
|
18
18
|
|
19
19
|
require 'csv_row_model/validators/validate_attributes'
|
20
20
|
|
21
21
|
require 'csv_row_model/model'
|
22
|
-
require 'csv_row_model/model/
|
22
|
+
require 'csv_row_model/model/file_model'
|
23
23
|
|
24
24
|
require 'csv_row_model/import'
|
25
|
-
require 'csv_row_model/import/
|
25
|
+
require 'csv_row_model/import/file_model'
|
26
26
|
require 'csv_row_model/import/csv'
|
27
27
|
require 'csv_row_model/import/file'
|
28
28
|
|
29
29
|
|
30
30
|
require 'csv_row_model/export'
|
31
|
-
require 'csv_row_model/export/
|
32
|
-
require 'csv_row_model/export/
|
31
|
+
require 'csv_row_model/export/file'
|
32
|
+
require 'csv_row_model/export/file_model'
|
33
33
|
end
|
34
34
|
|
35
35
|
require 'csv_row_model/validators/default_change'
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module CsvRowModel
|
2
2
|
module Concerns
|
3
|
-
module
|
3
|
+
module InheritedClassVar
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
6
|
class_methods do
|
7
7
|
# Clears the cache for a variable
|
8
8
|
# @param variable_name [Symbol] variable_name to cache against
|
9
9
|
def clear_class_cache(variable_name)
|
10
|
-
instance_variable_set
|
10
|
+
instance_variable_set inherited_class_variable_name(variable_name), nil
|
11
11
|
end
|
12
12
|
|
13
13
|
protected
|
@@ -44,8 +44,8 @@ module CsvRowModel
|
|
44
44
|
# @param default_value [Object] default value of the class variable
|
45
45
|
# @param merge_method [Symbol] method to merge values of the class variable
|
46
46
|
# @return [Object] a class variable merged across ancestors until deep_class_module
|
47
|
-
def
|
48
|
-
|
47
|
+
def inherited_class_var(variable_name, default_value, merge_method)
|
48
|
+
class_cache(variable_name) do
|
49
49
|
value = default_value
|
50
50
|
|
51
51
|
inherited_ancestors.each do |ancestor|
|
@@ -59,25 +59,25 @@ module CsvRowModel
|
|
59
59
|
|
60
60
|
# @param variable_name [Symbol] variable_name to cache against
|
61
61
|
# @return [String] the cache variable name for the cache
|
62
|
-
def
|
63
|
-
"#{variable_name}
|
62
|
+
def inherited_class_variable_name(variable_name)
|
63
|
+
"#{variable_name}_inherited_class_cache"
|
64
64
|
end
|
65
65
|
|
66
66
|
# Clears the cache for a variable and the same variable for all it's dependant descendants
|
67
67
|
# @param variable_name [Symbol] variable_name to cache against
|
68
|
-
def
|
68
|
+
def deep_clear_class_cache(variable_name)
|
69
69
|
([self] + descendants).each do |descendant|
|
70
70
|
descendant.try(:clear_class_cache, variable_name)
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
|
-
# Memozies a
|
74
|
+
# Memozies a inherited_class_variable_name
|
75
75
|
# @param variable_name [Symbol] variable_name to cache against
|
76
|
-
def
|
76
|
+
def class_cache(variable_name)
|
77
77
|
#
|
78
|
-
# equal to: (has @)
|
78
|
+
# equal to: (has @)inherited_class_variable_name ||= yield
|
79
79
|
#
|
80
|
-
cache_variable_name =
|
80
|
+
cache_variable_name = inherited_class_variable_name(variable_name)
|
81
81
|
instance_variable_get(cache_variable_name) || instance_variable_set(cache_variable_name, yield)
|
82
82
|
end
|
83
83
|
end
|
data/lib/csv_row_model/export.rb
CHANGED
@@ -24,7 +24,7 @@ module CsvRowModel
|
|
24
24
|
# @param [Hash] context
|
25
25
|
def initialize(source_model, context={})
|
26
26
|
@source_model = source_model
|
27
|
-
@context = context
|
27
|
+
@context = OpenStruct.new(context)
|
28
28
|
end
|
29
29
|
|
30
30
|
def to_rows
|
@@ -37,34 +37,9 @@ module CsvRowModel
|
|
37
37
|
end
|
38
38
|
|
39
39
|
class_methods do
|
40
|
-
|
41
|
-
|
42
|
-
def column_headers
|
43
|
-
@column_headers ||= begin
|
44
|
-
columns.map do |name, options|
|
45
|
-
options[:header] || format_header(name)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# Safe to override
|
51
|
-
#
|
52
|
-
# @return [String] formatted header
|
53
|
-
def format_header(column_name)
|
54
|
-
column_name
|
55
|
-
end
|
56
|
-
|
57
|
-
|
58
|
-
# @return [Boolean] by default false
|
59
|
-
def single_model?
|
60
|
-
false
|
40
|
+
def setup(csv, with_headers: true)
|
41
|
+
csv << headers if with_headers
|
61
42
|
end
|
62
43
|
end
|
63
|
-
|
64
|
-
private
|
65
|
-
|
66
|
-
def is_column_name? column_name
|
67
|
-
column_name.is_a?(Symbol) && self.class.index(column_name)
|
68
|
-
end
|
69
44
|
end
|
70
45
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module CsvRowModel
|
2
|
+
module Export
|
3
|
+
class File
|
4
|
+
attr_reader :export_model_class, :csv, :file, :context
|
5
|
+
|
6
|
+
# @param [Export] export_model export model class
|
7
|
+
def initialize(export_model_class, context={})
|
8
|
+
@export_model_class = export_model_class
|
9
|
+
@context = context.to_h.symbolize_keys
|
10
|
+
end
|
11
|
+
|
12
|
+
def headers
|
13
|
+
export_model_class.headers
|
14
|
+
end
|
15
|
+
|
16
|
+
# Add a row_model to the
|
17
|
+
# @param [] source_model the source model of the export row model
|
18
|
+
# @param [Hash] context the extra context given to the instance of the row model
|
19
|
+
def append_model(source_model, context={})
|
20
|
+
export_model_class.new(source_model, context.to_h.reverse_merge(self.context)).to_rows.each do |row|
|
21
|
+
csv << row
|
22
|
+
end
|
23
|
+
end
|
24
|
+
alias_method :<<, :append_model
|
25
|
+
|
26
|
+
# @return [Boolean] true, if a csv file is generated
|
27
|
+
def generated?
|
28
|
+
!!file
|
29
|
+
end
|
30
|
+
|
31
|
+
# Open a block to generate a file
|
32
|
+
# @param [Boolean] with_headers adds the header to the file if true
|
33
|
+
def generate(with_headers: true)
|
34
|
+
@file = Tempfile.new([export_model_class.name, ".csv"])
|
35
|
+
CSV.open(file.path, "wb") do |csv|
|
36
|
+
@csv = csv
|
37
|
+
export_model_class.setup(csv, with_headers: with_headers)
|
38
|
+
yield self
|
39
|
+
end
|
40
|
+
ensure
|
41
|
+
@csv = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
file.read
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -1,27 +1,15 @@
|
|
1
1
|
module CsvRowModel
|
2
2
|
module Export
|
3
|
-
module
|
3
|
+
module FileModel
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
-
included do
|
7
|
-
|
8
|
-
alias_method :is_row_name?, :is_column_name?
|
9
|
-
|
10
|
-
class << self
|
11
|
-
# @return [Boolean] by default false
|
12
|
-
def single_model?
|
13
|
-
true
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
6
|
# @return [Array] an array of rows, where if cell is row_name, it's parsed into the header_match
|
19
7
|
# and everything else is return as is.
|
20
8
|
def to_rows
|
21
|
-
|
9
|
+
rows_template.map do |row|
|
22
10
|
result = []
|
23
11
|
row.each do |cell|
|
24
|
-
if is_row_name? cell
|
12
|
+
if self.class.is_row_name? cell
|
25
13
|
header_matchs = self.class.options(cell)[:header_matchs]
|
26
14
|
result << "#{header_matchs ? header_matchs.first : self.class.format_header(cell)}"
|
27
15
|
result << "#{attributes[cell]}"
|
@@ -38,10 +26,13 @@ module CsvRowModel
|
|
38
26
|
# @return [Array<Array>] an array of arrays, where every represents a row and every row
|
39
27
|
# can have strings and row_name (column_name). By default,
|
40
28
|
# returns a row_name for every row
|
41
|
-
def
|
42
|
-
@
|
29
|
+
def rows_template
|
30
|
+
@rows_template ||= self.class.row_names.map{ |row_name| [row_name]}
|
43
31
|
end
|
44
32
|
|
33
|
+
class_methods do
|
34
|
+
def setup(csv, with_headers: true); end
|
35
|
+
end
|
45
36
|
end
|
46
37
|
end
|
47
38
|
end
|
data/lib/csv_row_model/import.rb
CHANGED
@@ -95,17 +95,31 @@ module CsvRowModel
|
|
95
95
|
end
|
96
96
|
|
97
97
|
class_methods do
|
98
|
-
# by default import model is a collection model
|
99
|
-
def type
|
100
|
-
:collection_model
|
101
|
-
end
|
102
|
-
|
103
98
|
# See {Model#column}
|
104
99
|
def column(column_name, options={})
|
105
100
|
super
|
106
101
|
define_attribute_method(column_name)
|
107
102
|
end
|
108
103
|
|
104
|
+
# @param [Import::Csv] csv to read from
|
105
|
+
# @param [Hash] context extra data you want to work with the model
|
106
|
+
# @param [Import] prevuous the previous row model
|
107
|
+
# @return [Import] the next model instance from the csv
|
108
|
+
def next(csv, context={}, previous=nil)
|
109
|
+
csv.skip_header
|
110
|
+
row_model = nil
|
111
|
+
|
112
|
+
loop do # loop until the next parent or end_of_file? (need to read children rows)
|
113
|
+
csv.read_row
|
114
|
+
row_model ||= new(csv.current_row, index: csv.index, context: context, previous: previous)
|
115
|
+
|
116
|
+
return row_model if csv.end_of_file?
|
117
|
+
|
118
|
+
next_row_is_parent = !row_model.append_child(csv.next_row)
|
119
|
+
return row_model if next_row_is_parent
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
109
123
|
# @return [Class] the Class of the Presenter
|
110
124
|
def presenter_class
|
111
125
|
@presenter_class ||= inherited_custom_class(:presenter_class, Presenter)
|
@@ -20,28 +20,29 @@ module CsvRowModel
|
|
20
20
|
|
21
21
|
# @return [Hash] a map of `column_name => original_attribute(column_name)`
|
22
22
|
def original_attributes
|
23
|
-
|
24
|
-
|
25
|
-
self.class.column_names.zip(values).to_h
|
26
|
-
end
|
23
|
+
self.class.column_names.each { |column_name| original_attribute(column_name) }
|
24
|
+
@original_attributes
|
27
25
|
end
|
28
26
|
|
29
27
|
# @return [Object] the column's attribute before override
|
30
28
|
def original_attribute(column_name)
|
29
|
+
@original_attributes ||= {}
|
31
30
|
@default_changes ||= {}
|
32
31
|
|
32
|
+
return @original_attributes[column_name] if @original_attributes.has_key? column_name
|
33
|
+
|
33
34
|
csv_string_model.valid?
|
34
35
|
return nil unless csv_string_model.errors[column_name].blank?
|
35
36
|
|
36
37
|
value = self.class.format_cell(mapped_row[column_name], column_name, self.class.index(column_name))
|
37
38
|
if value.present?
|
38
|
-
instance_exec(value, &self.class.parse_lambda(column_name))
|
39
|
+
value = instance_exec(value, &self.class.parse_lambda(column_name))
|
39
40
|
elsif self.class.options(column_name)[:default]
|
40
41
|
original_value = value
|
41
42
|
value = instance_exec(value, &self.class.default_lambda(column_name))
|
42
43
|
@default_changes[column_name] = [original_value, value]
|
43
|
-
value
|
44
44
|
end
|
45
|
+
@original_attributes[column_name] = value
|
45
46
|
end
|
46
47
|
|
47
48
|
# return [Hash] a map changes from {.column}'s default option': `column_name -> [value_before_default, default_set]`
|
@@ -3,30 +3,33 @@ require 'csv_row_model/import/file/validations'
|
|
3
3
|
|
4
4
|
module CsvRowModel
|
5
5
|
module Import
|
6
|
-
# Represents a csv file and handles parsing to return `Import`
|
6
|
+
# Represents a csv file and handles parsing to return `Import`
|
7
7
|
class File
|
8
8
|
include Callbacks
|
9
9
|
include Validations
|
10
10
|
|
11
11
|
# @return [Csv]
|
12
12
|
attr_reader :csv
|
13
|
-
# @return [Input
|
13
|
+
# @return [Input] model class returned for importing
|
14
14
|
attr_reader :row_model_class
|
15
15
|
|
16
16
|
# Current index of the row model
|
17
17
|
# @return [Integer] returns -1 = start of file, 0 to infinity = index of row_model, nil = end of file, no row_model
|
18
18
|
attr_reader :index
|
19
|
-
# @return [Input
|
19
|
+
# @return [Input] the current row model set by {#next}
|
20
20
|
attr_reader :current_row_model
|
21
|
-
# @return [Input
|
21
|
+
# @return [Input] the previous row model set by {#next}
|
22
22
|
attr_reader :previous_row_model
|
23
|
+
# @return [Hash] context passed to the {Import}
|
24
|
+
attr_reader :context
|
23
25
|
|
24
|
-
delegate :header, :size, :skipped_rows, to: :csv
|
26
|
+
delegate :header, :size, :skipped_rows, :end_of_file?, to: :csv
|
25
27
|
|
26
28
|
# @param [String] file_path path of csv file
|
27
|
-
# @param [Import
|
28
|
-
|
29
|
-
|
29
|
+
# @param [Import] row_model_class model class returned for importing
|
30
|
+
# @param context [Hash] context passed to the {Import}
|
31
|
+
def initialize(file_path, row_model_class, context={})
|
32
|
+
@csv, @row_model_class, @context = Csv.new(file_path), row_model_class, context.to_h.symbolize_keys
|
30
33
|
reset
|
31
34
|
end
|
32
35
|
|
@@ -38,96 +41,35 @@ module CsvRowModel
|
|
38
41
|
end
|
39
42
|
|
40
43
|
# Gets the next row model based on the context
|
41
|
-
#
|
42
|
-
# @param context [Hash] context passed to the {Import}
|
43
44
|
def next(context={})
|
44
|
-
if
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
45
|
+
return if end_of_file?
|
46
|
+
|
47
|
+
run_callbacks :next do
|
48
|
+
context = context.to_h.reverse_merge(self.context)
|
49
|
+
@previous_row_model = current_row_model
|
50
|
+
@current_row_model = row_model_class.next(csv, context, previous_row_model)
|
51
|
+
@index += 1
|
52
|
+
@current_row_model = @index = nil if end_of_file?
|
49
53
|
end
|
50
|
-
end
|
51
54
|
|
52
|
-
|
53
|
-
def end_of_file?
|
54
|
-
csv.end_of_file?
|
55
|
+
current_row_model
|
55
56
|
end
|
56
57
|
|
57
58
|
# Iterates through the entire csv file and provides the `current_row_model` in a block, while handing aborts and skips
|
58
59
|
# via. calling {Model#abort?} and {Model#skip?}
|
59
|
-
#
|
60
|
-
# @param context [Hash] context passed to the {Import}
|
61
60
|
def each(context={})
|
62
|
-
return to_enum(__callee__
|
61
|
+
return to_enum(__callee__) unless block_given?
|
63
62
|
return false if _abort?
|
64
63
|
|
65
64
|
while self.next(context)
|
66
|
-
|
67
|
-
|
65
|
+
run_callbacks :each_iteration do
|
66
|
+
return false if _abort?
|
67
|
+
next if _skip?
|
68
68
|
|
69
|
-
run_callbacks :yield do
|
70
69
|
yield current_row_model
|
71
70
|
end
|
72
71
|
end
|
73
72
|
end
|
74
|
-
|
75
|
-
protected
|
76
|
-
|
77
|
-
# @return [boolean] if type of model is collection_model
|
78
|
-
def is_single_model?
|
79
|
-
@is_single_model ||= begin
|
80
|
-
row_model_class.respond_to?(:type) ? (row_model_class.type == :single_model) : false
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
def set_current_collection_model(context)
|
85
|
-
@current_row_model = row_model_class.new(csv.current_row, index: csv.index, context: context, source_header: header, previous: previous_row_model)
|
86
|
-
@index += 1
|
87
|
-
end
|
88
|
-
|
89
|
-
def set_single_model(context={})
|
90
|
-
source_row = Array.new(row_model_class.header_matchers.size)
|
91
|
-
while !end_of_file?
|
92
|
-
csv.read_row
|
93
|
-
update_source_row(source_row)
|
94
|
-
end
|
95
|
-
@current_row_model = row_model_class.new(source_row, context: context)
|
96
|
-
end
|
97
|
-
|
98
|
-
def update_source_row(source_row)
|
99
|
-
current_row = csv.current_row
|
100
|
-
return unless current_row
|
101
|
-
current_row.each_with_index do |cell, position|
|
102
|
-
next if cell.blank?
|
103
|
-
index = row_model_class.index_header_match(cell)
|
104
|
-
next unless index
|
105
|
-
source_row[index] = current_row[position + 1]
|
106
|
-
break
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def next_collection_model(context)
|
111
|
-
csv.skip_header
|
112
|
-
|
113
|
-
next_row_is_parent = true
|
114
|
-
loop do
|
115
|
-
@previous_row_model = current_row_model if next_row_is_parent
|
116
|
-
|
117
|
-
csv.read_row
|
118
|
-
return set_end_of_file if csv.end_of_file?
|
119
|
-
|
120
|
-
set_current_collection_model(context) if next_row_is_parent
|
121
|
-
|
122
|
-
next_row_is_parent = !current_row_model.append_child(csv.next_row)
|
123
|
-
return current_row_model if next_row_is_parent
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
def set_end_of_file
|
128
|
-
# please return nil
|
129
|
-
@current_row_model = @index = nil
|
130
|
-
end
|
131
73
|
end
|
132
74
|
end
|
133
75
|
end
|