csv_row_model 0.1.0 → 0.1.1

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