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