diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed84544 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.bundle/ +log/*.log +pkg/ +.DS_Store \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..c80ee36 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "http://rubygems.org" + +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..5b5cef9 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,101 @@ +PATH + remote: . + specs: + csv_validator (0.0.1) + activemodel + +GEM + remote: http://rubygems.org/ + specs: + actionpack (3.2.3) + activemodel (= 3.2.3) + activesupport (= 3.2.3) + builder (~> 3.0.0) + erubis (~> 2.7.0) + journey (~> 1.0.1) + rack (~> 1.4.0) + rack-cache (~> 1.2) + rack-test (~> 0.6.1) + sprockets (~> 2.1.2) + activemodel (3.2.3) + activesupport (= 3.2.3) + builder (~> 3.0.0) + activesupport (3.2.3) + i18n (~> 0.6) + multi_json (~> 1.0) + archive-tar-minitar (0.5.2) + builder (3.0.0) + columnize (0.3.6) + diff-lcs (1.1.3) + erubis (2.7.0) + ffi (1.0.11) + guard (1.0.3) + ffi (>= 0.5.0) + thor (>= 0.14.6) + guard-rspec (0.7.3) + guard (>= 0.10.0) + hike (1.2.1) + i18n (0.6.0) + journey (1.0.3) + json (1.7.3) + linecache19 (0.5.12) + ruby_core_source (>= 0.1.4) + multi_json (1.3.2) + rack (1.4.1) + rack-cache (1.2) + rack (>= 0.4) + rack-ssl (1.3.2) + rack + rack-test (0.6.1) + rack (>= 1.0) + railties (3.2.3) + actionpack (= 3.2.3) + activesupport (= 3.2.3) + rack-ssl (~> 1.3.2) + rake (>= 0.8.7) + rdoc (~> 3.4) + thor (~> 0.14.6) + rake (0.9.2.2) + rdoc (3.12) + json (~> 1.4) + rspec (2.9.0) + rspec-core (~> 2.9.0) + rspec-expectations (~> 2.9.0) + rspec-mocks (~> 2.9.0) + rspec-core (2.9.0) + rspec-expectations (2.9.0) + diff-lcs (~> 1.1.3) + rspec-mocks (2.9.0) + rspec-rails (2.9.0) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec (~> 2.9.0) + ruby-debug-base19 (0.11.25) + columnize (>= 0.3.1) + linecache19 (>= 0.5.11) + ruby_core_source (>= 0.1.4) + ruby-debug19 (0.11.6) + columnize (>= 0.3.1) + linecache19 (>= 0.5.11) + ruby-debug-base19 (>= 0.11.19) + ruby_core_source (0.1.5) + archive-tar-minitar (>= 0.5.2) + sprockets (2.1.3) + hike (~> 1.2) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + thor (0.14.6) + tilt (1.3.3) + +PLATFORMS + ruby + +DEPENDENCIES + csv_validator! + guard + guard-rspec + rake + rspec + rspec-rails + ruby-debug19 diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..f6a6882 --- /dev/null +++ b/Guardfile @@ -0,0 +1,7 @@ +guard 'rspec', :cli => "-d --color", :version => 2 do + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } + watch(%r{^lib/csv_validator/csv_validator.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + watch('spec/spec_helper.rb') { "spec" } +end + diff --git a/MIT-LICENSE b/MIT-LICENSE new file mode 100644 index 0000000..406f17b --- /dev/null +++ b/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright 2012 YOURNAME + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.rdoc b/README.rdoc new file mode 100644 index 0000000..93f1582 --- /dev/null +++ b/README.rdoc @@ -0,0 +1,3 @@ += CsvValidator + +This project rocks and uses MIT-LICENSE. \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..83f24b3 --- /dev/null +++ b/Rakefile @@ -0,0 +1,27 @@ +#!/usr/bin/env rake +begin + require 'bundler/setup' +rescue LoadError + puts 'You must `gem install bundler` and `bundle install` to run rake tasks' +end +begin + require 'rdoc/task' +rescue LoadError + require 'rdoc/rdoc' + require 'rake/rdoctask' + RDoc::Task = Rake::RDocTask +end + +RDoc::Task.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'CsvValidator' + rdoc.options << '--line-numbers' + rdoc.rdoc_files.include('README.rdoc') + rdoc.rdoc_files.include('lib/**/*.rb') +end + + + + +Bundler::GemHelper.install_tasks + diff --git a/csv_file_validator.gemspec b/csv_file_validator.gemspec new file mode 100644 index 0000000..8df7985 --- /dev/null +++ b/csv_file_validator.gemspec @@ -0,0 +1,24 @@ +$:.push File.expand_path("../lib", __FILE__) + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |s| + s.name = "csv_validator" + s.version = "0.0.1" + s.authors = ["TODO: Your name"] + s.email = ["TODO: Your email"] + s.homepage = "TODO" + s.summary = "TODO: Summary of CsvValidator." + s.description = "TODO: Description of CsvValidator." + + s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.rdoc"] + + s.require_paths = %w(lib) + s.add_dependency("activemodel", ">= 0") + s.add_development_dependency("rake") + s.add_development_dependency("rspec", ">= 0") + s.add_development_dependency("rspec-rails") + s.add_development_dependency("ruby-debug19") + s.add_development_dependency("guard") + s.add_development_dependency("guard-rspec") + +end diff --git a/lib/csv_validator.rb b/lib/csv_validator.rb new file mode 100644 index 0000000..ab9488b --- /dev/null +++ b/lib/csv_validator.rb @@ -0,0 +1,99 @@ +require 'csv' + +class CsvValidator < ActiveModel::EachValidator + @@default_options = {} + + def self.default_options + @@default_options + end + + def validate_each(record, attribute, value) + options = @@default_options.merge(self.options) + + begin + csv = CSV.read(value) + rescue CSV::MalformedCSVError + record.errors.add(attribute, options[:message] || "is not a valid CSV file") + return + end + + if options[:columns] + unless csv[0].length == options[:columns] + record.errors.add(attribute, options[:message] || "should have #{options[:columns]} columns") + end + end + + if options[:max_columns] + if csv[0].length > options[:max_columns] + record.errors.add(attribute, options[:message] || "should have no more than #{options[:max_columns]} columns") + end + end + + if options[:min_columns] + if csv[0].length < options[:min_columns] + record.errors.add(attribute, options[:message] || "should have at least #{options[:min_columns]} columns") + end + end + + if options[:rows] + unless csv.length == options[:rows] + record.errors.add(attribute, options[:message] || "should have #{options[:rows]} rows") + end + end + + if options[:min_rows] + if csv.length < options[:min_rows] + record.errors.add(attribute, options[:message] || "should have at least #{options[:min_rows]} rows") + end + end + + if options[:max_rows] + if csv.length > options[:max_rows] + record.errors.add(attribute, options[:message] || "should have no more than #{options[:max_rows]} rows") + end + end + + if options[:email] + emails = column_to_array(csv, options[:email]) + invalid_emails = [] + emails.each do |email| + unless email.match /\A[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\z/i + invalid_emails << email + end + end + if invalid_emails.length > 0 + record.errors.add(attribute, options[:message] || "contains invalid emails (#{invalid_emails.join(', ')})") + end + end + + if options[:numericality] + numbers = column_to_array(csv, options[:numericality]) + numbers.each do |number| + unless is_numeric?(number) + record.errors.add(attribute, options[:message] || "contains non-numeric content in column #{options[:numericality]}") + return + end + end + end + + end + + private + + def column_to_array(csv, column_index) + column_contents = [] + csv.each do |column| + column_contents << column[column_index].strip + end + column_contents + end + + def is_numeric?(string) + Float(string) + true + rescue + false + end + + +end diff --git a/spec/csv_validator_spec.rb b/spec/csv_validator_spec.rb new file mode 100644 index 0000000..ae051bc --- /dev/null +++ b/spec/csv_validator_spec.rb @@ -0,0 +1,181 @@ +require 'spec_helper' + +describe CsvValidator do + + describe "general validation" do + + class TestUser1 < TestModel + validates :csv, :csv => true + end + + it "should be valid" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + TestUser1.new(:csv => csv_file).should be_valid + end + + it "should be invalid due to maformed CSV" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/not_csv.png')) + testUser = TestUser1.new(:csv => csv_file) + testUser.should have(1).error_on(:csv) + testUser.error_on(:csv)[0].should eq("is not a valid CSV file") + end + + end + + describe "column count validation" do + + class TestUser2 < TestModel + validates :csv, :csv => {:columns => 3} + end + + class TestUser3 < TestModel + validates :csv, :csv => {:max_columns => 2} + end + + class TestUser4 < TestModel + validates :csv, :csv => {:min_columns => 15} + end + + class TestUser5 < TestModel + validates :csv, :csv => {:min_columns => 2} + end + + it "should be invalid due to exact column count" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/2x6.csv')) + testUser = TestUser2.new(:csv => csv_file) + testUser.should have(1).error_on(:csv) + testUser.error_on(:csv)[0].should eq("should have 3 columns") + end + + it "should be valid with exact column count" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + TestUser2.new(:csv => csv_file).should be_valid + end + + it "should be invalid due to max column count" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + testUser = TestUser3.new(:csv => csv_file) + testUser.should have(1).error_on(:csv) + testUser.error_on(:csv)[0].should eq("should have no more than 2 columns") + end + + it "should be valid with max column count" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/2x6.csv')) + TestUser3.new(:csv => csv_file).should be_valid + end + + it "should be invalid due to min column count" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + testUser = TestUser4.new(:csv => csv_file) + testUser.should have(1).error_on(:csv) + testUser.error_on(:csv)[0].should eq("should have at least 15 columns") + end + + it "should be valid with min column count" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + TestUser5.new(:csv => csv_file).should be_valid + end + + end + + describe "row count validation" do + + class TestUser6 < TestModel + validates :csv, :csv => {:rows => 6} + end + + class TestUser7 < TestModel + validates :csv, :csv => {:min_rows => 10} + end + + class TestUser8 < TestModel + validates :csv, :csv => {:min_rows => 6} + end + + class TestUser9 < TestModel + validates :csv, :csv => {:max_rows => 6} + end + + it "should be valid with exact row count" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + TestUser6.new(:csv => csv_file).should be_valid + end + + it "should be invalid due to exact row count" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x7.csv')) + testUser = TestUser6.new(:csv => csv_file) + testUser.should have(1).error_on(:csv) + testUser.error_on(:csv)[0].should eq("should have 6 rows") + end + + it "should be invalid due to min row count" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + testUser = TestUser7.new(:csv => csv_file) + testUser.should have(1).error_on(:csv) + testUser.error_on(:csv)[0].should eq("should have at least 10 rows") + end + + it "should be valid with min row count" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + TestUser8.new(:csv => csv_file).should be_valid + end + + it "should be invalid due to max row count" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x7.csv')) + testUser = TestUser9.new(:csv => csv_file) + testUser.should have(1).error_on(:csv) + testUser.error_on(:csv)[0].should eq("should have no more than 6 rows") + end + + it "should be valid with max row count" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + TestUser9.new(:csv => csv_file).should be_valid + end + + end + + describe "content validation" do + + class TestUser10 < TestModel + validates :csv, :csv => {:email => 1} + end + + class TestUser11 < TestModel + validates :csv, :csv => {:email => 0} + end + + class TestUser12 < TestModel + validates :csv, :csv => {:numericality => 2} + end + + class TestUser13 < TestModel + validates :csv, :csv => {:numericality => 0} + end + + it "should be invalid with a column specified as containing only emails" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + testUser = TestUser10.new(:csv => csv_file) + testUser.should have(1).error_on(:csv) + testUser.error_on(:csv)[0].should include("contains invalid emails") + end + + it "should be valid with a column specified as containing only emails" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + TestUser11.new(:csv => csv_file).should be_valid + end + + it "should be valid with a column specified as containing only numbers" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + TestUser12.new(:csv => csv_file).should be_valid + end + + it "should be invalid with a column specified as containing only numbers" do + csv_file = File.open(File.join(File.dirname(__FILE__), 'support/3x6.csv')) + testUser = TestUser13.new(:csv => csv_file) + testUser.should have(1).error_on(:csv) + testUser.error_on(:csv)[0].should include("contains non-numeric content in column 0") + end + + end + +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..3f71882 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,21 @@ +require 'rubygems' +require 'rspec' +require 'active_model' +require 'rspec/rails/extensions' + +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$LOAD_PATH.unshift(File.dirname(__FILE__)) + +require 'csv_validator' + +class TestModel + include ActiveModel::Validations + + def initialize(attributes = {}) + @attributes = attributes + end + + def read_attribute_for_validation(key) + @attributes[key] + end +end \ No newline at end of file diff --git a/spec/support/2x6.csv b/spec/support/2x6.csv new file mode 100644 index 0000000..35fda04 --- /dev/null +++ b/spec/support/2x6.csv @@ -0,0 +1,6 @@ +test@email.com, test@email.com +test@email.com, test@email.com +test@email.com, test@email.com +test@email.com, test@email.com +test@email.com, test@email.com +test@email.com, test@email.com \ No newline at end of file diff --git a/spec/support/3x6.csv b/spec/support/3x6.csv new file mode 100644 index 0000000..ce796e1 --- /dev/null +++ b/spec/support/3x6.csv @@ -0,0 +1,6 @@ +test@email.com, test, 101 +test@email.com, test, 101 +test@email.com, test, 101 +test@email.com, test, 101 +test@email.com, test, 101 +test@email.com, test, 101 \ No newline at end of file diff --git a/spec/support/3x7.csv b/spec/support/3x7.csv new file mode 100644 index 0000000..e19abd2 --- /dev/null +++ b/spec/support/3x7.csv @@ -0,0 +1,7 @@ +test@email.com, test@email.com, test@email.com +test@email.com, test@email.com, test@email.com +test@email.com, test@email.com, test@email.com +test@email.com, test@email.com, test@email.com +test@email.com, test@email.com, test@email.com +test@email.com, test@email.com, test@email.com +test@email.com, test@email.com, test@email.com \ No newline at end of file diff --git a/spec/support/not_csv.png b/spec/support/not_csv.png new file mode 100644 index 0000000..ef0c7ed Binary files /dev/null and b/spec/support/not_csv.png differ