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