csv_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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 670a14423c708636825d5a2cebed117a0ac186e2
4
+ data.tar.gz: 7c39766c75b1b629c743036a5791da36c93032c9
5
+ SHA512:
6
+ metadata.gz: 8fb5608b94c655f60c36c89b469f651ddd4a725a64fa5359b6534ec4cc37197cb80387e3bc213d40cb09b49569a0417ebfa63c60ccf4e81e54d2db41d71f46e4
7
+ data.tar.gz: 03c07a434abc2fec5416f62cff6453a4bffd2758f017e5c91cadb35daeca408adf26c03b1592e721ee6051c9be864f74f04e6d05c2c062a8a28bfffada80bc54
data/.gitignore ADDED
@@ -0,0 +1,34 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Specific to RubyMotion:
13
+ .dat*
14
+ .repl_history
15
+ build/
16
+
17
+ ## Documentation cache and generated files:
18
+ /.yardoc/
19
+ /_yardoc/
20
+ /doc/
21
+ /rdoc/
22
+
23
+ ## Environment normalisation:
24
+ /.bundle/
25
+ /lib/bundler/man/
26
+
27
+ # for a library or gem, you might want to ignore these files since the code is
28
+ # intended to run in multiple environments; otherwise, check them in:
29
+ # Gemfile.lock
30
+ # .ruby-version
31
+ # .ruby-gemset
32
+
33
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
34
+ .rvmrc
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ rvm:
2
+ - 2.0.0
3
+ - 2.1.1
4
+
5
+ install:
6
+ - "travis_retry bundle install"
7
+
8
+ script: "bundle exec rake"
9
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,31 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ csv_model (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.2.5)
10
+ rake (10.3.1)
11
+ rspec (3.0.0)
12
+ rspec-core (~> 3.0.0)
13
+ rspec-expectations (~> 3.0.0)
14
+ rspec-mocks (~> 3.0.0)
15
+ rspec-core (3.0.2)
16
+ rspec-support (~> 3.0.0)
17
+ rspec-expectations (3.0.2)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.0.0)
20
+ rspec-mocks (3.0.2)
21
+ rspec-support (~> 3.0.0)
22
+ rspec-support (3.0.2)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ bundler (~> 1.6)
29
+ csv_model!
30
+ rake
31
+ rspec (= 3.0.0)
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Scrimmage
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,6 @@
1
+ csv_model
2
+ =========
3
+
4
+ [![Build Status](https://travis-ci.org/Scrimmage/csv_model.svg)](https://travis-ci.org/Scrimmage/csv_model)
5
+
6
+ [![Code Climate](https://codeclimate.com/github/Scrimmage/csv_model.png)](https://codeclimate.com/github/Scrimmage/csv_model)
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'rspec/core/rake_task'
4
+
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ desc 'Default: run the specs.'
8
+ task :default do
9
+ system("bundle exec rake -s spec:unit;")
10
+ end
11
+
12
+ namespace :spec do
13
+ desc "Run unit specs"
14
+ RSpec::Core::RakeTask.new('unit') do |t|
15
+ t.pattern = 'spec/**/*_spec.rb}'
16
+ end
17
+ end
18
+
19
+ desc "Run the unit and acceptance specs"
20
+ task :spec => ['spec:unit']
data/csv_model.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'csv_model/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "csv_model"
8
+ spec.version = CSVModel::VERSION
9
+ spec.authors = ["Matthew Chadwick"]
10
+ spec.email = ["matthew.chadwick@gmail.com"]
11
+ spec.summary = %q{Utility library for importing CSV data.}
12
+ spec.description = %q{}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec", "3.0.0"
24
+ end
@@ -0,0 +1,27 @@
1
+ using CSVModel::Extensions
2
+
3
+ module CSVModel
4
+ class Column
5
+ include Utilities::Options
6
+
7
+ attr_reader :name
8
+
9
+ def initialize(name, options = {})
10
+ @name = name
11
+ @options = options
12
+ end
13
+
14
+ def is_primary_key?
15
+ option(:primary_key, false)
16
+ end
17
+
18
+ def key
19
+ name.to_column_key
20
+ end
21
+
22
+ def model_attribute
23
+ key.underscore.to_sym
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ module CSVModel
2
+
3
+ class ParseError < RuntimeError; end
4
+
5
+ end
@@ -0,0 +1,45 @@
1
+ module CSVModel
2
+ module Extensions
3
+
4
+ refine Array do
5
+ def all_values_blank?
6
+ return true if empty?
7
+ each { |value| return false if !value.nil? && value.try(:strip) != "" }
8
+ true
9
+ end
10
+ end
11
+
12
+ refine NilClass do
13
+ def try(*args)
14
+ nil
15
+ end
16
+ end
17
+
18
+ refine Object do
19
+ def try(*a, &b)
20
+ if a.empty? && block_given?
21
+ yield self
22
+ else
23
+ public_send(*a, &b) if respond_to?(a.first)
24
+ end
25
+ end
26
+ end
27
+
28
+ refine String do
29
+ def to_column_key
30
+ downcase.strip
31
+ end
32
+
33
+ def underscore
34
+ tr(' ', '_').tr("-", "_")
35
+ end
36
+ end
37
+
38
+ refine Symbol do
39
+ def to_column_key
40
+ to_s.downcase.strip
41
+ end
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,133 @@
1
+ using CSVModel::Extensions
2
+
3
+ module CSVModel
4
+ class HeaderRow
5
+ include Utilities::Options
6
+
7
+ attr_reader :data
8
+
9
+ def initialize(data, options = {})
10
+ @data = data
11
+ @options = options
12
+ end
13
+
14
+ def columns
15
+ @columns ||= data.collect { |x| Column.new(x) }
16
+ end
17
+
18
+ def column_count
19
+ columns.count
20
+ end
21
+
22
+ def column_index(key)
23
+ column_keys.index(key.to_column_key)
24
+ end
25
+
26
+ def errors
27
+ duplicate_column_errors + illegal_column_errors + missing_column_errors
28
+ end
29
+
30
+ # TODO: Remove? Not currently used.
31
+ def has_column?(key)
32
+ !column_index(key).nil?
33
+ end
34
+
35
+ def primary_key_columns
36
+ @primary_key_columns ||= columns.select { |x| primary_key_column_keys.include?(x.key) }
37
+ end
38
+
39
+ def valid?
40
+ has_required_columns? && !has_duplicate_columns? && !has_illegal_columns?
41
+ end
42
+
43
+ protected
44
+
45
+ def column_keys
46
+ @column_keys ||= columns.collect { |x| x.key }
47
+ end
48
+
49
+ def column_map
50
+ @column_map ||= Hash[columns.collect { |x| [x.key, x] }]
51
+ end
52
+
53
+ def column_name(column_key)
54
+ data.find { |entry| entry.to_column_key == column_key }
55
+ end
56
+
57
+ def duplicate_column_errors
58
+ duplicate_column_names.collect { |name| "Multiple columns found for #{name}, column headings must be unique" }
59
+ end
60
+
61
+ def duplicate_column_names
62
+ data.collect { |x| x.to_column_key }
63
+ .inject(Hash.new(0)) { |counts, key| counts[key] += 1; counts }
64
+ .select { |key, count| count > 1 }
65
+ .collect { |key, count| column_name(key) }
66
+ end
67
+
68
+ def has_duplicate_columns?
69
+ data.count != column_map.keys.count
70
+ end
71
+
72
+ def has_illegal_columns?
73
+ illegal_column_keys.any?
74
+ end
75
+
76
+ def has_required_columns?
77
+ missing_column_keys.empty?
78
+ end
79
+
80
+ def illegal_column_errors
81
+ illegal_column_names.collect { |name| "Unknown column #{name}" }
82
+ end
83
+
84
+ def illegal_column_keys
85
+ legal_column_names.any? ? column_keys - legal_column_keys : []
86
+ end
87
+
88
+ def illegal_column_names
89
+ illegal_column_keys.collect { |key| column_name(key) }
90
+ end
91
+
92
+ def legal_column_names
93
+ option(:legal_columns, [])
94
+ end
95
+
96
+ def legal_column_keys
97
+ legal_column_names.collect { |x| x.to_column_key }
98
+ end
99
+
100
+ def missing_column_errors
101
+ missing_column_names.collect { |name| "Missing column #{name}" }
102
+ end
103
+
104
+ def missing_column_keys
105
+ required_column_keys - column_keys
106
+ end
107
+
108
+ def missing_column_names
109
+ missing_column_keys.collect { |key| required_column_name(key) }
110
+ end
111
+
112
+ def primary_key_column_keys
113
+ primary_key_column_names.collect { |x| x.to_column_key }
114
+ end
115
+
116
+ def primary_key_column_names
117
+ option(:primary_key, [])
118
+ end
119
+
120
+ def required_column_keys
121
+ required_column_names.collect { |x| x.to_column_key }
122
+ end
123
+
124
+ def required_column_name(column_key)
125
+ required_column_names.find { |entry| entry.to_column_key == column_key }
126
+ end
127
+
128
+ def required_column_names
129
+ option(:required_columns, [])
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,96 @@
1
+ using CSVModel::Extensions
2
+
3
+ module CSVModel
4
+ class Model
5
+ include Utilities::Options
6
+
7
+ attr_reader :data, :header, :keys, :parse_error, :rows
8
+
9
+ def initialize(data, options = {})
10
+ @data = data
11
+ @rows = []
12
+ @options = options
13
+ @keys = Set.new
14
+ end
15
+
16
+ def row_count
17
+ rows.count
18
+ end
19
+
20
+ def structure_errors
21
+ return [parse_error] if parse_error
22
+ return header.errors if !header.valid?
23
+ []
24
+ end
25
+
26
+ def structure_valid?
27
+ parse_error.nil? && header.valid?
28
+ end
29
+
30
+ instance_methods(false).each do |method_name|
31
+ method = instance_method(method_name)
32
+ define_method(method_name) do |*args, &block|
33
+ parse_data
34
+ method.bind(self).(*args, &block)
35
+ end
36
+ end
37
+
38
+ protected
39
+
40
+ def create_header_row(row)
41
+ header_class.new(row, options)
42
+ end
43
+
44
+ def create_row(row)
45
+ row = row_class.new(header, row, options)
46
+ row.mark_as_duplicate if is_duplicate_key?(row.key)
47
+ row
48
+ end
49
+
50
+ def header_class
51
+ option(:header_class, HeaderRow)
52
+ end
53
+
54
+ def is_duplicate_key?(value)
55
+ return false if value.nil? || value == "" || (value.is_a?(Array) && value.all_values_blank?)
56
+ !keys.add?(value)
57
+ end
58
+
59
+ def parse_data
60
+ return if @parsed
61
+ @parsed = true
62
+
63
+ begin
64
+ CSV.parse(data, { col_sep: "\t" }).each_with_index do |row, index|
65
+ if index == 0
66
+ @header = create_header_row(row)
67
+ end
68
+
69
+ if row.size != header.column_count
70
+ raise ParseError.new("Each row should have exactly #{header.column_count} columns. Error on row #{index + 1}.")
71
+ end
72
+
73
+ # TODO: Should we keep this?
74
+ # if index > (limit - 1)
75
+ # raise ParseError.new("You can only import #{limit} records at a time. Please split your import into multiple parts.")
76
+ # end
77
+
78
+ if index > 0
79
+ @rows << create_row(row)
80
+ end
81
+ end
82
+ rescue CSV::MalformedCSVError => e
83
+ @parse_error = "The data could not be parsed. Please check for formatting errors: #{e.message}"
84
+ rescue ParseError => e
85
+ @parse_error = e.message
86
+ rescue Exception => e
87
+ @parse_error = "An unexpected error occurred. Please try again or contact support if the issue persists: #{e.message}"
88
+ end
89
+ end
90
+
91
+ def row_class
92
+ option(:row_class, Row)
93
+ end
94
+
95
+ end
96
+ end
@@ -0,0 +1,112 @@
1
+ using CSVModel::Extensions
2
+
3
+ module CSVModel
4
+ class ObjectWithStatusSnapshot < SimpleDelegator
5
+ include RecordStatus
6
+
7
+ def assign_attributes(attributes)
8
+ __getobj__.try(:assign_attributes, attributes)
9
+ end
10
+
11
+ def errors
12
+ if __getobj__.nil?
13
+ ["Record could not be created or updated"]
14
+ else
15
+ value = __getobj__.errors
16
+ value.try(:full_messages) || value
17
+ end
18
+ end
19
+
20
+ def mark_as_duplicate
21
+ @is_duplicate = true
22
+ end
23
+
24
+ def save(options = {})
25
+ capture_state(options[:dry_run])
26
+ @was_saved = was_editable? && was_valid? && (is_dry_run? || save_or_destroy)
27
+ end
28
+
29
+ def status
30
+ return ERROR_ON_READ if __getobj__.nil?
31
+ return DUPLICATE if is_dry_run? && is_duplicate?
32
+ return status_for_new_record if was_new?
33
+ return status_for_existing_record if was_existing?
34
+ UNKNOWN
35
+ end
36
+
37
+ def valid?
38
+ __getobj__.try(:valid?)
39
+ end
40
+
41
+ private
42
+
43
+ def capture_state(dry_run)
44
+ @is_dry_run = dry_run == true
45
+ if !__getobj__.nil?
46
+ @was_changed = changed?
47
+ @was_deleted = marked_for_destruction?
48
+ @was_editable = __getobj__.respond_to?(:editable?) ? __getobj__.editable? : true
49
+ @was_new = new_record?
50
+ @was_valid = valid?
51
+ end
52
+ end
53
+
54
+ def is_duplicate?
55
+ @is_duplicate
56
+ end
57
+
58
+ def is_dry_run?
59
+ @is_dry_run
60
+ end
61
+
62
+ def save_or_destroy
63
+ marked_for_destruction? ? __getobj__.destroy : __getobj__.save
64
+ end
65
+
66
+ def status_for_existing_record
67
+ return DELETE if was_deleted?
68
+ return NOT_CHANGED if !was_changed?
69
+ return UPDATE if was_valid? && was_saved?
70
+ ERROR_ON_UPDATE # if (!was_editable? || !was_valid? || !was_saved?)
71
+ end
72
+
73
+ def status_for_new_record
74
+ return ERROR_ON_DELETE if was_deleted?
75
+ return ERROR_ON_CREATE if was_not_valid?
76
+ CREATE # if valid?
77
+ end
78
+
79
+ def was_changed?
80
+ @was_changed
81
+ end
82
+
83
+ def was_deleted?
84
+ @was_deleted
85
+ end
86
+
87
+ def was_editable?
88
+ @was_editable
89
+ end
90
+
91
+ def was_existing?
92
+ !was_new?
93
+ end
94
+
95
+ def was_new?
96
+ @was_new
97
+ end
98
+
99
+ def was_saved?
100
+ @was_saved
101
+ end
102
+
103
+ def was_not_valid?
104
+ !was_valid?
105
+ end
106
+
107
+ def was_valid?
108
+ @was_valid
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,16 @@
1
+ module CSVModel
2
+ module RecordStatus
3
+
4
+ UNKNOWN = 0
5
+ ERROR_ON_READ = 1
6
+ NOT_CHANGED = 2
7
+ DUPLICATE = 3
8
+ CREATE = 4
9
+ DELETE = 5
10
+ UPDATE = 6
11
+ ERROR_ON_CREATE = 7
12
+ ERROR_ON_DELETE = 8
13
+ ERROR_ON_UPDATE = 9
14
+
15
+ end
16
+ end