csv_row_model 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 = ["steve.chung7@gmail.com"]
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"
@@ -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/deep_class_var'
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/single_model'
22
+ require 'csv_row_model/model/file_model'
23
23
 
24
24
  require 'csv_row_model/import'
25
- require 'csv_row_model/import/single_model'
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/csv'
32
- require 'csv_row_model/export/single_model'
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 DeepClassVar
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 deep_class_cache_variable_name(variable_name), nil
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 deep_class_var(variable_name, default_value, merge_method)
48
- deep_class_cache(variable_name) do
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 deep_class_cache_variable_name(variable_name)
63
- "#{variable_name}_deep_class_cache"
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 clear_deep_class_cache(variable_name)
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 deep_class_cache_variable_name
74
+ # Memozies a inherited_class_variable_name
75
75
  # @param variable_name [Symbol] variable_name to cache against
76
- def deep_class_cache(variable_name)
76
+ def class_cache(variable_name)
77
77
  #
78
- # equal to: (has @)variable_name_deep_class_cache ||= Cache.new(klass, variable_name)
78
+ # equal to: (has @)inherited_class_variable_name ||= yield
79
79
  #
80
- cache_variable_name = deep_class_cache_variable_name(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
@@ -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
- # @return [Array] column headers for the row model
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 SingleModel
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
- rows_temaplate.map do |row|
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 rows_temaplate
42
- @rows_temaplate ||= self.class.row_names.map{ |row_name| [row_name]}
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
@@ -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
- @original_attributes ||= begin
24
- values = self.class.column_names.map { |column_name| original_attribute(column_name) }
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` or `Mapper`
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,Mapper] model class returned for importing
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, Mapper] the current row model set by {#next}
19
+ # @return [Input] the current row model set by {#next}
20
20
  attr_reader :current_row_model
21
- # @return [Input, Mapper] the previous row model set by {#next}
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, Mapper] row_model_class model class returned for importing
28
- def initialize(file_path, row_model_class)
29
- @csv, @row_model_class = Csv.new(file_path), row_model_class
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 is_single_model?
45
- return set_end_of_file if end_of_file?
46
- set_single_model(context)
47
- else
48
- next_collection_model(context)
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
- # @return [Boolean] returns true, if the object is at the end of file
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__, context) unless block_given?
61
+ return to_enum(__callee__) unless block_given?
63
62
  return false if _abort?
64
63
 
65
64
  while self.next(context)
66
- return false if _abort?
67
- next if _skip?
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