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