csv_row_model 0.1.0
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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/.yardopts +10 -0
- data/CONTRIBUTING.md +7 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +369 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/csv_row_model.gemspec +23 -0
- data/lib/csv_row_model.rb +46 -0
- data/lib/csv_row_model/concerns/deep_class_var.rb +86 -0
- data/lib/csv_row_model/concerns/inspect.rb +10 -0
- data/lib/csv_row_model/engine.rb +5 -0
- data/lib/csv_row_model/export.rb +70 -0
- data/lib/csv_row_model/export/csv.rb +43 -0
- data/lib/csv_row_model/export/single_model.rb +47 -0
- data/lib/csv_row_model/import.rb +125 -0
- data/lib/csv_row_model/import/attributes.rb +105 -0
- data/lib/csv_row_model/import/csv.rb +136 -0
- data/lib/csv_row_model/import/csv/row.rb +17 -0
- data/lib/csv_row_model/import/file.rb +133 -0
- data/lib/csv_row_model/import/file/callbacks.rb +16 -0
- data/lib/csv_row_model/import/file/validations.rb +39 -0
- data/lib/csv_row_model/import/presenter.rb +160 -0
- data/lib/csv_row_model/import/single_model.rb +41 -0
- data/lib/csv_row_model/model.rb +68 -0
- data/lib/csv_row_model/model/children.rb +79 -0
- data/lib/csv_row_model/model/columns.rb +73 -0
- data/lib/csv_row_model/model/csv_string_model.rb +16 -0
- data/lib/csv_row_model/model/single_model.rb +16 -0
- data/lib/csv_row_model/validators/boolean_format.rb +11 -0
- data/lib/csv_row_model/validators/date_format.rb +9 -0
- data/lib/csv_row_model/validators/default_change.rb +6 -0
- data/lib/csv_row_model/validators/float_format.rb +9 -0
- data/lib/csv_row_model/validators/integer_format.rb +9 -0
- data/lib/csv_row_model/validators/number_validator.rb +16 -0
- data/lib/csv_row_model/validators/validate_attributes.rb +27 -0
- data/lib/csv_row_model/version.rb +3 -0
- metadata +116 -0
@@ -0,0 +1,160 @@
|
|
1
|
+
module CsvRowModel
|
2
|
+
module Import
|
3
|
+
class Presenter
|
4
|
+
include Concerns::DeepClassVar
|
5
|
+
include Concerns::Inspect
|
6
|
+
include ActiveWarnings
|
7
|
+
|
8
|
+
attr_reader :row_model
|
9
|
+
|
10
|
+
delegate :context, to: :row_model
|
11
|
+
|
12
|
+
def initialize(row_model)
|
13
|
+
@row_model = row_model
|
14
|
+
end
|
15
|
+
|
16
|
+
# Safe to override.
|
17
|
+
#
|
18
|
+
# @return [Boolean] returns true, if this instance should be skipped
|
19
|
+
def skip?
|
20
|
+
!valid?
|
21
|
+
end
|
22
|
+
|
23
|
+
# Safe to override.
|
24
|
+
#
|
25
|
+
# @return [Boolean] returns true, if the entire csv file should stop reading
|
26
|
+
def abort?
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def valid?(*args)
|
31
|
+
super
|
32
|
+
filter_errors
|
33
|
+
errors.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Presenter] returns the presenter of the previous row_model
|
37
|
+
def previous
|
38
|
+
row_model.previous.try(:presenter)
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
# add errors from row_model and remove each dependent attribute from errors if it's row_model_dependencies
|
44
|
+
# are in the errors
|
45
|
+
def filter_errors
|
46
|
+
using_warnings? ? row_model.using_warnings { _filter_errors } : _filter_errors
|
47
|
+
end
|
48
|
+
|
49
|
+
def _filter_errors
|
50
|
+
row_model.valid?
|
51
|
+
self.class.attribute_names.each do |attribute_name|
|
52
|
+
next unless errors.messages[attribute_name] &&
|
53
|
+
row_model.errors.messages.slice(*self.class.options(attribute_name)[:dependencies]).present?
|
54
|
+
errors.delete attribute_name
|
55
|
+
end
|
56
|
+
|
57
|
+
errors.messages.reverse_merge!(row_model.errors.messages)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param [Symbol] attribute_name the attribute to check
|
61
|
+
# @return [Boolean] if the dependencies are valid
|
62
|
+
def valid_dependencies?(attribute_name)
|
63
|
+
row_model.valid? || (row_model.errors.keys & self.class.options(attribute_name)[:dependencies]).empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
# equal to: @method_name ||= yield
|
67
|
+
# @param [Symbol] method_name method_name in description
|
68
|
+
# @return [Object] the memoized result
|
69
|
+
def memoize(method_name)
|
70
|
+
variable_name = "@#{method_name}"
|
71
|
+
instance_variable_get(variable_name) || instance_variable_set(variable_name, yield)
|
72
|
+
end
|
73
|
+
|
74
|
+
class << self
|
75
|
+
def deep_class_module
|
76
|
+
Presenter
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [Array<Symbol>] attribute names for the Mapper
|
80
|
+
def attribute_names
|
81
|
+
attributes.keys
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [Hash{Symbol => Array}] map of `attribute_name => [options, block]`
|
85
|
+
def attributes
|
86
|
+
deep_class_var :@_mapper_attributes, {}, :merge
|
87
|
+
end
|
88
|
+
|
89
|
+
# @param [Symbol] attribute_name name of attribute to find option
|
90
|
+
# @return [Hash] options for the attribute_name
|
91
|
+
def options(attribute_name)
|
92
|
+
attributes[attribute_name].first
|
93
|
+
end
|
94
|
+
|
95
|
+
# @param [Symbol] attribute_name name of attribute to find block
|
96
|
+
# @return [Proc, Lambda] block called for attribute
|
97
|
+
def block(attribute_name)
|
98
|
+
attributes[attribute_name].last
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return [Hash{Symbol => Array}] map of `dependency => [array of mapper attributes dependent on dependency]`
|
102
|
+
def dependencies
|
103
|
+
deep_class_cache(:@_mapper_dependencies) do
|
104
|
+
dependencies = {}
|
105
|
+
attribute_names.each do |attribute_name|
|
106
|
+
options(attribute_name)[:dependencies].each do |dependency|
|
107
|
+
dependencies[dependency] ||= []
|
108
|
+
dependencies[dependency] << attribute_name
|
109
|
+
end
|
110
|
+
end
|
111
|
+
dependencies
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
protected
|
116
|
+
def inspect_methods
|
117
|
+
@inspect_methods ||= %i[row_model].freeze
|
118
|
+
end
|
119
|
+
|
120
|
+
def merge_attribute(attribute_hash)
|
121
|
+
@_mapper_attributes ||= {}
|
122
|
+
clear_deep_class_cache(:@_mapper_attributes)
|
123
|
+
clear_deep_class_cache(:@_mapper_dependencies)
|
124
|
+
@_mapper_attributes.merge! attribute_hash
|
125
|
+
end
|
126
|
+
|
127
|
+
# Adds column to the row model
|
128
|
+
#
|
129
|
+
# @param [Symbol] attribute_name name of attribute to add
|
130
|
+
# @param [Proc] block to calculate the attribute
|
131
|
+
# @param options [Hash]
|
132
|
+
# @option options [Hash] :memoize whether to memoize the attribute (default: true)
|
133
|
+
# @option options [Hash] :dependencies the dependcies it has with the underlying row_model (default: [])
|
134
|
+
def attribute(attribute_name, options={}, &block)
|
135
|
+
default_options = { memoize: true, dependencies: [] }
|
136
|
+
invalid_options = options.keys - default_options.keys
|
137
|
+
raise ArgumentError.new("Invalid option(s): #{invalid_options}") if invalid_options.present?
|
138
|
+
|
139
|
+
options = options.reverse_merge(default_options)
|
140
|
+
|
141
|
+
merge_attribute(attribute_name.to_sym => [options, block])
|
142
|
+
define_attribute_method(attribute_name)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Define the attribute_method
|
146
|
+
# @param [Symbol] attribute_name name of attribute to add
|
147
|
+
def define_attribute_method(attribute_name)
|
148
|
+
define_method("__#{attribute_name}", &block(attribute_name))
|
149
|
+
|
150
|
+
define_method(attribute_name) do
|
151
|
+
return unless valid_dependencies?(attribute_name)
|
152
|
+
self.class.options(attribute_name)[:memoize] ?
|
153
|
+
memoize(attribute_name) { public_send("__#{attribute_name}") } :
|
154
|
+
public_send("__#{attribute_name}")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module CsvRowModel
|
2
|
+
# Include this to with {Model} to have a RowModel for importing csvs that
|
3
|
+
# represents just one model.
|
4
|
+
# It needs CsvRowModel::Import
|
5
|
+
module Import
|
6
|
+
module SingleModel
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
|
11
|
+
# @return [Symbol] returns type of import
|
12
|
+
def type
|
13
|
+
:single_model
|
14
|
+
end
|
15
|
+
|
16
|
+
# Safe to override
|
17
|
+
#
|
18
|
+
# @param cell [String] the cell's string
|
19
|
+
# @return [Integer] returns index of the header_match that cell match
|
20
|
+
def index_header_match(cell)
|
21
|
+
match = header_matchers.each_with_index.select do |matcher, index|
|
22
|
+
cell.match(matcher)
|
23
|
+
end.first
|
24
|
+
match ? match[1] : nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Array] header_matchs matchers for the row model
|
28
|
+
def header_matchers
|
29
|
+
@header_matchers ||= begin
|
30
|
+
columns.map do |name, options|
|
31
|
+
matchers = options[:header_matchs] || [name.to_s]
|
32
|
+
Regexp.new(matchers.join('|'),Regexp::IGNORECASE)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'csv_row_model/model/csv_string_model'
|
2
|
+
|
3
|
+
require 'csv_row_model/model/columns'
|
4
|
+
require 'csv_row_model/model/children'
|
5
|
+
|
6
|
+
module CsvRowModel
|
7
|
+
# Base module for representing a RowModel---a model that represents row(s).
|
8
|
+
module Model
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
include Concerns::DeepClassVar
|
13
|
+
|
14
|
+
include ActiveWarnings
|
15
|
+
include Validators::ValidateAttributes
|
16
|
+
|
17
|
+
include Columns
|
18
|
+
include Children
|
19
|
+
|
20
|
+
# @return [Model] return the parent, if this instance is a child
|
21
|
+
attr_reader :parent
|
22
|
+
|
23
|
+
# @return [DateTime] return when self has been intialized
|
24
|
+
attr_reader :initialized_at
|
25
|
+
|
26
|
+
validate_attributes :parent
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param [NilClass] source not used here, see {Input}
|
30
|
+
# @param [Hash] options
|
31
|
+
# @option options [String] :parent if the instance is a child, pass the parent
|
32
|
+
def initialize(source=nil, options={})
|
33
|
+
@initialized_at = DateTime.now
|
34
|
+
@parent = options[:parent]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Safe to override.
|
38
|
+
#
|
39
|
+
# @return [Boolean] returns true, if this instance should be skipped
|
40
|
+
def skip?
|
41
|
+
!valid?
|
42
|
+
end
|
43
|
+
|
44
|
+
# Safe to override.
|
45
|
+
#
|
46
|
+
# @return [Boolean] returns true, if the entire csv file should stop reading
|
47
|
+
def abort?
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
class_methods do
|
52
|
+
# @return [Class] the Class with validations of the csv_string_model
|
53
|
+
def csv_string_model_class
|
54
|
+
@csv_string_model_class ||= inherited_custom_class(:csv_string_model_class, CsvStringModel)
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
# Called to add validations to the csv_string_model_class
|
59
|
+
def csv_string_model(&block)
|
60
|
+
csv_string_model_class.class_eval &block
|
61
|
+
end
|
62
|
+
|
63
|
+
def deep_class_module
|
64
|
+
Model
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module CsvRowModel
|
2
|
+
module Model
|
3
|
+
module Children
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# @return [Boolean] returns true, if the instance is a child
|
7
|
+
def child?
|
8
|
+
!!parent
|
9
|
+
end
|
10
|
+
|
11
|
+
# Appends child to the parent and returns it
|
12
|
+
#
|
13
|
+
# @return [Model] return the child if it is valid, otherwise returns nil
|
14
|
+
def append_child(source, options={})
|
15
|
+
self.class.has_many_relationships.each do |relation_name, child_class|
|
16
|
+
child_row_model = child_class.new(source, options.reverse_merge(parent: self))
|
17
|
+
if child_row_model.valid?
|
18
|
+
public_send(relation_name) << child_row_model
|
19
|
+
return child_row_model
|
20
|
+
end
|
21
|
+
end
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# Convenience method to return an array of calling `public_send(method_name)` on it's children
|
26
|
+
#
|
27
|
+
# @return [Array] results of `public_send(method_name)` in a flattened array
|
28
|
+
def children_public_send(method_name)
|
29
|
+
self.class.has_many_relationships.keys.map do |relation_name|
|
30
|
+
public_send(relation_name).map(&method_name)
|
31
|
+
end.flatten(1)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Convenience method to return an array of calling `public_send(method_name)` on itself and it's children
|
35
|
+
#
|
36
|
+
# @return [Array] results of `public_send(method_name)` in a flattened array
|
37
|
+
def deep_public_send(method_name)
|
38
|
+
result = [public_send(method_name)]
|
39
|
+
result + children_public_send(method_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
class_methods do
|
43
|
+
# Won't work for Export right now
|
44
|
+
#
|
45
|
+
# @return [Hash] map of `relation_name => CsvRowModel::Import or CsvRowModel::Export class`
|
46
|
+
def has_many_relationships
|
47
|
+
deep_class_var :@_has_many_relationships, {}, :merge
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
def merge_has_many_relationships(relation_hash)
|
52
|
+
@_has_many_relationships ||= {}
|
53
|
+
clear_deep_class_cache(:@_has_many_relationships)
|
54
|
+
@_has_many_relationships.merge! relation_hash
|
55
|
+
end
|
56
|
+
|
57
|
+
# Defines a relationship between a row model (only one relation per model for now).
|
58
|
+
#
|
59
|
+
# @param [Symbol] relation_name the name of the relation
|
60
|
+
# @param [CsvRowModel::Import] row_model_class class of the relation
|
61
|
+
def has_many(relation_name, row_model_class)
|
62
|
+
raise "for now, CsvRowModel's has_many may only be called once" if @_has_many_relationships.present?
|
63
|
+
|
64
|
+
relation_name = relation_name.to_sym
|
65
|
+
|
66
|
+
merge_has_many_relationships(relation_name => row_model_class)
|
67
|
+
|
68
|
+
define_method(relation_name) do
|
69
|
+
#
|
70
|
+
# equal to: @relation_name ||= []
|
71
|
+
#
|
72
|
+
variable_name = "@#{relation_name}"
|
73
|
+
instance_variable_get(variable_name) || instance_variable_set(variable_name, [])
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module CsvRowModel
|
2
|
+
module Model
|
3
|
+
module Columns
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# @return [Hash] a map of `column_name => public_send(column_name)`
|
7
|
+
def attributes
|
8
|
+
self.class.column_names
|
9
|
+
.zip(self.class.column_names.map { |column_name| public_send(column_name) })
|
10
|
+
.to_h
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_json
|
14
|
+
attributes.to_json
|
15
|
+
end
|
16
|
+
|
17
|
+
class_methods do
|
18
|
+
# @return [Array<Symbol>] column names for the row model
|
19
|
+
def column_names
|
20
|
+
columns.keys
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Hash] column names mapped to their options
|
24
|
+
def columns
|
25
|
+
deep_class_var(:@_columns, {}, :merge)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param [Symbol] column_name name of column to find option
|
29
|
+
# @return [Hash] options for the column_name
|
30
|
+
def options(column_name)
|
31
|
+
columns[column_name]
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param [Symbol] column_name name of column to find index
|
35
|
+
# @return [Integer] index of the column_name
|
36
|
+
def index(column_name)
|
37
|
+
column_names.index column_name
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def merge_columns(column_hash)
|
43
|
+
@_columns ||= {}
|
44
|
+
clear_deep_class_cache(:@_columns)
|
45
|
+
@_columns.merge!(column_hash)
|
46
|
+
end
|
47
|
+
|
48
|
+
VALID_OPTIONS_KEYS = %i[type parse validate_type default header header_matchs].freeze
|
49
|
+
|
50
|
+
# Adds column to the row model
|
51
|
+
#
|
52
|
+
# @param [Symbol] column_name name of column to add
|
53
|
+
# @param options [Hash]
|
54
|
+
#
|
55
|
+
# @option options [class] :type class you want to automatically parse to (by default does nothing, equivalent to String)
|
56
|
+
# @option options [Lambda, Proc] :parse for parsing the cell
|
57
|
+
# @option options [Boolean] :validate_type adds a validations within a {::csv_string_model} call.
|
58
|
+
# if true, it will add the default validation for the given :type (if applicable)
|
59
|
+
#
|
60
|
+
# @option options [Object] :default default value of the column if it is blank?, can pass Proc
|
61
|
+
# @option options [String] :header human friendly string of the column name, by default format_header(column_name)
|
62
|
+
# @option options [Hash] :header_matchs array with string to match cell to find in the row, by default column name
|
63
|
+
def column(column_name, options={})
|
64
|
+
extra_keys = options.keys - VALID_OPTIONS_KEYS
|
65
|
+
raise ArgumentError.new("invalid options #{extra_keys}") unless extra_keys.empty?
|
66
|
+
|
67
|
+
merge_columns(column_name.to_sym => options)
|
68
|
+
end
|
69
|
+
# alias_method :row, :column
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module CsvRowModel
|
2
|
+
module Model
|
3
|
+
class CsvStringModel
|
4
|
+
include ActiveWarnings
|
5
|
+
|
6
|
+
def initialize(source)
|
7
|
+
@source = source.symbolize_keys
|
8
|
+
end
|
9
|
+
|
10
|
+
def method_missing(name, *args, &block)
|
11
|
+
return super unless @source.keys.include? name
|
12
|
+
@source[name]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module CsvRowModel
|
2
|
+
module Model
|
3
|
+
module SingleModel
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
|
8
|
+
class << self
|
9
|
+
alias_method :row_names, :column_names
|
10
|
+
alias_method :rows, :columns
|
11
|
+
alias_method :row, :column
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|