csv_row_model 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +10 -0
  7. data/.yardopts +10 -0
  8. data/CONTRIBUTING.md +7 -0
  9. data/Gemfile +20 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +369 -0
  12. data/Rakefile +1 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +7 -0
  15. data/csv_row_model.gemspec +23 -0
  16. data/lib/csv_row_model.rb +46 -0
  17. data/lib/csv_row_model/concerns/deep_class_var.rb +86 -0
  18. data/lib/csv_row_model/concerns/inspect.rb +10 -0
  19. data/lib/csv_row_model/engine.rb +5 -0
  20. data/lib/csv_row_model/export.rb +70 -0
  21. data/lib/csv_row_model/export/csv.rb +43 -0
  22. data/lib/csv_row_model/export/single_model.rb +47 -0
  23. data/lib/csv_row_model/import.rb +125 -0
  24. data/lib/csv_row_model/import/attributes.rb +105 -0
  25. data/lib/csv_row_model/import/csv.rb +136 -0
  26. data/lib/csv_row_model/import/csv/row.rb +17 -0
  27. data/lib/csv_row_model/import/file.rb +133 -0
  28. data/lib/csv_row_model/import/file/callbacks.rb +16 -0
  29. data/lib/csv_row_model/import/file/validations.rb +39 -0
  30. data/lib/csv_row_model/import/presenter.rb +160 -0
  31. data/lib/csv_row_model/import/single_model.rb +41 -0
  32. data/lib/csv_row_model/model.rb +68 -0
  33. data/lib/csv_row_model/model/children.rb +79 -0
  34. data/lib/csv_row_model/model/columns.rb +73 -0
  35. data/lib/csv_row_model/model/csv_string_model.rb +16 -0
  36. data/lib/csv_row_model/model/single_model.rb +16 -0
  37. data/lib/csv_row_model/validators/boolean_format.rb +11 -0
  38. data/lib/csv_row_model/validators/date_format.rb +9 -0
  39. data/lib/csv_row_model/validators/default_change.rb +6 -0
  40. data/lib/csv_row_model/validators/float_format.rb +9 -0
  41. data/lib/csv_row_model/validators/integer_format.rb +9 -0
  42. data/lib/csv_row_model/validators/number_validator.rb +16 -0
  43. data/lib/csv_row_model/validators/validate_attributes.rb +27 -0
  44. data/lib/csv_row_model/version.rb +3 -0
  45. 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