csv_model 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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