csv_row_model 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/.yardopts +10 -0
- data/CONTRIBUTING.md +7 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +369 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/csv_row_model.gemspec +23 -0
- data/lib/csv_row_model.rb +46 -0
- data/lib/csv_row_model/concerns/deep_class_var.rb +86 -0
- data/lib/csv_row_model/concerns/inspect.rb +10 -0
- data/lib/csv_row_model/engine.rb +5 -0
- data/lib/csv_row_model/export.rb +70 -0
- data/lib/csv_row_model/export/csv.rb +43 -0
- data/lib/csv_row_model/export/single_model.rb +47 -0
- data/lib/csv_row_model/import.rb +125 -0
- data/lib/csv_row_model/import/attributes.rb +105 -0
- data/lib/csv_row_model/import/csv.rb +136 -0
- data/lib/csv_row_model/import/csv/row.rb +17 -0
- data/lib/csv_row_model/import/file.rb +133 -0
- data/lib/csv_row_model/import/file/callbacks.rb +16 -0
- data/lib/csv_row_model/import/file/validations.rb +39 -0
- data/lib/csv_row_model/import/presenter.rb +160 -0
- data/lib/csv_row_model/import/single_model.rb +41 -0
- data/lib/csv_row_model/model.rb +68 -0
- data/lib/csv_row_model/model/children.rb +79 -0
- data/lib/csv_row_model/model/columns.rb +73 -0
- data/lib/csv_row_model/model/csv_string_model.rb +16 -0
- data/lib/csv_row_model/model/single_model.rb +16 -0
- data/lib/csv_row_model/validators/boolean_format.rb +11 -0
- data/lib/csv_row_model/validators/date_format.rb +9 -0
- data/lib/csv_row_model/validators/default_change.rb +6 -0
- data/lib/csv_row_model/validators/float_format.rb +9 -0
- data/lib/csv_row_model/validators/integer_format.rb +9 -0
- data/lib/csv_row_model/validators/number_validator.rb +16 -0
- data/lib/csv_row_model/validators/validate_attributes.rb +27 -0
- data/lib/csv_row_model/version.rb +3 -0
- metadata +116 -0
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -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
|
data/bin/setup
ADDED
@@ -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,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
|