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 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "csv_row_model"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'csv_row_model/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "csv_row_model"
8
+ spec.version = CsvRowModel::VERSION
9
+ spec.authors = ["Steve Chung"]
10
+ spec.email = ["steve.chung7@gmail.com"]
11
+
12
+ spec.summary = "Import and export your custom CSVs with a intuitive shared Ruby interface."
13
+ spec.homepage = "https://github.com/s12chung/csv_row_model"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activemodel", "~> 4.2"
22
+ spec.add_dependency "active_warnings", ">= 0.1.2"
23
+ end
@@ -0,0 +1,46 @@
1
+ autoload = false
2
+ # autoload = true #uncomment for testing purposes only, not covered by rspec
3
+
4
+ class Boolean; end unless defined? Boolean
5
+
6
+ require 'active_model'
7
+ require 'active_support/all'
8
+ require 'active_warnings'
9
+ require 'csv'
10
+
11
+ if autoload && defined?(Rails)
12
+ require 'csv_row_model/engine'
13
+ else
14
+ require 'csv_row_model/version'
15
+
16
+ require 'csv_row_model/concerns/inspect'
17
+ require 'csv_row_model/concerns/deep_class_var'
18
+
19
+ require 'csv_row_model/validators/validate_attributes'
20
+
21
+ require 'csv_row_model/model'
22
+ require 'csv_row_model/model/single_model'
23
+
24
+ require 'csv_row_model/import'
25
+ require 'csv_row_model/import/single_model'
26
+ require 'csv_row_model/import/csv'
27
+ require 'csv_row_model/import/file'
28
+
29
+
30
+ require 'csv_row_model/export'
31
+ require 'csv_row_model/export/csv'
32
+ require 'csv_row_model/export/single_model'
33
+ end
34
+
35
+ require 'csv_row_model/validators/default_change'
36
+
37
+ require 'csv_row_model/validators/number_validator'
38
+ require 'csv_row_model/validators/boolean_format'
39
+ require 'csv_row_model/validators/date_format'
40
+ require 'csv_row_model/validators/float_format'
41
+ require 'csv_row_model/validators/integer_format'
42
+
43
+ module CsvRowModel
44
+ class RowModelClassNotDefined < StandardError; end
45
+ class AccessedInvalidAttribute < StandardError; end
46
+ end
@@ -0,0 +1,86 @@
1
+ module CsvRowModel
2
+ module Concerns
3
+ module DeepClassVar
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ # Clears the cache for a variable
8
+ # @param variable_name [Symbol] variable_name to cache against
9
+ def clear_class_cache(variable_name)
10
+ instance_variable_set deep_class_cache_variable_name(variable_name), nil
11
+ end
12
+
13
+ protected
14
+
15
+ # @param included_module [Module] module to search for
16
+ # @return [Array<Module>] inherited_ancestors of included_module (including self)
17
+ def inherited_ancestors(included_module=deep_class_module)
18
+ included_model_index = ancestors.index(included_module)
19
+ included_model_index == 0 ? [included_module] : ancestors[0..(included_model_index - 1)]
20
+ end
21
+
22
+ # @param accessor_method_name [Symbol] method to access the inherited_custom_class
23
+ # @param base_parent_class [Class] class that the custom class inherits from if there's no parent
24
+ # @return [Class] a custom class with the inheritance following self. for example:
25
+ #
26
+ # grandparent -> parent -> self
27
+ #
28
+ # grandparent has inherited_custom_class, but parent, doesn't.
29
+ #
30
+ # then: base_parent_class -> grandparent::inherited_custom_class -> self::inherited_custom_class
31
+ def inherited_custom_class(accessor_method_name, base_parent_class)
32
+ parent_class = inherited_ancestors[1..-1].find do |klass|
33
+ klass.respond_to?(accessor_method_name)
34
+ end.try(accessor_method_name)
35
+ parent_class ||= base_parent_class
36
+
37
+ klass = Class.new(parent_class)
38
+ # how else can i get the current scopes name...
39
+ klass.send(:define_singleton_method, :name, &eval("-> { \"#{name}#{base_parent_class.name.demodulize}\" }"))
40
+ klass
41
+ end
42
+
43
+ # @param variable_name [Symbol] class variable name (recommend :@_variable_name)
44
+ # @param default_value [Object] default value of the class variable
45
+ # @param merge_method [Symbol] method to merge values of the class variable
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
49
+ value = default_value
50
+
51
+ inherited_ancestors.each do |ancestor|
52
+ ancestor_value = ancestor.instance_variable_get(variable_name)
53
+ value = ancestor_value.public_send(merge_method, value) if ancestor_value.present?
54
+ end
55
+
56
+ value
57
+ end
58
+ end
59
+
60
+ # @param variable_name [Symbol] variable_name to cache against
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"
64
+ end
65
+
66
+ # Clears the cache for a variable and the same variable for all it's dependant descendants
67
+ # @param variable_name [Symbol] variable_name to cache against
68
+ def clear_deep_class_cache(variable_name)
69
+ ([self] + descendants).each do |descendant|
70
+ descendant.try(:clear_class_cache, variable_name)
71
+ end
72
+ end
73
+
74
+ # Memozies a deep_class_cache_variable_name
75
+ # @param variable_name [Symbol] variable_name to cache against
76
+ def deep_class_cache(variable_name)
77
+ #
78
+ # equal to: (has @)variable_name_deep_class_cache ||= Cache.new(klass, variable_name)
79
+ #
80
+ cache_variable_name = deep_class_cache_variable_name(variable_name)
81
+ instance_variable_get(cache_variable_name) || instance_variable_set(cache_variable_name, yield)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,10 @@
1
+ module CsvRowModel
2
+ module Concerns
3
+ module Inspect
4
+ def inspect
5
+ s = self.class.send(:inspect_methods).map { |method| "#{method}=#{public_send(method).inspect}" }.join(", ")
6
+ "#<#{self.class.name}:#{object_id} #{s}>"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ module CsvRowModel
2
+ class Engine < ::Rails::Engine
3
+ config.autoload_paths += %W[#{config.root}/lib/]
4
+ end
5
+ end
@@ -0,0 +1,70 @@
1
+ module CsvRowModel
2
+ # Include this to with {Model} to have a RowModel for exporting to CSVs.
3
+ module Export
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attr_reader :source_model, :context
8
+
9
+ self.column_names.each do |column_name|
10
+
11
+ # Safe to override
12
+ #
13
+ #
14
+ # @return [String] a string of public_send(column_name) of the CSV model
15
+ define_method(column_name) do
16
+ source_model.public_send(column_name)
17
+ end
18
+ end
19
+
20
+ validates :source_model, presence: true
21
+ end
22
+
23
+ # @param [Model] source_model object to export to CSV
24
+ # @param [Hash] context
25
+ def initialize(source_model, context={})
26
+ @source_model = source_model
27
+ @context = context
28
+ end
29
+
30
+ def to_rows
31
+ [to_row]
32
+ end
33
+
34
+ # @return [Array] an array of public_send(column_name) of the CSV model
35
+ def to_row
36
+ attributes.values
37
+ end
38
+
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
61
+ end
62
+ 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
+ end
70
+ end
@@ -0,0 +1,43 @@
1
+ require 'csv'
2
+
3
+ module CsvRowModel
4
+ module Export
5
+ class Csv
6
+ attr_reader :export_model_class, :csv, :file
7
+
8
+ # @param [Export] export_model export model class
9
+ def initialize(export_model_class)
10
+ @export_model_class = export_model_class
11
+ @file = Tempfile.new("#{export_model_class}.csv")
12
+ end
13
+
14
+ def header
15
+ export_model_class.column_headers
16
+ end
17
+
18
+ def append_header
19
+ csv << header
20
+ end
21
+
22
+ def append_model(model, context={})
23
+ export_model_class.new(model, context).to_rows.each do |row|
24
+ csv << row
25
+ end
26
+ end
27
+
28
+ def generate(with_header: true)
29
+ CSV.open(file.path,"wb") do |csv|
30
+ @csv = csv
31
+ append_header if with_header && !export_model_class.single_model?
32
+ yield self
33
+ end
34
+ ensure
35
+ @csv = nil
36
+ end
37
+
38
+ def to_s
39
+ file.read
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,47 @@
1
+ module CsvRowModel
2
+ module Export
3
+ module SingleModel
4
+ extend ActiveSupport::Concern
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
+ # @return [Array] an array of rows, where if cell is row_name, it's parsed into the header_match
19
+ # and everything else is return as is.
20
+ def to_rows
21
+ rows_temaplate.map do |row|
22
+ result = []
23
+ row.each do |cell|
24
+ if is_row_name? cell
25
+ header_matchs = self.class.options(cell)[:header_matchs]
26
+ result << "#{header_matchs ? header_matchs.first : self.class.format_header(cell)}"
27
+ result << "#{attributes[cell]}"
28
+ else
29
+ result << cell.to_s
30
+ end
31
+ end
32
+ result
33
+ end
34
+ end
35
+
36
+ # Safe to override
37
+ #
38
+ # @return [Array<Array>] an array of arrays, where every represents a row and every row
39
+ # can have strings and row_name (column_name). By default,
40
+ # returns a row_name for every row
41
+ def rows_temaplate
42
+ @rows_temaplate ||= self.class.row_names.map{ |row_name| [row_name]}
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,125 @@
1
+ require 'csv_row_model/import/attributes'
2
+ require 'csv_row_model/import/presenter'
3
+
4
+ module CsvRowModel
5
+ # Include this to with {Model} to have a RowModel for importing csvs.
6
+ module Import
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include Concerns::Inspect
11
+ include Attributes
12
+
13
+ attr_reader :attr_reader, :source_header, :source_row, :context, :index, :previous
14
+
15
+ self.column_names.each { |*args| define_attribute_method(*args) }
16
+
17
+ validates :source_row, presence: true
18
+ end
19
+
20
+ # @param [Array] source_row the csv row
21
+ # @param options [Hash]
22
+ # @option options [Integer] :index index in the CSV file
23
+ # @option options [Hash] :context extra data you want to work with the model
24
+ # @option options [Array] :source_header the csv header row
25
+ # @option options [CsvRowModel::Import] :previous the previous row model
26
+ # @option options [CsvRowModel::Import] :parent if the instance is a child, pass the parent
27
+ def initialize(source_row, options={})
28
+ options = options.symbolize_keys.reverse_merge(context: {})
29
+ @source_row, @context = source_row, OpenStruct.new(options[:context])
30
+ @index, @source_header, @previous = options[:index], options[:source_header], options[:previous].try(:dup)
31
+
32
+ previous.try(:free_previous)
33
+ super(source_row, options)
34
+ end
35
+
36
+ # @return [Hash] a map of `column_name => source_row[index_of_column_name]`
37
+ def mapped_row
38
+ return {} unless source_row
39
+ @mapped_row ||= self.class.column_names.zip(source_row).to_h
40
+ end
41
+
42
+ # Free `previous` from memory to avoid making a linked list
43
+ def free_previous
44
+ @previous = nil
45
+ end
46
+
47
+ # @return [Presenter] the presenter of self
48
+ def presenter
49
+ @presenter ||= self.class.presenter_class.new(self)
50
+ end
51
+
52
+ # @return [Model::CsvStringModel] a model with validations related to Model::csv_string_model (values are from format_cell)
53
+ def csv_string_model
54
+ @csv_string_model ||= begin
55
+ if source_row
56
+ column_names = self.class.column_names
57
+ hash = column_names.zip(column_names.map.with_index do |column_name, index|
58
+ self.class.format_cell(source_row[index], column_name, index)
59
+ end).to_h
60
+ else
61
+ hash = {}
62
+ end
63
+ self.class.csv_string_model_class.new(hash)
64
+ end
65
+ end
66
+
67
+ # Safe to override.
68
+ #
69
+ # @return [Boolean] returns true, if this instance should be skipped
70
+ def skip?
71
+ !valid? || presenter.skip?
72
+ end
73
+
74
+ # Safe to override.
75
+ #
76
+ # @return [Boolean] returns true, if the entire csv file should stop reading
77
+ def abort?
78
+ presenter.abort?
79
+ end
80
+
81
+ def valid?(*args)
82
+ super
83
+
84
+ proc = -> do
85
+ csv_string_model.valid?(*args)
86
+ errors.messages.merge!(csv_string_model.errors.messages.reject {|k, v| v.empty? })
87
+ errors.empty?
88
+ end
89
+
90
+ if using_warnings?
91
+ csv_string_model.using_warnings &proc
92
+ else
93
+ proc.call
94
+ end
95
+ end
96
+
97
+ class_methods do
98
+ # by default import model is a collection model
99
+ def type
100
+ :collection_model
101
+ end
102
+
103
+ # See {Model#column}
104
+ def column(column_name, options={})
105
+ super
106
+ define_attribute_method(column_name)
107
+ end
108
+
109
+ # @return [Class] the Class of the Presenter
110
+ def presenter_class
111
+ @presenter_class ||= inherited_custom_class(:presenter_class, Presenter)
112
+ end
113
+
114
+ protected
115
+ def inspect_methods
116
+ @inspect_methods ||= %i[mapped_row initialized_at parent context previous].freeze
117
+ end
118
+
119
+ # Call to define the presenter
120
+ def presenter(&block)
121
+ presenter_class.class_eval &block
122
+ end
123
+ end
124
+ end
125
+ end