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