├── Gemfile ├── lib ├── active_validators │ ├── active_model │ │ └── validations │ │ │ ├── date_validator.rb │ │ │ ├── hex_color_validator.rb │ │ │ ├── slug_validator.rb │ │ │ ├── regexp_validator.rb │ │ │ ├── respond_to_validator.rb │ │ │ ├── shared │ │ │ └── luhn_checker.rb │ │ │ ├── password_validator.rb │ │ │ ├── phone_validator.rb │ │ │ ├── siren_validator.rb │ │ │ ├── ip_validator.rb │ │ │ ├── twitter_validator.rb │ │ │ ├── barcode_validator.rb │ │ │ ├── email_validator.rb │ │ │ ├── credit_card_validator.rb │ │ │ ├── ssn_validator.rb │ │ │ ├── nino_validator.rb │ │ │ ├── tracking_number_validator.rb │ │ │ ├── sin_validator.rb │ │ │ ├── postal_code_validator.rb │ │ │ └── url_validator.rb │ └── one_nine_shims │ │ └── one_nine_string.rb └── activevalidators.rb ├── .gitignore ├── checksums ├── 3.1.2.sha512 ├── 3.2.0.sha512 ├── 4.0.0.sha512 ├── 4.0.1.sha512 ├── 4.0.3.sha512 └── 4.1.0.sha512 ├── script └── generate_authors ├── AUTHORS.md ├── test ├── test_helper.rb └── validations │ ├── date_test.rb │ ├── regexp_test.rb │ ├── respond_to_test.rb │ ├── slug_test.rb │ ├── hex_color_test.rb │ ├── password_test.rb │ ├── siren_test.rb │ ├── barcode_test.rb │ ├── ssn_test.rb │ ├── ip_test.rb │ ├── phone_test.rb │ ├── sin_test.rb │ ├── credit_card_test.rb │ ├── postal_code_test.rb │ ├── nino_test.rb │ ├── url_test.rb │ ├── email_test.rb │ ├── tracking_number_test.rb │ └── twitter_test.rb ├── LICENSE ├── activevalidators.gemspec ├── Rakefile ├── certs └── franckverrot.pem ├── .circleci └── config.yml ├── README.md └── ChangeLog.md /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/date_validator.rb: -------------------------------------------------------------------------------- 1 | require 'date_validator' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.gem 3 | *.rbc 4 | Gemfile.lock 5 | .ruby-gemset 6 | .ruby-version 7 | .idea 8 | -------------------------------------------------------------------------------- /checksums/3.1.2.sha512: -------------------------------------------------------------------------------- 1 | 4faf052c6018bd3f17ab23c2cc6f5e2e7c9beaadd3fbb85652fc70cac785b1a6886ffa1a0d0c164607acf5bbf43881f7a17b75a3eba5647784d7c67c274574c4 -------------------------------------------------------------------------------- /checksums/3.2.0.sha512: -------------------------------------------------------------------------------- 1 | 282e0d989bc5987b9ec98124ffeca9be5708389674a6f7bea23050f4bf1451054cb49d705e63f4a7e0d2654df14bdfd737d39c9653c74d3685777a525268cb49 -------------------------------------------------------------------------------- /checksums/4.0.0.sha512: -------------------------------------------------------------------------------- 1 | 98b18c6ed30a7993b5701f3258a482ea2aea8b57238721d304d7517481b7bf168ebd51964072edc58c26896301b9eb23a5034c60601ac51d6365224b41209450 -------------------------------------------------------------------------------- /checksums/4.0.1.sha512: -------------------------------------------------------------------------------- 1 | 0fd70ceed9eaca16b22509a99454d893b84ad2d253bb196e6ca79639aba9f6662c682171d38e4de940983b37eb8eb9f68a54d4ab6fc31c625dffa2b67f6f74e2 -------------------------------------------------------------------------------- /checksums/4.0.3.sha512: -------------------------------------------------------------------------------- 1 | 5208e1aa894b00822300785f163943cd31b2e2edb519498b590f7c86fd4777a21f6add301c64abe84557115fb881d0823682856a068d7e66c5fd337b9cdac86c -------------------------------------------------------------------------------- /checksums/4.1.0.sha512: -------------------------------------------------------------------------------- 1 | 739d5f9cd19c8e5a492e2e530504d04aa017109f359881594ed02c9761d74deab2e2366954ca3d6522737f1636e6029d95819f2c495da5b742e241f695294af2 -------------------------------------------------------------------------------- /script/generate_authors: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cat << EOF > AUTHORS.md 4 | # Authors 5 | 6 | In alphabetical order: 7 | 8 | EOF 9 | 10 | git log --pretty=format:" * %an" | sort -u >> AUTHORS.md 11 | -------------------------------------------------------------------------------- /lib/active_validators/one_nine_shims/one_nine_string.rb: -------------------------------------------------------------------------------- 1 | module ActiveValidators 2 | module OneNineShims 3 | class OneNineString < String 4 | def gsub(pattern, hash) 5 | super(pattern) do |m| 6 | hash[m] 7 | end 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/hex_color_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | class HexColorValidator < EachValidator 4 | def validate_each(record, attribute, value) 5 | unless value =~ /\A(\h{3}){,2}\z/ 6 | record.errors.add(attribute) 7 | end 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/slug_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | class SlugValidator < EachValidator 4 | def validate_each(record, attribute, value) 5 | if value.nil? 6 | record.errors.add(attribute, :blank) 7 | elsif value != value.parameterize 8 | record.errors.add(attribute) 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/regexp_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | class RegexpValidator < EachValidator 4 | def validate_each(record, attribute, value) 5 | unless valid_regexp?(value) 6 | record.errors.add(attribute) 7 | end 8 | end 9 | 10 | 11 | private 12 | 13 | def valid_regexp?(value) 14 | Regexp.compile(value.to_s) 15 | true 16 | 17 | rescue RegexpError 18 | false 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/respond_to_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | class RespondToValidator < EachValidator 4 | RESERVED_OPTIONS = [:if, :unless] 5 | def validate_each(record,attribute,value) 6 | responders = options.dup 7 | RESERVED_OPTIONS.each do |opt,should_apply| responders.delete(opt) end 8 | responders.each do |method,dummy| 9 | record.errors.add(attribute) unless value.respond_to? method 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/shared/luhn_checker.rb: -------------------------------------------------------------------------------- 1 | module LuhnChecker 2 | # This method implements Luhn algorythm. 3 | # Details about it's work may be founded here: http://en.wikipedia.org/wiki/Luhn_Algorithm 4 | def self.valid?(s) 5 | value = s.gsub(/\D/, '').reverse 6 | 7 | sum = i = 0 8 | 9 | value.each_char do |ch| 10 | n = ch.to_i 11 | 12 | n *= 2 if i.odd? 13 | 14 | n = 1 + (n - 10) if n >= 10 15 | 16 | sum += n 17 | i += 1 18 | end 19 | 20 | (sum % 10).zero? 21 | end 22 | end -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | In alphabetical order: 4 | 5 | * Abe Fehr 6 | * Alyssa Ross 7 | * Brian Moseley 8 | * Cédric FABIANSKI 9 | * Etienne Depaulis 10 | * Franck Verrot 11 | * Garrett Bjerkhoel 12 | * Guten 13 | * Igor Fedoronchuk 14 | * John 15 | * Julien Fusco 16 | * Kevin Sołtysiak 17 | * Kris Windham 18 | * Manuel Menezes de Sequeira 19 | * Oriol Gual 20 | * Paco Guzmán 21 | * Pierre-Baptiste Béchu 22 | * Radovan Šmitala 23 | * Ray Krueger 24 | * Renato Riccieri 25 | * Richard Michael 26 | * Rob Zuber 27 | * Serj L 28 | * Stefan Kracht 29 | * William Howard 30 | * Zachary Porter 31 | * daslicious 32 | * dewski 33 | * lukas -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | gem 'minitest' 2 | require 'minitest/autorun' 3 | require 'minitest/mock' 4 | old_w, $-w = $-w, false 5 | require 'active_support/test_case' 6 | $-w = old_w 7 | require 'activevalidators' 8 | 9 | class TestRecord 10 | include ActiveModel::Validations 11 | attr_accessor :ip, :url, :slug, :responder, :global_condition, 12 | :local_condition, :phone, :email, :card, :password, :twitter_username, 13 | :postal_code, :carrier, :tracking_number, :start_date, :end_date, :siren, :ssn, :sin, :nino, :barcode, 14 | :text_color, :redirect_rule 15 | 16 | def initialize(attrs = {}) 17 | attrs.each_pair { |k,v| send("#{k}=", v) } 18 | end 19 | end 20 | 21 | class Minitest::Spec 22 | include ActiveSupport::Testing::Deprecation 23 | end 24 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/password_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | class PasswordValidator < EachValidator 4 | REGEXES = { 5 | :weak => /(?=.{6,}).*/, # 6 characters 6 | :medium => /^(?=.{7,})(((?=.*[A-Z])(?=.*[a-z]))|((?=.*[A-Z])(?=.*[0-9]))|((?=.*[a-z])(?=.*[0-9]))).*$/, #len=7 chars and numbers 7 | :strong => /^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[\d\W]).*$/#len=8 chars and numbers and special chars 8 | } 9 | 10 | def validate_each(record, attribute, value) 11 | required_strength = options.fetch(:strength, :weak) 12 | if (REGEXES[required_strength] !~ value) 13 | record.errors.add(attribute) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/validations/date_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'active_support/core_ext/hash/reverse_merge' 3 | ActiveValidators.activate(:date) 4 | 5 | # ActiveValidators relies on another gem called "date_validator" edited 6 | # by the fine folks at Codegram. 7 | # 8 | # If you wanna see tests for this gem, go take a look at the repository 9 | # [on Github](https://github.com/codegram/date_validator) 10 | describe "Date Validation" do 11 | it "finds the translations" do 12 | d = build_date_record 13 | 14 | refute d.valid? 15 | refute_includes d.errors.to_s, 'translation missing' 16 | end 17 | 18 | def build_date_record 19 | TestRecord.reset_callbacks(:validate) 20 | TestRecord.validates :start_date, :date => { :before => :end_date } 21 | TestRecord.new(:start_date => Time.now, :end_date => Time.now - 1) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/validations/regexp_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:regexp) 3 | 4 | describe "Regexp Validation" do 5 | let(:invalid_message) { subject.errors.generate_message(:redirect_rule, :invalid) } 6 | 7 | subject { TestRecord.new } 8 | 9 | before do 10 | TestRecord.reset_callbacks(:validate) 11 | TestRecord.validates :redirect_rule, :regexp => true 12 | end 13 | 14 | it "accepts blank value" do 15 | subject.redirect_rule = '' 16 | 17 | _(subject).must_be(:valid?) 18 | _(subject.errors).must_be(:empty?) 19 | end 20 | 21 | it "rejects malformed regular expressions" do 22 | subject.redirect_rule = '[' 23 | 24 | _(subject).must_be(:invalid?) 25 | _(subject.errors[:redirect_rule]).must_include(invalid_message) 26 | end 27 | 28 | it "allow proper regular expressions" do 29 | subject.redirect_rule = '^/vanity-url(-2014)?' 30 | 31 | _(subject).must_be(:valid?) 32 | _(subject.errors).must_be(:empty?) 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/phone_validator.rb: -------------------------------------------------------------------------------- 1 | require 'phony' 2 | require 'countries' 3 | module ActiveModel 4 | module Validations 5 | class PhoneValidator < EachValidator 6 | def validate_each(record, attribute, value) 7 | plausible = case (country = options[:country]) 8 | when ->(s) { s.blank? } 9 | # Without a country, try to figure out if it sounds like 10 | # a plausible phone number. 11 | Phony.plausible?(value) 12 | else 13 | # In the presence of the country option, provide Phony the country 14 | # code associated with it. 15 | country_code = ISO3166::Country.new(country.to_s.upcase).country_code 16 | Phony.plausible?(value, :cc => country_code) 17 | end 18 | 19 | if !plausible 20 | record.errors.add(attribute, :invalid) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/siren_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | class SirenValidator < EachValidator 4 | # Validates siren format according to 5 | # http://fr.wikipedia.org/wiki/SIREN 6 | def valid_siren?(input) 7 | str = input.to_s 8 | reversed_array = str.split('').reverse 9 | 10 | digits = reversed_array.each_with_index.map do |char, i| 11 | coeff = (i % 2) + 1 12 | (char.to_i * coeff).to_s.split('') 13 | end 14 | 15 | sum = digits.flatten.map(&:to_i).inject(:+) 16 | 17 | (sum % 10) == 0 18 | end 19 | 20 | def validate_each(record, attribute, value) 21 | if value.nil? 22 | record.errors.add(attribute, :blank) 23 | else 24 | if value.to_s.length != 9 25 | record.errors.add attribute, :length 26 | else 27 | record.errors.add attribute, :format unless valid_siren?(value) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Franck Verrot 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/activevalidators.rb: -------------------------------------------------------------------------------- 1 | require 'active_model' 2 | require 'active_validators/one_nine_shims/one_nine_string' 3 | 4 | module ActiveValidators 5 | def self.activevalidators 6 | %w(email url respond_to phone slug ip credit_card date password twitter postal_code tracking_number siren ssn sin nino barcode date hex_color regexp) 7 | end 8 | 9 | # Require each validator independently or just pass :all 10 | # 11 | # call-seq: 12 | # ActiveValidators.activate(:phone, :email, :date) 13 | # ActiveValidators.activate(:all) 14 | def self.activate(*validators) 15 | syms = validators.include?(:all) ? activevalidators : validators.map(&:to_s) & activevalidators 16 | 17 | syms.each do |validator_name| 18 | require "active_validators/active_model/validations/#{validator_name}_validator" 19 | end 20 | end 21 | 22 | # Defines methods like validates_credit_card 23 | def define_helper_method_for_validator(validator) 24 | define_method('validates_'+validator) do |*fields| 25 | options ||= (fields.delete fields.find { |f| f.kind_of? Hash}) || true 26 | args = fields.push({ validator => options }) 27 | validates(*args) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/ip_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | class IpValidator < EachValidator 4 | def validate_each(record, attribute, value) 5 | value_str = value.to_s # might be an IPAddr 6 | record.errors.add(attribute) if value_str.blank? || !regex.match(value_str) 7 | end 8 | 9 | def check_validity! 10 | raise ArgumentError, "Unknown IP validator format #{options[:format].inspect}" unless [:v4, :v6].include? options[:format] 11 | end 12 | 13 | private 14 | def regex 15 | case options[:format] 16 | when :v4 17 | ipv4_regex 18 | when :v6 19 | ipv6_regex 20 | end 21 | end 22 | 23 | def ipv4_regex 24 | # Extracted from ruby 1.9.2 25 | regex256 = 26 | /0 27 | |1(?:[0-9][0-9]?)? 28 | |2(?:[0-4][0-9]?|5[0-5]?|[6-9])? 29 | |[3-9][0-9]?/x 30 | /\A(#{regex256})\.(#{regex256})\.(#{regex256})\.(#{regex256})\z/ 31 | end 32 | 33 | def ipv6_regex 34 | require 'resolv' 35 | Resolv::IPv6::Regex 36 | end 37 | 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /activevalidators.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | Gem::Specification.new do |s| 3 | s.name = 'activevalidators' 4 | s.version = '6.0.0' 5 | s.platform = Gem::Platform::RUBY 6 | s.authors = ['Franck Verrot'] 7 | s.email = ['franck@verrot.fr'] 8 | s.homepage = 'http://github.com/franckverrot/activevalidators' 9 | s.summary = %q{Collection of ActiveModel/ActiveRecord validations} 10 | s.description = %q{ActiveValidators is a collection of ActiveModel/ActiveRecord validations} 11 | s.license = 'MIT' 12 | 13 | s.required_ruby_version = '>= 2.4.4' 14 | 15 | s.add_development_dependency 'bundler' 16 | s.add_development_dependency 'minitest' 17 | s.add_dependency 'rake' 18 | s.add_dependency 'mail' 19 | s.add_dependency 'date_validator' 20 | s.add_dependency 'activemodel' , '>= 3.0' 21 | s.add_dependency 'phony' , '~> 2.0' 22 | s.add_dependency 'countries' , '>= 1.2', '< 4.0' 23 | s.add_dependency 'credit_card_validations', '~> 3.2' 24 | 25 | s.files = `git ls-files`.split("\n") 26 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 27 | 28 | s.cert_chain = ['certs/franckverrot.pem'] 29 | s.signing_key = File.expand_path(ENV['RUBYGEMS_CERT_PATH']) if $0 =~ /gem\z/ 30 | end 31 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/twitter_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | 4 | class TwitterValidator < EachValidator 5 | 6 | # Basic username regexp 7 | TWITTER_USERNAME_REGEXP = /([A-Za-z0-9_]{1,15})/i 8 | 9 | # Regexp used to detect twitter username within the URL. 10 | TWITTER_URL_REGEXP = %r{\Ahttps?://(?:www\.)?twitter.com/#{TWITTER_USERNAME_REGEXP}\z}i 11 | 12 | # Regexp to test using twitter username as @sign. 13 | TWITTER_ATSIGN_REGEXP = /\A@#{TWITTER_USERNAME_REGEXP}\z/i 14 | 15 | # Regexp to test against usernames without the @sign 16 | TWITTER_NOATSIGN_REGEXP = /\A#{TWITTER_USERNAME_REGEXP}\z/i 17 | 18 | def validate_each(record, attribute, value) 19 | format = options[:format].to_sym if options[:format] 20 | 21 | if value.nil? 22 | record.errors.add(attribute, :blank) 23 | elsif format == :url 24 | match = value.match(TWITTER_URL_REGEXP) 25 | record.errors.add(attribute) unless match && !match[1].nil? 26 | elsif format == :username_with_at 27 | record.errors.add(attribute) unless value =~ TWITTER_ATSIGN_REGEXP 28 | else 29 | record.errors.add(attribute) unless value =~ TWITTER_NOATSIGN_REGEXP 30 | end 31 | end 32 | end 33 | 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/validations/respond_to_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:respond_to) 3 | 4 | describe "Respond To Validation" do 5 | def build_respond_to_record attrs = {} 6 | TestRecord.reset_callbacks(:validate) 7 | TestRecord.validates :responder, :respond_to => { :call => true, :if => :local_condition }, :if => :global_condition 8 | TestRecord.new attrs 9 | end 10 | 11 | it "respond_to?" do 12 | subject = build_respond_to_record 13 | subject.responder = lambda {} 14 | subject.global_condition = true 15 | subject.local_condition = true 16 | 17 | _(subject.valid?).must_equal(true) 18 | _(subject.errors.size).must_equal(0) 19 | end 20 | 21 | describe "when does not respond_to?" do 22 | it "rejects the responder" do 23 | subject = build_respond_to_record :responder => 42, :global_condition => true, :local_condition => true 24 | _(subject.valid?).must_equal(false) 25 | _(subject.errors.size).must_equal(1) 26 | end 27 | 28 | it "generates an error message of type invalid" do 29 | subject = build_respond_to_record :responder => 42, :global_condition => true, :local_condition => true 30 | _(subject.valid?).must_equal(false) 31 | _(subject.errors[:responder].include?(subject.errors.generate_message(:responder, :invalid))).must_equal(true) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/validations/slug_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:slug) 3 | 4 | describe "Slug Validation" do 5 | def build_slug_validation attrs = {} 6 | TestRecord.reset_callbacks(:validate) 7 | TestRecord.validates :slug, :slug => true 8 | TestRecord.new attrs 9 | end 10 | 11 | it "accepts valid slugs" do 12 | subject = build_slug_validation 13 | subject.slug = '1234567890-foo-bar-bar' 14 | _(subject.valid?).must_equal(true) 15 | _(subject.errors.size).must_equal(0) 16 | end 17 | 18 | describe "for invalid slugs" do 19 | it "rejects invalid slugs" do 20 | subject = build_slug_validation :slug => '@#$%^' 21 | _(subject.valid?).must_equal(false) 22 | _(subject.errors.size).must_equal(1) 23 | end 24 | 25 | it "generates an error message of type invalid" do 26 | subject = build_slug_validation :slug => '@#$%^' 27 | _(subject.valid?).must_equal(false) 28 | _(subject.errors[:slug].include?(subject.errors.generate_message(:slug, :invalid))).must_equal(true) 29 | end 30 | end 31 | 32 | describe "for empty slugs" do 33 | it "generates an error message of type blank" do 34 | subject = build_slug_validation :slug => nil 35 | _(subject.valid?).must_equal(false) 36 | _(subject.errors[:slug].include?(subject.errors.generate_message(:slug, :blank))).must_equal(true) 37 | end 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /test/validations/hex_color_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:hex_color) 3 | 4 | describe "Hex-Color Validation" do 5 | let(:invalid_message) { subject.errors.generate_message(:text_color, :invalid) } 6 | 7 | subject { TestRecord.new } 8 | 9 | before do 10 | TestRecord.reset_callbacks(:validate) 11 | TestRecord.validates :text_color, :hex_color => true 12 | end 13 | 14 | it "accepts blank value" do 15 | subject.text_color = '' 16 | 17 | _(subject).must_be(:valid?) 18 | _(subject.errors).must_be(:empty?) 19 | end 20 | 21 | it "accepts 3 hex characters" do 22 | subject.text_color = 'abc' 23 | 24 | _(subject).must_be(:valid?) 25 | _(subject.errors).must_be(:empty?) 26 | end 27 | 28 | it "accepts 6 hex characters" do 29 | subject.text_color = 'abc012' 30 | 31 | _(subject).must_be(:valid?) 32 | _(subject.errors).must_be(:empty?) 33 | end 34 | 35 | it "rejects non-hex characters" do 36 | subject.text_color = 'efg345' 37 | 38 | _(subject).must_be(:invalid?) 39 | _(subject.errors[:text_color]).must_include(invalid_message) 40 | end 41 | 42 | it "rejects too few characters" do 43 | subject.text_color = 'ef' 44 | 45 | _(subject).must_be(:invalid?) 46 | _(subject.errors[:text_color]).must_include(invalid_message) 47 | end 48 | 49 | it "rejects too many characters" do 50 | subject.text_color = 'efab001' 51 | 52 | _(subject).must_be(:invalid?) 53 | _(subject.errors[:text_color]).must_include(invalid_message) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rubygems/specification' 3 | 4 | require 'bundler' 5 | Bundler::GemHelper.install_tasks 6 | 7 | require 'rake/testtask' 8 | Rake::TestTask.new do |t| 9 | t.libs << "test" 10 | t.pattern = "test/**/*_test.rb" 11 | t.verbose = true 12 | t.warning = true 13 | end 14 | 15 | def gemspec 16 | @gemspec ||= begin 17 | file = File.expand_path('../activevalidators.gemspec', __FILE__) 18 | eval(File.read(file), binding, file) 19 | end 20 | end 21 | 22 | desc "Clean the current directory" 23 | task :clean do 24 | rm_rf 'tmp' 25 | rm_rf 'pkg' 26 | end 27 | 28 | desc "Run the full spec suite" 29 | task :full => ["clean", "test"] 30 | 31 | desc "install the gem locally" 32 | task :install => :package do 33 | sh %{gem install pkg/#{gemspec.name}-#{gemspec.version}} 34 | end 35 | 36 | desc "validate the gemspec" 37 | task :gemspec do 38 | gemspec.validate 39 | end 40 | 41 | desc "Build the gem" 42 | task :gem => [:gemspec, :build] do 43 | mkdir_p "pkg" 44 | sh "gem build activevalidators.gemspec" 45 | mv "#{gemspec.full_name}.gem", "pkg" 46 | 47 | require 'digest/sha2' 48 | built_gem_path = "pkg/#{gemspec.full_name}.gem" 49 | checksum = Digest::SHA512.new.hexdigest(File.read(built_gem_path)) 50 | checksum_path = "checksums/#{gemspec.version}.sha512" 51 | File.open(checksum_path, 'w' ) {|f| f.write(checksum) } 52 | end 53 | 54 | desc "Install ActiveValidators" 55 | task :install => :gem do 56 | sh "gem install pkg/#{gemspec.full_name}.gem" 57 | end 58 | 59 | task :default => :full 60 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/barcode_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | class BarcodeValidator < EachValidator 4 | # We check the validity of :format option 5 | # More at https://github.com/rails/rails/blob/aa7fdfb859d8a73f58460a7aba7174a47b5101d5/activemodel/lib/active_model/validator.rb#L180 6 | def check_validity! 7 | format = options.fetch(:format) 8 | raise ArgumentError, ":format cannot be blank!" if format.blank? 9 | method = "valid_#{format.to_s}?" 10 | raise ArgumentError, "Barcode format (#{format}) not supported" unless self.respond_to?(method) 11 | end 12 | 13 | def validate_each(record, attribute, value) 14 | method = "valid_#{options[:format].to_s}?" 15 | record.errors.add(attribute) if value.blank? || !self.send(method, value.to_s) 16 | end 17 | 18 | def valid_ean13?(value) 19 | if value =~ /^\d{13}$/ 20 | ean13_check_digit(value.slice(0,12)) == value.slice(12) 21 | end 22 | end 23 | 24 | private 25 | # Comes from http://fr.wikipedia.org/wiki/Code-barres_EAN 26 | def ean13_check_digit(value) 27 | even_sum, uneven_sum = 0, 0 28 | value.split('').each_with_index do |digit, index| 29 | if (index+1).even? 30 | even_sum += digit.to_i 31 | else 32 | uneven_sum += digit.to_i 33 | end 34 | end 35 | ((10 - ((even_sum*3 + uneven_sum) % 10)) % 10).to_s 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /certs/franckverrot.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEMDCCApigAwIBAgIBATANBgkqhkiG9w0BAQsFADAhMR8wHQYDVQQDDBZmcmFu 3 | Y2svREM9dmVycm90L0RDPWZyMB4XDTE5MTIzMTA0NDkyOFoXDTIwMTIzMDA0NDky 4 | OFowITEfMB0GA1UEAwwWZnJhbmNrL0RDPXZlcnJvdC9EQz1mcjCCAaIwDQYJKoZI 5 | hvcNAQEBBQADggGPADCCAYoCggGBAMdPy9SjSIWxUF0E6AZycgmgJYJPzFRuLBi4 6 | JoT7QSgBoY85Lmzgy/tk0dEeha1B6Wt4xSR5XB1v3pws77IVGofZ4Pr46mueJlBo 7 | f3U2Xm8nykUWjufBqbKgwvdrnFq/utrWNjO+8oMYCEfKzsAQM5u1pcwUknR1ZR/s 8 | HdD8V9/bACS6rDuiOt7XBCosgxJ78ImtIBjldWy+6KssuEuacoGglqqAsisazvdc 9 | 3DkzqqnB6Mlg1+LYtuQ9m5bViA87FH3AD2H+EIYKsr7Q/1jfMFzH2U6m/jBNTQ/v 10 | uuD9PbiHtXatzmW1H/6OcsoS9lV2jjU09vJYOOGaXEMLqbAC9WxYaoE34GwAX6EL 11 | VOGVmtYMdwSkgMamDs6iUTqRZl4LMxtWff54T5EHk+ul6yDs/1ND5nbOeFpkz6JK 12 | eHUnxTxTgD48Zo3PWCtvGK36ZHVj0RmB3GqNZBr/Z+vfGeVTjzQXyy7+Gu7okGdN 13 | s1gcDoVe1uUSbot1Hi9yOwG94MV0BQIDAQABo3MwcTAJBgNVHRMEAjAAMAsGA1Ud 14 | DwQEAwIEsDAdBgNVHQ4EFgQUIWiEXS2Ddxrtivykq3L2AJMcNWAwGwYDVR0RBBQw 15 | EoEQZnJhbmNrQHZlcnJvdC5mcjAbBgNVHRIEFDASgRBmcmFuY2tAdmVycm90LmZy 16 | MA0GCSqGSIb3DQEBCwUAA4IBgQCFMXnQLS2lC537h5iMXJsxXffgrQmxElrpyJL6 17 | 0Edlr0udk9WWrvcfAeDDnciKEclzIHC9CSE3zzAX2wtLq+52JPp9f9T3ZAv+VgR6 18 | azNNOkYuyr1b8IVUVPQDjm+wjfeKvSF5pSbLCoRsswZzdyoCLj7d+E+eGt2+vksd 19 | 6O9SYwJqLNmul8V4XU6og+CkLzlHlnLnJ64WYPmd0M/RAW0NWRbDjD440mlw7p4U 20 | 6ZiUKsOT6zcDUcotKyHDjQbx17Mw0z7ETzOh1/rpuxnOWXbEEcHFbqZVcvII9Zqn 21 | EoFKzXPEEv2cl71U9NMNp/ChoCEpaY4+ZwSbpefjEeyuG64leJt7ZrFVzUK7yYlL 22 | upHY23hQxN/JZ/5w6PMX16CuRQyy8QiM3v3G6/tUC+tz8e87gUR38T+kZz9WswzF 23 | dRt6SQwLMV0PupkBmyc7hvwkjo0eXQ+SbuxwqYu03GBEapii1kjozUclRJI7Nj97 24 | Viwe79MCVe/o/vXeOr6Gso2TSEY= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /test/validations/password_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:password) 3 | 4 | describe "Password Validation" do 5 | STRENGTHS = { 6 | :weak => { :valid => 'sixchr', :invalid => 'foo' }, 7 | :medium => { :valid => 'chrs123', :invalid => 'sixchr' }, 8 | :strong => { :valid => 'HQSij2323#$%', :invalid => 'chrs123' } 9 | } 10 | 11 | STRENGTHS.each_pair do |strength, passwords| 12 | describe "#{strength} mode" do 13 | describe "valid passwords" do 14 | it "accepts a #{strength} password like #{passwords[:valid]}" do 15 | subject = build_password_record strength, :password => passwords[:valid] 16 | _(subject.valid?).must_equal(true) 17 | _(subject.errors.size).must_equal(0) 18 | end 19 | end 20 | 21 | describe "invalid passwords" do 22 | it "rejects invalid passwords like #{passwords[:invalid]}" do 23 | subject = build_password_record strength, :password => passwords[:invalid] 24 | _(subject.valid?).must_equal(false) 25 | _(subject.errors.size).must_equal(1) 26 | end 27 | 28 | it "generates an error message of type invalid" do 29 | subject = build_password_record strength, :password => passwords[:invalid] 30 | _(subject.valid?).must_equal(false) 31 | _(subject.errors[:password].include?(subject.errors.generate_message(:password, :invalid))).must_equal(true) 32 | end 33 | end 34 | end 35 | end 36 | def build_password_record(strength, attrs = {}) 37 | TestRecord.reset_callbacks(:validate) 38 | TestRecord.validates :password, :password => { :strength => strength } 39 | TestRecord.new attrs 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/validations/siren_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:siren) 3 | 4 | describe "Siren Validation" do 5 | it "Rejects if not supplied" do 6 | subject = build_siren_record true 7 | _(subject.valid?).must_equal(false) 8 | _(subject.errors.size).must_equal(1) 9 | end 10 | 11 | describe 'supplied as a string' do 12 | it "Accepts if valid" do 13 | subject = build_siren_record true, :siren => '552100554' 14 | _(subject.valid?).must_equal(true) 15 | _(subject.errors.size).must_equal(0) 16 | end 17 | 18 | it "Reject if invalid" do 19 | subject = build_siren_record true, :siren => '552100553' 20 | _(subject.valid?).must_equal(false) 21 | _(subject.errors.size).must_equal(1) 22 | end 23 | 24 | it "Reject if not the right size" do 25 | subject = build_siren_record true, :siren => '55210055' 26 | _(subject.valid?).must_equal(false) 27 | _(subject.errors.size).must_equal(1) 28 | end 29 | end 30 | 31 | describe 'supplied as a number' do 32 | it "Accepts if valid" do 33 | subject = build_siren_record true, :siren => 732829320 34 | _(subject.valid?).must_equal(true) 35 | _(subject.errors.size).must_equal(0) 36 | end 37 | 38 | it "Reject if invalid" do 39 | subject = build_siren_record true, :siren => 732829321 40 | _(subject.valid?).must_equal(false) 41 | _(subject.errors.size).must_equal(1) 42 | end 43 | 44 | it "Reject if not the right size" do 45 | subject = build_siren_record true, :siren => 73282932 46 | _(subject.valid?).must_equal(false) 47 | _(subject.errors.size).must_equal(1) 48 | end 49 | end 50 | 51 | def build_siren_record siren, attrs = {} 52 | TestRecord.reset_callbacks(:validate) 53 | TestRecord.validates :siren, :siren => siren 54 | TestRecord.new attrs 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/email_validator.rb: -------------------------------------------------------------------------------- 1 | require 'mail' 2 | 3 | old_w, $-w = $-w, false 4 | require 'mail/elements/address' 5 | $-w = old_w 6 | 7 | module ActiveModel 8 | module Validations 9 | class EmailValidator < EachValidator 10 | def check_validity! 11 | raise ArgumentError, "Not a callable object #{options[:with].inspect}" unless options[:with] == nil || options[:with].respond_to?(:call) 12 | end 13 | 14 | def validate_each(record, attribute, value) 15 | valid = begin 16 | mail = Mail::Address.new(value) 17 | 18 | basic_check(mail) && value.include?(mail.address) 19 | rescue Exception => _ 20 | false 21 | end 22 | 23 | if options[:with] 24 | # technically the test suite will pass without the boolean coercion 25 | # but we know the code is safer with it in place 26 | valid &&= !!options[:with].call(mail) 27 | end 28 | 29 | record.errors.add attribute, (options.fetch(:message, :invalid)) unless valid 30 | end 31 | 32 | def basic_check(mail) 33 | # We must check that value contains a domain and that value is an email address 34 | valid = !!mail.domain 35 | 36 | if options[:only_address] 37 | # We need to dig into treetop 38 | # A valid domain must have dot_atom_text elements size > 1 39 | # user@localhost is excluded 40 | # treetop must respond to domain 41 | # We exclude valid email values like 42 | # Hence we use m.__send__(tree).domain 43 | tree = mail.__send__(:tree) 44 | valid &&= (tree.domain.dot_atom_text.elements.size > 1) 45 | else 46 | valid &&= (mail.domain.split('.').length > 1) 47 | end 48 | valid 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/credit_card_validator.rb: -------------------------------------------------------------------------------- 1 | require 'credit_card_validations' 2 | module ActiveModel 3 | module Validations 4 | 5 | class CreditCardValidator < EachValidator 6 | def validate_each(record, attribute, value) 7 | brand = options.fetch(:type, :any) 8 | brands = (brand == :any ? [] : Array.wrap(brand)) 9 | record.errors.add(attribute) if value.blank? || !ActiveCreditCardBrand.new(value).valid?(*brands) 10 | end 11 | 12 | class ActiveCreditCardBrand 13 | 14 | DEPRECATED_BRANDS = [ 15 | :en_route, # belongs to Diners Club since 1992 obsolete 16 | :carte_blanche # belongs to Diners Club ,was finally phased out by 2005 17 | ] 18 | 19 | BRANDS_ALIASES = { 20 | master_card: :mastercard, 21 | diners_club: :diners, 22 | en_route: :diners, 23 | carte_blanche: :diners 24 | } 25 | 26 | def initialize(number) 27 | @number = number 28 | end 29 | 30 | def valid?(*brands) 31 | deprecated_brands(brands).each do |brand| 32 | ActiveSupport::Deprecation.warn("support for #{brand} will be removed in future versions, please use #{BRANDS_ALIASES[brand]} instead") 33 | end 34 | detector.valid?(*normalize_brands(brands)) 35 | end 36 | 37 | private 38 | 39 | def detector 40 | CreditCardValidations::Detector.new(@number) 41 | end 42 | 43 | def deprecated_brands(brands) 44 | DEPRECATED_BRANDS & brands 45 | end 46 | 47 | def normalize_brands(brands = []) 48 | brands.uniq.each_with_index do |brand, index| 49 | brands[index] = BRANDS_ALIASES[brand].present? ? BRANDS_ALIASES[brand] : brand 50 | end 51 | brands 52 | end 53 | 54 | end 55 | 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/ssn_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | class SsnValidator < EachValidator 4 | def validate_each(record, attribute, value) 5 | record.errors.add(attribute) if value.blank? || !SsnValidatorGeneral.valid?(options, value) 6 | end 7 | end 8 | 9 | # DEPRECATED: Please use :ssn => true instead. 10 | class SsnValidatorGeneral 11 | def self.valid?(options, value) 12 | if options[:type] == :usa_ssn 13 | ActiveSupport::Deprecation.warn "providing {:type => :usa_ssn} is deprecated and will be removed in the future. Please use `:ssn => true` instead." 14 | end 15 | 16 | SsnValidatorUSA.new(value).valid? 17 | end 18 | end 19 | 20 | # The Social Security number is a nine-digit number in the format "AAA-GG-SSSS". The number is divided into three parts. 21 | # AAA - is the first, GG - is the second and the SSSS is the third. 22 | # More details: http://en.wikipedia.org/wiki/Social_Security_number 23 | class SsnValidatorUSA 24 | def initialize(value) 25 | @value = value 26 | @first_group_num = value[0..2].to_i 27 | @second_group_num = value[3..4].to_i 28 | @third_group_num = value[5..8].to_i 29 | end 30 | 31 | def valid? 32 | all_groups_integers? && first_group_valid? && second_group_valid? && third_group_valid? 33 | end 34 | 35 | def all_groups_integers? 36 | begin 37 | !!Integer(@value) 38 | rescue ArgumentError, TypeError 39 | false 40 | end 41 | end 42 | 43 | def first_group_valid? 44 | @first_group_num != 0 && @first_group_num != 666 && (@first_group_num < 900 || @first_group_num > 999) 45 | end 46 | 47 | def second_group_valid? 48 | @second_group_num != 0 49 | end 50 | 51 | def third_group_valid? 52 | @third_group_num != 0 53 | end 54 | end 55 | end 56 | end -------------------------------------------------------------------------------- /test/validations/barcode_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:barcode) 3 | 4 | describe "Barcode Validation" do 5 | describe "EAN13 Validation" do 6 | it "accepts valid EAN13s" do 7 | ["9782940199617", "9782940199600"].each do |barcode| 8 | subject = build_barcode_record :ean13, :barcode => barcode 9 | _(subject.valid?).must_equal(true) 10 | _(subject.errors.size).must_equal(0) 11 | end 12 | end 13 | 14 | it "accepts EAN13s as integers" do 15 | subject = build_barcode_record :ean13, :barcode => 9782940199617 16 | _(subject.valid?).must_equal(true) 17 | _(subject.errors.size).must_equal(0) 18 | end 19 | 20 | describe "for invalid EAN13s" do 21 | it "rejects invalid EAN13s" do 22 | subject = build_barcode_record :ean13, :barcode => "9782940199616" 23 | _(subject.valid?).must_equal(false) 24 | _(subject.errors.size).must_equal(1) 25 | end 26 | 27 | it "rejects EAN13s with invalid length" do 28 | subject = build_barcode_record :ean13, :barcode => "50239201872045879" 29 | _(subject.valid?).must_equal(false) 30 | _(subject.errors.size).must_equal(1) 31 | end 32 | 33 | it "rejects EAN13s with invalid value (not only integers)" do 34 | subject = build_barcode_record :ean13, :barcode => "502392de872e4" 35 | _(subject.valid?).must_equal(false) 36 | _(subject.errors.size).must_equal(1) 37 | end 38 | end 39 | end 40 | 41 | describe "Invalid options given to the validator" do 42 | it "raises an error when format is not supported" do 43 | error = assert_raises ArgumentError do 44 | build_barcode_record :format_not_supported, :barcode => "9782940199617" 45 | end 46 | _(error.message).must_equal("Barcode format (format_not_supported) not supported") 47 | end 48 | 49 | it "raises an error when you omit format option" do 50 | error = assert_raises ArgumentError do 51 | build_barcode_record nil, :barcode => "9782940199617" 52 | end 53 | _(error.message).must_equal(":format cannot be blank!") 54 | end 55 | end 56 | 57 | def build_barcode_record(type, attrs = {}) 58 | TestRecord.reset_callbacks(:validate) 59 | TestRecord.validates :barcode, :barcode => { :format => type } 60 | TestRecord.new attrs 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/validations/ssn_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:ssn) 3 | 4 | describe "SSN validations" do 5 | describe "USA ssn" do 6 | describe "for invalid" do 7 | it "rejects empty ssn" do 8 | subject = build_ssn_record({:ssn => ''}, true) 9 | _(subject.valid?).must_equal(false) 10 | end 11 | 12 | it "rejects ssn when it doesn't consist of numbers" do 13 | subject = build_ssn_record({:ssn => 'aaabbcccc'}, true) 14 | _(subject.valid?).must_equal(false) 15 | end 16 | 17 | it "rejects ssn when the first group of digits is 000" do 18 | subject = build_ssn_record({:ssn => '000112222'}, true) 19 | _(subject.valid?).must_equal(false) 20 | end 21 | 22 | it "rejects ssn when the first group of digits is 666" do 23 | subject = build_ssn_record({:ssn => '666112222'}, true) 24 | _(subject.valid?).must_equal(false) 25 | end 26 | 27 | (900..999).each do |first_group_num| 28 | it "rejects ssn when the first group of digits is #{first_group_num}" do 29 | subject = build_ssn_record({:ssn => "#{first_group_num}112222"}, true) 30 | _(subject.valid?).must_equal(false) 31 | end 32 | end 33 | 34 | it "reject ssn when the second group of digits is 00" do 35 | subject = build_ssn_record({:ssn => "555002222"}, true) 36 | _(subject.valid?).must_equal(false) 37 | end 38 | 39 | it "reject ssn when the third group of digits is 0000" do 40 | subject = build_ssn_record({:ssn => "555660000"}, true) 41 | _(subject.valid?).must_equal(false) 42 | end 43 | 44 | (987654320..987654329).each do |reserved_ssn| 45 | it "rejects reserved ssn such as #{reserved_ssn}" do 46 | subject = build_ssn_record({:ssn => "#{reserved_ssn}"}, true) 47 | _(subject.valid?).must_equal(false) 48 | end 49 | end 50 | end 51 | 52 | describe "for valid" do 53 | it "supports deprecated usa_ssn syntax" do 54 | assert_deprecated do 55 | subject = build_ssn_record({:ssn => '444556666'}, {:type => :usa_ssn}) 56 | _(subject.valid?).must_equal(true) 57 | end 58 | end 59 | 60 | it "accept ssn without type (and use by default 'usa_ssn')" do 61 | subject = build_ssn_record({:ssn => '444556666'}, true) 62 | _(subject.valid?).must_equal(true) 63 | end 64 | end 65 | end 66 | 67 | def build_ssn_record(attrs = {}, validator) 68 | TestRecord.reset_callbacks(:validate) 69 | TestRecord.validates :ssn, :ssn => validator 70 | TestRecord.new attrs 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/nino_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | class NinoValidator < EachValidator 4 | def validate_each(record, attribute, value) 5 | record.errors.add(attribute) if value.blank? || !NinoValidatorUK.new(value).valid? 6 | end 7 | end 8 | 9 | # The National Insurance number is a number used in the United Kingdom. 10 | # The format of the number is two prefix letters, six digits, and one suffix letter. The example used is typically AB123456C. 11 | # Where AB - prefix, 123456 - number, C - suffix. 12 | # More details: http://en.wikipedia.org/wiki/National_Insurance_number 13 | class NinoValidatorUK 14 | def initialize(value) 15 | @nino = value.gsub(/\s/, '').upcase # Remove spaces (they may be) and make text to be same. 16 | @first_char = @nino[0] 17 | @second_char = @nino[1] 18 | @prefix = @nino[0..1] 19 | @number = @nino[2..7] 20 | @suffix = @nino[-1] 21 | end 22 | 23 | def valid? 24 | size_is?(9) && first_char_valid? && second_char_valid? && prefix_valid? && 25 | prefix_not_allocated? && prefix_not_administrative_number? && number_valid? && suffix_valid? 26 | end 27 | 28 | private 29 | 30 | def size_is?(count) 31 | @nino.size == count 32 | end 33 | 34 | def first_char_valid? 35 | forbidden_chars = %w[D F I Q U V] 36 | !forbidden_chars.include?(@first_char) 37 | end 38 | 39 | def second_char_valid? 40 | forbidden_chars = %w[D F I Q U V O] 41 | !forbidden_chars.include?(@second_char) 42 | end 43 | 44 | def prefix_valid? 45 | prefix_rule = /[a-zA-Z]{2}/ # Exactly 2 alphabet chars. 46 | !!(prefix_rule =~ @prefix) 47 | end 48 | 49 | def number_valid? 50 | number_rule = /^[0-9]{6}$/ # Exactly 6 digits. 51 | !!(number_rule =~ @number) 52 | end 53 | 54 | def suffix_valid? 55 | allowed_chars = %w[A B C D] 56 | allowed_chars.include?(@suffix) 57 | end 58 | 59 | def prefix_not_allocated? 60 | forbidden_prefixes = %w[BG GB NK KN TN NT ZZ] 61 | !forbidden_prefixes.include?(@prefix) 62 | end 63 | 64 | def prefix_not_administrative_number? 65 | administrative_prefixes = %w[OO CR FY MW NC PP PY PZ] 66 | !administrative_prefixes.include?(@prefix) 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/validations/ip_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'ipaddr' 3 | ActiveValidators.activate(:ip) 4 | 5 | describe "IP Validation" do 6 | describe "IPv4 Validation" do 7 | it "accepts valid IPs" do 8 | subject = build_ip_record :v4, :ip => '192.168.1.1' 9 | _(subject.valid?).must_equal(true) 10 | _(subject.errors.size).must_equal(0) 11 | end 12 | 13 | it "accepts IPAddrs" do 14 | subject = build_ip_record :v4, :ip => IPAddr.new('192.168.1.1') 15 | _(subject.valid?).must_equal(true) 16 | _(subject.errors.size).must_equal(0) 17 | end 18 | 19 | describe "for invalid IPs" do 20 | it "rejects invalid IPs" do 21 | subject = build_ip_record :v4, :ip => '267.34.56.3' 22 | _(subject.valid?).must_equal(false) 23 | _(subject.errors.size).must_equal(1) 24 | end 25 | 26 | it "generates an error message of type invalid" do 27 | subject = build_ip_record :v4, :ip => '267.34.56.3' 28 | _(subject.valid?).must_equal(false) 29 | _(subject.errors[:ip].include?(subject.errors.generate_message(:ip, :invalid))).must_equal(true) 30 | end 31 | end 32 | end 33 | 34 | describe "IPv6 Validation" do 35 | it "accepts valid IPs" do 36 | subject = build_ip_record :v6, :ip => '::1' 37 | _(subject.valid?).must_equal(true) 38 | _(subject.errors.size).must_equal(0) 39 | end 40 | 41 | it "accepts IPAddrs" do 42 | subject = build_ip_record :v6, :ip => IPAddr.new('::1') 43 | _(subject.valid?).must_equal(true) 44 | _(subject.errors.size).must_equal(0) 45 | end 46 | 47 | describe "for invalid IPs" do 48 | it "rejects invalid IPs" do 49 | subject = build_ip_record :v6, :ip => '192.168.1.1' 50 | _(subject.valid?).must_equal(false) 51 | _(subject.errors.size).must_equal(1) 52 | end 53 | 54 | it "generates an error message of type invalid" do 55 | subject = build_ip_record :v6, :ip => '192.168.1.1' 56 | _(subject.valid?).must_equal(false) 57 | _(subject.errors[:ip].include?(subject.errors.generate_message(:ip, :invalid))).must_equal(true) 58 | end 59 | end 60 | end 61 | 62 | it "checks validity of the arguments" do 63 | [3, "foo", 1..6].each do |wrong_argument| 64 | assert_raises(ArgumentError,"Unknown IP validator format #{wrong_argument.inspect}") do 65 | TestRecord.validates :ip, :ip => { :format => wrong_argument } 66 | end 67 | end 68 | end 69 | 70 | def build_ip_record(version, attrs = {}) 71 | TestRecord.reset_callbacks(:validate) 72 | TestRecord.validates :ip, :ip => { :format => version } 73 | TestRecord.new attrs 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/validations/phone_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:phone) 3 | 4 | describe "Phone Validation" do 5 | def build_phone_validation phone, attrs = {} 6 | TestRecord.reset_callbacks(:validate) 7 | TestRecord.validates :phone, :phone => phone 8 | TestRecord.new attrs 9 | end 10 | 11 | describe "when a country is given" do 12 | it "allows numbers matching that country" do 13 | subject = build_phone_validation(country: :gb) 14 | subject.phone = '+441234567890' 15 | _(subject.valid?).must_equal(true) 16 | end 17 | 18 | it "does not allow numbers from other countries" do 19 | subject = build_phone_validation(country: :gb) 20 | subject.phone = '+19999999999' 21 | _(subject.valid?).must_equal(false) 22 | end 23 | end 24 | 25 | describe "when no country is given" do 26 | it 'should validate format of phone with ###-###-####' do 27 | subject = build_phone_validation true 28 | subject.phone = '1-999-999-9999' 29 | _(subject.valid?).must_equal(true) 30 | _(subject.errors.size).must_equal(0) 31 | end 32 | 33 | it 'should validate format of phone with ##########' do 34 | subject = build_phone_validation true 35 | subject.phone = '19999999999' 36 | _(subject.valid?).must_equal(true) 37 | _(subject.errors.size).must_equal(0) 38 | end 39 | 40 | it 'should validate format of phone with ###.###.####' do 41 | subject = build_phone_validation true 42 | subject.phone = '1999.999.9999' 43 | _(subject.valid?).must_equal(true) 44 | _(subject.errors.size).must_equal(0) 45 | end 46 | 47 | it 'should validate format of phone with ### ### ####' do 48 | subject = build_phone_validation true 49 | subject.phone = '1999 999 9999' 50 | _(subject.valid?).must_equal(true) 51 | _(subject.errors.size).must_equal(0) 52 | end 53 | 54 | it 'should validate format of phone with (###) ###-####' do 55 | subject = build_phone_validation true 56 | subject.phone = '1(999) 999-9999' 57 | _(subject.valid?).must_equal(true) 58 | _(subject.errors.size).must_equal(0) 59 | end 60 | 61 | end 62 | 63 | 64 | describe "for invalid formats" do 65 | it "rejects invalid formats" do 66 | subject = build_phone_validation true 67 | subject.phone = '999' 68 | _(subject.valid?).must_equal(false) 69 | _(subject.errors.size).must_equal(1) 70 | end 71 | 72 | it "generates an error message of type invalid" do 73 | subject = build_phone_validation true 74 | subject.phone = '999' 75 | _(subject.valid?).must_equal(false) 76 | 77 | message = subject.errors.generate_message(:phone, :invalid) 78 | _(subject.errors[:phone].include?(message)).must_equal(true) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/tracking_number_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | class TrackingNumberValidator < EachValidator 4 | def validate_each(record, attribute, value) 5 | carrier = options[:carrier] || (options[:carrier_field] && record.send(options[:carrier_field])) 6 | raise "Carrier option required" unless carrier 7 | method = "valid_#{carrier.to_s}?" 8 | raise "Tracking number validation not supported for carrier #{carrier}" unless self.respond_to?(method) 9 | record.errors.add(attribute) if value.blank? || !self.send(method, value) 10 | end 11 | 12 | # UPS: 13 | # ups tracking codes are validated solely on their format 14 | # see https://www.ups.com/content/us/en/tracking/help/tracking/tnh.html 15 | UPS_REGEXES = [ /\A1Z[a-zA-Z0-9]{16}\z/, /\A[a-zA-Z0-9]{12}\z/, /\A[a-zA-Z0-9]{9}\z/, /\AT[a-zA-Z0-9]{10}\z/ ] 16 | def valid_ups?(value) 17 | !!UPS_REGEXES.detect { |fmt| value.match(fmt) } 18 | end 19 | 20 | # USPS: 21 | # usps tracking codes are validated based on format (one of USS228 or USS39) 22 | # and a check digit (using either of the USPS's MOD10 or MOD11 algorithms) 23 | # see USPS Publications: 24 | # - #91 (05/2008) pp. 38 25 | # - #97 (05/2002) pp. 62-63 26 | # - #109 (09/2007) pp. 19-21 27 | def valid_usps?(value) 28 | uss228?(value) || uss39?(value) 29 | end 30 | 31 | USS128_REGEX = /\A(\d{19,21})(\d)\z/ 32 | def uss228?(value) 33 | m = value.match(USS128_REGEX) 34 | m.present? && (m[2].to_i == usps_mod10(m[1])) 35 | end 36 | 37 | USS39_REGEX = /\A[a-zA-Z0-9]{2}(\d{8})(\d)US\z/ 38 | def uss39?(value) 39 | m = value.match(USS39_REGEX) 40 | # it appears to be valid for a USS39 barcode's checkdigit to be calculated with either the usps mod 10 41 | # algorithm or the usps mod 11. 42 | m.present? && (m[2].to_i == usps_mod10(m[1]) || m[2].to_i == usps_mod11(m[1])) 43 | end 44 | 45 | MOD10_WEIGHTS = [3,1] 46 | def usps_mod10(chars) 47 | (10 - weighted_sum(chars.reverse, MOD10_WEIGHTS) % 10) % 10 48 | end 49 | 50 | MOD11_WEIGHTS = [8,6,4,2,3,5,9,7] 51 | def usps_mod11(chars) 52 | mod = weighted_sum(chars, MOD11_WEIGHTS) % 11 53 | case mod 54 | when 0 then 5 55 | when 1 then 0 56 | else 11 - mod 57 | end 58 | end 59 | 60 | # takes a string containing digits and calculates a checksum using the provided weight array 61 | # cycles the weight array if it's not long enough 62 | def weighted_sum(value, weights) 63 | digits = value.split('').map { |d| d.to_i } 64 | weights = weights.cycle.take(digits.count) if weights.count < digits.count 65 | digits.zip(weights).inject(0) { |s,p| s + p[0] * p[1] } 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/validations/sin_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:sin) 3 | 4 | describe "SIN validations" do 5 | describe "for Canada" do 6 | describe "for invalid" do 7 | it "rejects empty sin" do 8 | subject = build_sin_record({:sin => ''}) 9 | _(subject.valid?).must_equal(false) 10 | end 11 | 12 | it "rejects sin made out of zeroes" do 13 | subject = build_sin_record({:sin => '000 000 000'}, {:country => :canada}) 14 | _(subject.valid?).must_equal(false) 15 | end 16 | 17 | it "rejects empty sin when country is provided" do 18 | subject = build_sin_record({:sin => ''}, {:country => :canada }) 19 | _(subject.valid?).must_equal(false) 20 | end 21 | 22 | it "rejects too long sin" do 23 | subject = build_sin_record({:sin => '123 456 789 0'}, {:country => :canada }) 24 | _(subject.valid?).must_equal(false) 25 | end 26 | 27 | it "rejects too short sin" do 28 | subject = build_sin_record({:sin => '123'}, {:country => :canada }) 29 | _(subject.valid?).must_equal(false) 30 | end 31 | 32 | it "rejects valid sin for temporary residents by default" do 33 | subject = build_sin_record({:sin => '996 454 286'}, {:country => :canada }) 34 | _(subject.valid?).must_equal(false) 35 | end 36 | 37 | it "rejects valid business numbers by default" do 38 | subject = build_sin_record({:sin => '897 454 286'}, {:country => :canada }) 39 | _(subject.valid?).must_equal(false) 40 | end 41 | 42 | it "rejects invalid sin for permanent residents" do 43 | subject = build_sin_record({:sin => '123 456 789'}, {:country => :canada }) 44 | _(subject.valid?).must_equal(false) 45 | end 46 | end 47 | 48 | describe "for valid" do 49 | it "accept valid sin for permanent residents without flags (Canada by default)" do 50 | subject = build_sin_record({:sin => '046 454 286'}, true) 51 | _(subject.valid?).must_equal(true) 52 | end 53 | 54 | it "accept valid sin for permanent residents" do 55 | subject = build_sin_record({:sin => '046 454 286'}, {:country => :canada }) 56 | _(subject.valid?).must_equal(true) 57 | end 58 | 59 | it "accept valid sin for temporary residents when flag is provided" do 60 | subject = build_sin_record({:sin => '996 454 286'}, 61 | {:country => :canada, :country_options => { allow_permanent_residents: true } }) 62 | _(subject.valid?).must_equal(true) 63 | end 64 | 65 | it "accept valid business numbers when flag is provided" do 66 | subject = build_sin_record({:sin => '897 454 286'}, 67 | {:country => :canada, :country_options => { allow_business_numbers: true } }) 68 | _(subject.valid?).must_equal(true) 69 | end 70 | end 71 | end 72 | 73 | def build_sin_record(attrs = {}, validator) 74 | TestRecord.reset_callbacks(:validate) 75 | TestRecord.validates :sin, :sin => validator 76 | TestRecord.new attrs 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/validations/credit_card_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:credit_card) 3 | 4 | describe "Credit Card Validation" do 5 | # Here are some valid credit cards 6 | VALID_CARDS = 7 | { 8 | #American Express 9 | :amex => '3400 0000 0000 009', 10 | #Discover 11 | :discover => '6011 0000 0000 0004', 12 | #Diners Club 13 | :diners_club => '3852 0000 0232 37', 14 | #JCB 15 | :jcb => '3530 1113 3330 0000', 16 | #MasterCard 17 | :master_card => '5500 0000 0000 0004', 18 | 19 | :mastercard => '5500 0000 0000 0004', 20 | #Solo 21 | :solo => '6334 0000 0000 0004', 22 | #maestro 23 | :maestro => '6759 6498 2643 8453', 24 | #Visa 25 | :visa => '4111 1111 1111 1111', 26 | } 27 | 28 | VALID_CARDS.each_pair do |card, number| 29 | describe "it accepts #{card} cards" do 30 | it "using a specific card type" do 31 | subject = build_card_record({:card => number}, {:type => card}) 32 | assert card_is_valid?(subject) 33 | end 34 | it "using :credit_card => { :type => :any }" do 35 | subject = build_card_record({:card => number}, {:type => :any}) 36 | assert card_is_valid?(subject) 37 | end 38 | it "using :credit_card => true" do 39 | subject = build_card_record({:card => number}, true) 40 | assert card_is_valid?(subject) 41 | end 42 | end 43 | end 44 | 45 | describe "carte blanche" do 46 | it "is deprecated" do 47 | subject = build_card_record( 48 | { card: '3800 0000 0000 06' }, 49 | { type: :carte_blanche }, 50 | ) 51 | assert_deprecated do 52 | card_is_valid?(subject) 53 | end 54 | end 55 | end 56 | 57 | describe "using multiple card types" do 58 | it "accepts card if one of type valid" do 59 | subject = build_card_record({:card => VALID_CARDS[:amex]}, {:type => [:visa, :master_card, :amex]}) 60 | assert card_is_valid?(subject) 61 | end 62 | 63 | it "rejects card if none of type valid" do 64 | subject = build_card_record({:card => VALID_CARDS[:solo]}, {:type => [:visa, :master_card, :amex]}) 65 | assert card_is_invalid?(subject) 66 | end 67 | end 68 | describe "for invalid cards" do 69 | it "rejects invalid cards and generates an error message of type invalid" do 70 | subject = build_card_record :card => '99999' 71 | assert card_is_invalid?(subject) 72 | end 73 | end 74 | 75 | def build_card_record(attrs = {}, validator = {:type => :any}) 76 | TestRecord.reset_callbacks(:validate) 77 | TestRecord.validates :card, :credit_card => validator 78 | TestRecord.new attrs 79 | end 80 | 81 | def card_is_valid?(subject) 82 | _(subject.valid?).must_equal(true) 83 | _(subject.errors.size).must_equal(0) 84 | end 85 | 86 | def card_is_invalid?(subject) 87 | _(subject.valid?).must_equal(false) 88 | _(subject.errors.size).must_equal(1) 89 | _(subject.errors[:card].include?(subject.errors.generate_message(:card, :invalid))).must_equal(true) 90 | end 91 | 92 | end 93 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/sin_validator.rb: -------------------------------------------------------------------------------- 1 | require 'active_validators/active_model/validations/shared/luhn_checker' 2 | 3 | module ActiveModel 4 | module Validations 5 | class SinValidator < EachValidator 6 | def validate_each(record, attribute, value) 7 | country = options.fetch(:country, :canada) # :canada is default. 8 | record.errors.add(attribute) if value.blank? || !SinValidatorGeneral.valid?(country, value, options) 9 | end 10 | end 11 | 12 | class SinValidatorGeneral 13 | def self.valid?(country, value, options) 14 | if country == :canada 15 | SinValidatorCanada.new(value, options).valid? 16 | end 17 | end 18 | end 19 | 20 | # The Social Insurance Number is a nine-digit number in the format "AAA BBB CCC". 21 | # The number is divided into three parts. 22 | # AAA - is the first, BBB - is the second and the CCC is the third. 23 | # Numbers that begin with the number "9" are issued to temporary residents who are not Canadian citizens. 24 | # Numbers that begin with the number "8" are issued to businesses as "Business Numbers". 25 | # More details: http://en.wikipedia.org/wiki/Social_Insurance_Number 26 | # 27 | # Possible flags: 28 | # allow_permanent_residents - citizens with cars, which begins with "9" are valid. By default it is false. 29 | # allow_business_numbers - business numbers, which begins with "8" are valid. By default it is false. 30 | # 31 | # validates :sin, :sin => {:country => :canada, :country_options => {:allow_permanent_residents => true, :allow_business_numbers => true}} 32 | class SinValidatorCanada 33 | def initialize(str_value, options) 34 | @sin = str_value.gsub(/\D/, '') # keep only integers 35 | @allow_permanent_residents = false 36 | @allow_business_numbers = false 37 | 38 | country_options = options.fetch(:country_options, {}) 39 | allow_permanent_residents = country_options.fetch(:allow_permanent_residents, :nil) 40 | allow_business_numbers = country_options.fetch(:allow_business_numbers, :nil) 41 | 42 | @allow_permanent_residents = true if allow_permanent_residents == true 43 | @allow_business_numbers = true if allow_business_numbers == true 44 | end 45 | 46 | def valid? 47 | size_is?(9) && is_not_full_of_zeroes && allow_permanent_residents? && allow_business_numbers? && LuhnChecker.valid?(@sin) 48 | end 49 | 50 | def size_is?(count) 51 | @sin.size == count 52 | end 53 | 54 | def is_not_full_of_zeroes 55 | @sin != '000000000' 56 | end 57 | 58 | def allow_permanent_residents? 59 | permanent_residents_indentifier = "9" 60 | 61 | if @allow_permanent_residents == false && @sin.start_with?(permanent_residents_indentifier) 62 | false 63 | else 64 | true 65 | end 66 | end 67 | 68 | def allow_business_numbers? 69 | business_indentifier = '8' 70 | 71 | !(@allow_business_numbers == false && @sin.start_with?(business_indentifier)) 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | shared: &shared 4 | working_directory: ~/repo 5 | steps: 6 | - checkout 7 | 8 | # Download and cache dependencies 9 | - restore_cache: 10 | keys: 11 | - v1-dependencies-{{ checksum "Gemfile.lock" }} 12 | # fallback to using the latest cache if no exact match is found 13 | - v1-dependencies- 14 | 15 | - run: 16 | name: install dependencies 17 | command: | 18 | bundle install --jobs=4 --retry=3 --path vendor/bundle 19 | 20 | - save_cache: 21 | paths: 22 | - ./vendor/bundle 23 | key: v1-dependencies-{{ checksum "Gemfile.lock" }} 24 | 25 | # run tests! 26 | - run: 27 | name: run tests 28 | command: bundle exec rake 29 | 30 | jobs: 31 | # RAILS_VERSION=5.1.7 32 | "ruby-2.4/rails-5.1.7": 33 | <<: *shared 34 | docker: 35 | - image: circleci/ruby:2.4 36 | environment: 37 | RAILS_VERSION=5.1.7 38 | 39 | "ruby-2.5/rails-5.1.7": 40 | <<: *shared 41 | docker: 42 | - image: circleci/ruby:2.5 43 | environment: 44 | RAILS_VERSION=5.1.7 45 | 46 | "ruby-2.6/rails-5.1.7": 47 | <<: *shared 48 | docker: 49 | - image: circleci/ruby:2.6 50 | environment: 51 | RAILS_VERSION=5.1.7 52 | 53 | "ruby-2.7/rails-5.1.7": 54 | <<: *shared 55 | docker: 56 | - image: circleci/ruby:2.7 57 | environment: 58 | RAILS_VERSION=5.1.7 59 | 60 | # RAILS_VERSION=5.2.4 61 | "ruby-2.4/rails-5.2.4": 62 | <<: *shared 63 | docker: 64 | - image: circleci/ruby:2.4 65 | environment: 66 | RAILS_VERSION=5.2.4 67 | 68 | "ruby-2.5/rails-5.2.4": 69 | <<: *shared 70 | docker: 71 | - image: circleci/ruby:2.5 72 | environment: 73 | RAILS_VERSION=5.2.4 74 | 75 | "ruby-2.6/rails-5.2.4": 76 | <<: *shared 77 | docker: 78 | - image: circleci/ruby:2.6 79 | environment: 80 | RAILS_VERSION=5.2.4 81 | 82 | "ruby-2.7/rails-5.2.4": 83 | <<: *shared 84 | docker: 85 | - image: circleci/ruby:2.7 86 | environment: 87 | RAILS_VERSION=5.2.4 88 | 89 | # RAILS_VERSION=6.0.2 90 | "ruby-2.4/rails-6.0.2": 91 | <<: *shared 92 | docker: 93 | - image: circleci/ruby:2.4 94 | environment: 95 | RAILS_VERSION=6.0.2 96 | 97 | "ruby-2.5/rails-6.0.2": 98 | <<: *shared 99 | docker: 100 | - image: circleci/ruby:2.5 101 | environment: 102 | RAILS_VERSION=6.0.2 103 | 104 | "ruby-2.6/rails-6.0.2": 105 | <<: *shared 106 | docker: 107 | - image: circleci/ruby:2.6 108 | environment: 109 | RAILS_VERSION=6.0.2 110 | 111 | "ruby-2.7/rails-6.0.2": 112 | <<: *shared 113 | docker: 114 | - image: circleci/ruby:2.7 115 | environment: 116 | RAILS_VERSION=6.0.2 117 | 118 | workflows: 119 | version: 2 120 | build: 121 | jobs: 122 | - "ruby-2.4/rails-5.1.7" 123 | - "ruby-2.5/rails-5.1.7" 124 | - "ruby-2.6/rails-5.1.7" 125 | - "ruby-2.7/rails-5.1.7" 126 | - "ruby-2.4/rails-5.2.4" 127 | - "ruby-2.5/rails-5.2.4" 128 | - "ruby-2.6/rails-5.2.4" 129 | - "ruby-2.7/rails-5.2.4" 130 | - "ruby-2.4/rails-6.0.2" 131 | - "ruby-2.5/rails-6.0.2" 132 | - "ruby-2.6/rails-6.0.2" 133 | - "ruby-2.7/rails-6.0.2" -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/postal_code_validator.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module Validations 3 | class PostalCodeValidator < EachValidator 4 | def validate_each(record, attribute, value) 5 | @value = value.to_s 6 | unless country = options[:country] 7 | if country_method = options[:country_method] 8 | country = record.send(country_method) 9 | else 10 | country = 'us' 11 | end 12 | end 13 | @formats = PostalCodeValidator.known_formats[country.to_s.downcase] 14 | record.errors.add(attribute) if value.blank? || !matches_any? 15 | end 16 | 17 | def self.known_formats 18 | @@known_formats ||= { 19 | 'ad' => ['AD###', '###'], 20 | 'ar' => ['####', '@####@@@'], 21 | 'at' => ['####'], 22 | 'au' => ['####'], 23 | 'be' => ['####'], 24 | 'bg' => ['####'], 25 | 'br' => ['#####-###', '########'], 26 | 'ca' => ['@#@ #@#', '@#@#@#'], 27 | 'ch' => ['####'], 28 | 'cz' => ['### ##', '#####'], 29 | 'de' => ['#####'], 30 | 'dk' => ['####'], 31 | 'dr' => ['#####'], 32 | 'sp' => ['#####'], 33 | 'fi' => ['#####'], 34 | 'fr' => ['#####'], 35 | 'uk' => ['@# #@@', '@## #@@', '@@# #@@', '@@## #@@', '@#@ #@@', '@@#@ #@@'], 36 | 'gb' => ['@# #@@', '@## #@@', '@@# #@@', '@@## #@@', '@#@ #@@', '@@#@ #@@'], 37 | 'gf' => ['#####'], 38 | 'gl' => ['####'], 39 | 'gp' => ['#####'], 40 | 'gt' => ['#####'], 41 | 'hr' => ['HR-#####', 'H#####'], 42 | 'hu' => ['####'], 43 | 'in' => ['######'], 44 | 'ic' => ['###'], 45 | 'it' => ['#####'], 46 | 'jp' => ['###-####', '#######'], 47 | 'ky' => ['KY#-####'], 48 | 'li' => ['####'], 49 | 'lk' => ['#####'], 50 | 'lt' => ['LT-#####', '#####'], 51 | 'lu' => ['####'], 52 | 'mc' => ['#####'], 53 | 'md' => ['MD-####'], 54 | 'mk' => ['####'], 55 | 'mq' => ['#####'], 56 | 'mx' => ['#####'], 57 | 'my' => ['#####'], 58 | 'nl' => ['#### @@', '####@@'], 59 | 'no' => ['####'], 60 | 'nz' => ['####'], 61 | 'ph' => ['####'], 62 | 'pk' => ['#####'], 63 | 'pl' => ['##-###', '#####'], 64 | 'pm' => ['#####'], 65 | 'pt' => ['####', '####-###'], 66 | 'ru' => ['######'], 67 | 'se' => ['SE-#### ##', '#### ##', '######'], 68 | 'sg' => ['######'], 69 | 'si' => ['SI- ####', 'SI-####', '####'], 70 | 'sk' => ['### ##', '#####'], 71 | 'sm' => ['4789#', '#'], 72 | 'th' => ['#####'], 73 | 'tr' => ['#####'], 74 | 'us' => ['#####', '#####-####'], 75 | 'wf' => ['#####'], 76 | 'za' => ['####'] 77 | } 78 | end 79 | 80 | def matches_any? 81 | return true if @formats.nil? or not @formats.respond_to?(:detect) 82 | @formats.detect { |format| @value.match(PostalCodeValidator.regexp_from format) } 83 | end 84 | 85 | private 86 | 87 | def self.regexp_from(format) 88 | Regexp.new '\A' + ActiveValidators::OneNineShims::OneNineString.new(Regexp.escape format).gsub(/[@#]/, '@' => '[[:alpha:]]', '#' => 'd') + '\z' 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/validations/postal_code_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:postal_code) 3 | 4 | describe "Postal Code Validation" do 5 | def build_postal_code_record postal_code, attrs = {} 6 | TestRecord.reset_callbacks(:validate) 7 | TestRecord.validates :postal_code, :postal_code => postal_code 8 | TestRecord.new attrs 9 | end 10 | 11 | describe "when no country is given" do 12 | it 'should validate format of postal code with #####' do 13 | subject = build_postal_code_record true 14 | subject.postal_code = '11211' 15 | _(subject.valid?).must_equal(true) 16 | _(subject.errors.size).must_equal(0) 17 | end 18 | 19 | it 'should validate format of postal code with #####-#####' do 20 | subject = build_postal_code_record true 21 | subject.postal_code = '94117-1234' 22 | _(subject.valid?).must_equal(true) 23 | _(subject.errors.size).must_equal(0) 24 | end 25 | end 26 | 27 | ActiveModel::Validations::PostalCodeValidator.known_formats.each do |country, formats| 28 | describe "when given a :#{country} country parameter" do 29 | formats.each do |format| 30 | it "should validate format of lowercase postal code with #{format}" do 31 | subject = build_postal_code_record :country => country 32 | subject.postal_code = ActiveValidators::OneNineShims::OneNineString.new(format).gsub(/[@#]/, '@' => 'A', '#' => '9') 33 | _(subject.valid?).must_equal(true) 34 | _(subject.errors.size).must_equal(0) 35 | end 36 | 37 | it "should validate format of upcase postal code with #{format}" do 38 | subject = build_postal_code_record :country => country.upcase 39 | subject.postal_code = ActiveValidators::OneNineShims::OneNineString.new(format).gsub(/[@#]/, '@' => 'A', '#' => '9') 40 | _(subject.valid?).must_equal(true) 41 | _(subject.errors.size).must_equal(0) 42 | end 43 | 44 | # if format is entirely numeric 45 | if format !~ /[^#\d]/ 46 | it "should validate format of integer postal code with #{format}" do 47 | subject = build_postal_code_record :country => country 48 | subject.postal_code = ActiveValidators::OneNineShims::OneNineString.new(format).gsub(/[@#]/, '@' => 'A', '#' => '9').to_i 49 | _(subject.valid?).must_equal(true) 50 | _(subject.errors.size).must_equal(0) 51 | end 52 | end 53 | end 54 | end 55 | end 56 | 57 | describe "for a country without known formats" do 58 | it "accepts anything" do 59 | # aa is not in ActiveModel::Validations::PostalCodeValidator.known_formats 60 | subject = build_postal_code_record :country => 'aa' 61 | subject.postal_code = '999' 62 | _(subject.valid?).must_equal(true) 63 | _(subject.errors.size).must_equal(0) 64 | end 65 | end 66 | 67 | describe "for invalid formats" do 68 | it "rejects invalid formats" do 69 | subject = build_postal_code_record true, :postal_code => '999' 70 | _(subject.valid?).must_equal(false) 71 | _(subject.errors.size).must_equal(1) 72 | end 73 | 74 | it "generates an error message of type invalid" do 75 | subject = build_postal_code_record true, :postal_code => '999' 76 | _(subject.valid?).must_equal(false) 77 | _(subject.errors[:postal_code].include?(subject.errors.generate_message(:postal_code, :invalid))).must_equal(true) 78 | end 79 | 80 | it "rejects injected content" do 81 | subject = build_postal_code_record true, :postal_code => "injected\n11211" 82 | _(subject.valid?).must_equal(false) 83 | _(subject.errors.size).must_equal(1) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/active_validators/active_model/validations/url_validator.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/array/wrap' 2 | require 'uri' 3 | 4 | module ActiveModel 5 | module Validations 6 | 7 | # Public: Uses `URI.regexp` to validate URLs, by default only allows 8 | # the http and https protocols. 9 | # 10 | # Examples 11 | # 12 | # validates :url, :url => true 13 | # # => only allow http, https 14 | # 15 | # validates :url, :url => %w{http https ftp ftps} 16 | # # => allow http, https, ftp and ftps 17 | # 18 | class UrlValidator < EachValidator 19 | 20 | # Public: Creates a new instance, overrides `:protocols` if either 21 | # `:with` or `:in` are defined, to allow for shortcut setters. 22 | # 23 | # Examples: 24 | # 25 | # validates :url, :url => { :protocols => %w{http https ftp} } 26 | # # => accepts http, https and ftp URLs 27 | # 28 | # validates :url, :url => 'https' 29 | # # => accepts only https URLs (shortcut form of above) 30 | # 31 | # validates :url, :url => true 32 | # # => by default allows only http and https 33 | # 34 | # Raises an ArgumentError if the array with the allowed protocols 35 | # is empty. 36 | # 37 | # Returns a new instance. 38 | def initialize(options) 39 | options[:protocols] ||= options.delete(:protocol) || options.delete(:with) || options.delete(:in) 40 | super 41 | end 42 | 43 | # Public: Validate URL, if it fails adds an error. 44 | # 45 | # Returns nothing. 46 | def validate_each(record, attribute, value) 47 | uri = as_uri(value) 48 | tld_requirement_fullfilled = check_tld_requirement(value) 49 | record.errors.add(attribute) unless uri && value.to_s =~ uri_regexp && tld_requirement_fullfilled 50 | end 51 | 52 | private 53 | # Internal: Ensures that at least one protocol is defined. If the protocols 54 | # are empty it throws an ArgumentError. 55 | # 56 | # Raises ArgumentError if protocols is empty. 57 | # 58 | # Returns nothing. 59 | def check_validity! 60 | raise ArgumentError, "At least one URI protocol is required" if protocols.empty? 61 | end 62 | 63 | # Internal: Returns an array of protocols to use with the URI regexp. 64 | # The default protocols are `http` and `https`. 65 | # 66 | # Returns the Array with the allowed protocols. 67 | def protocols 68 | Array.wrap(options[:protocols] || %w{http https}) 69 | end 70 | 71 | # Internal: Constructs the regular expression to check 72 | # the URI for the configured protocols. 73 | # 74 | # Returns the Regexp. 75 | def uri_regexp 76 | @uri_regexp ||= /\A#{URI::Parser.new.make_regexp(protocols)}\z/ 77 | end 78 | 79 | # Internal: Checks if the tld requirements are fullfilled 80 | # 81 | # When :require_tld option is set to true, the url will be searched for 82 | # a dot anywhere inside the host except the first or last position 83 | # 84 | # Returns a boolean value. 85 | def check_tld_requirement(value) 86 | host = URI.parse(value.to_s).host rescue value 87 | options[:require_tld] === true ? host =~ /.(\.)\w+/ : true 88 | end 89 | 90 | # Internal: Tries to convert supplied string into URI, 91 | # if not possible returns nil => invalid URI. 92 | # 93 | # Returns the URI or nil. 94 | def as_uri(value) 95 | URI.parse(value.to_s) rescue nil if value.present? 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/validations/nino_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:nino) 3 | 4 | describe "NINO validations" do 5 | describe "for UK" do 6 | describe "for invalid" do 7 | it "rejects empty nino" do 8 | subject = build_nino_record({:nino => ''}) 9 | _(subject.valid?).must_equal(false) 10 | end 11 | 12 | it "rejects nino, which has invalid first letter" do 13 | invalid_first_letter = %w[d f i q u v] 14 | invalid_first_letter.each do |letter| 15 | invalid_nino = letter + 'B123456C' 16 | subject = build_nino_record({:nino => invalid_nino}) 17 | _(subject.valid?).must_equal(false) 18 | end 19 | end 20 | 21 | it "rejects nino, which has invalid second letter" do 22 | invalid_second_letter = %w[d f i q u v o] 23 | invalid_second_letter.each do |letter| 24 | invalid_nino = 'a' + letter + '123456C' 25 | subject = build_nino_record({:nino => invalid_nino}) 26 | _(subject.valid?).must_equal(false) 27 | end 28 | end 29 | 30 | it "rejects too long nino" do 31 | subject = build_nino_record({:nino => 'AB123456C100200'}) 32 | _(subject.valid?).must_equal(false) 33 | end 34 | 35 | it "rejects nino with invalid prefix" do 36 | subject = build_nino_record({:nino => '#$123456C'}) 37 | _(subject.valid?).must_equal(false) 38 | end 39 | 40 | it "rejects nino with invalid chars in number section" do 41 | subject = build_nino_record({:nino => 'AB@2%4#6C'}) 42 | _(subject.valid?).must_equal(false) 43 | end 44 | 45 | it "rejects wrong suffixes" do 46 | invalid_suffix = %w[E F G H] # and etc 47 | invalid_suffix.each do |suffix| 48 | invalid_nino = 'AB123456' + suffix 49 | subject = build_nino_record({:nino => invalid_nino}) 50 | _(subject.valid?).must_equal(false) 51 | end 52 | end 53 | 54 | it "rejects non allocated prefixes" do 55 | non_allocated_prefixes = %w[bg gb nk kn tn nt zz] 56 | non_allocated_prefixes.each do |suffix| 57 | invalid_nino = suffix + '123456C' 58 | subject = build_nino_record({:nino => invalid_nino}) 59 | _(subject.valid?).must_equal(false) 60 | end 61 | end 62 | 63 | it "rejects administrative numbers" do 64 | administrative_numbers = %w[oo cr fy mw nc pp py pz] 65 | administrative_numbers.each do |suffix| 66 | invalid_nino = suffix + '123456C' 67 | subject = build_nino_record({:nino => invalid_nino}) 68 | _(subject.valid?).must_equal(false) 69 | end 70 | end 71 | 72 | it "rejects temporary numbers" do 73 | subject = build_nino_record({:nino => '63T12345'}) 74 | _(subject.valid?).must_equal(false) 75 | end 76 | end 77 | 78 | describe "for valid" do 79 | it "acepts correct nino" do 80 | subject = build_nino_record({:nino => 'AB123456C'}) 81 | _(subject.valid?).must_equal(true) 82 | end 83 | 84 | it "acepts nino, divided by spaces" do 85 | subject = build_nino_record({:nino => 'AB 12 34 56 C'}) 86 | _(subject.valid?).must_equal(true) 87 | end 88 | 89 | it "acepts downcase nino" do 90 | subject = build_nino_record({:nino => 'ab 12 34 56 c'}) 91 | _(subject.valid?).must_equal(true) 92 | end 93 | 94 | it "acepts correct suffixes" do 95 | valid_suffix = %w[A B C D] 96 | valid_suffix.each do |suffix| 97 | valid_nino = 'AB123456' + suffix 98 | subject = build_nino_record({:nino => valid_nino}) 99 | _(subject.valid?).must_equal(true) 100 | end 101 | end 102 | end 103 | end 104 | 105 | def build_nino_record(attrs = {}) 106 | TestRecord.reset_callbacks(:validate) 107 | TestRecord.validates :nino, :nino => true 108 | TestRecord.new attrs 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /test/validations/url_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:url) 3 | 4 | describe "Url Validation" do 5 | def build_url_record 6 | TestRecord.reset_callbacks(:validate) 7 | TestRecord.validates :url, :url => true 8 | TestRecord.new 9 | end 10 | 11 | def build_url_record_require_tld 12 | TestRecord.reset_callbacks(:validate) 13 | TestRecord.validates :url, :url => { :require_tld => true } 14 | TestRecord.new 15 | end 16 | 17 | def build_ftp_record 18 | TestRecord.reset_callbacks(:validate) 19 | TestRecord.validates :url, :url => 'ftp' 20 | TestRecord.new 21 | end 22 | 23 | describe "valid urls" do 24 | it "accepts urls without port number" do 25 | subject = build_url_record 26 | subject.url = 'http://www.verrot.fr' 27 | _(subject.valid?).must_equal(true) 28 | _(subject.errors.size).must_equal(0) 29 | end 30 | 31 | it "accepts urls with port number" do 32 | subject = build_url_record 33 | subject.url = 'http://www.verrot.fr:1234' 34 | _(subject.valid?).must_equal(true) 35 | _(subject.errors.size).must_equal(0) 36 | end 37 | 38 | it "accepts urls with basic auth" do 39 | subject = build_url_record 40 | subject.url = 'http://foo:bar@www.verrot.fr' 41 | _(subject.valid?).must_equal(true) 42 | _(subject.errors.size).must_equal(0) 43 | end 44 | 45 | it "accepts valid SSL urls" do 46 | subject = build_url_record 47 | subject.url = 'https://www.verrot.fr' 48 | _(subject.valid?).must_equal(true) 49 | _(subject.errors.size).must_equal(0) 50 | end 51 | 52 | it "accepts ftp if defined" do 53 | subject = build_ftp_record 54 | subject.url = 'ftp://ftp.verrot.fr' 55 | _(subject.valid?).must_equal(true) 56 | _(subject.errors.size).must_equal(0) 57 | end 58 | 59 | it "accepts urls with tld when tld validation is required" do 60 | subject = build_url_record_require_tld 61 | subject.url = 'http://verrot.fr' 62 | _(subject.valid?).must_equal(true) 63 | _(subject.errors.size).must_equal(0) 64 | end 65 | end 66 | 67 | describe "for invalid urls" do 68 | it "rejects invalid urls" do 69 | subject = build_url_record 70 | subject.url = 'http://^^^^.fr' 71 | _(subject.valid?).must_equal(false) 72 | _(subject.errors.size).must_equal(1) 73 | end 74 | 75 | it "rejects injected urls" do 76 | subject = build_url_record 77 | subject.url = "javascript:alert('xss');\nhttp://google.com" 78 | _(subject.valid?).must_equal(false) 79 | _(subject.errors.size).must_equal(1) 80 | end 81 | 82 | it "generates an error message of type invalid" do 83 | subject = build_url_record 84 | subject.url = 'http://^^^^.fr' 85 | _(subject.valid?).must_equal(false) 86 | _(subject.errors[:url].include?(subject.errors.generate_message(:url, :invalid))).must_equal(true) 87 | end 88 | 89 | it "rejects nil urls" do 90 | subject = build_url_record 91 | subject.url = nil 92 | _(subject.valid?).must_equal(false) 93 | _(subject.errors.size).must_equal(1) 94 | end 95 | 96 | it "rejects empty urls" do 97 | subject = build_url_record 98 | subject.url = '' 99 | _(subject.valid?).must_equal(false) 100 | _(subject.errors.size).must_equal(1) 101 | end 102 | 103 | it "rejects invalid protocols" do 104 | subject = build_url_record 105 | subject.url = 'ftp://ftp.verrot.fr' 106 | _(subject.valid?).must_equal(false) 107 | _(subject.errors.size).must_equal(1) 108 | end 109 | 110 | it "rejects urls that have no tld but required tld validation" do 111 | subject = build_url_record_require_tld 112 | subject.url = 'http://verrot' 113 | _(subject.valid?).must_equal(false) 114 | _(subject.errors.size).must_equal(1) 115 | end 116 | 117 | it "will not accept domains that end with a dot as a tld" do 118 | subject = build_url_record_require_tld 119 | subject.url = 'http://verrot.' 120 | _(subject.valid?).must_equal(false) 121 | _(subject.errors.size).must_equal(1) 122 | end 123 | 124 | it "will not accept domains that start with a dot as a tld" do 125 | subject = build_url_record_require_tld 126 | subject.url = 'http://.verrot' 127 | _(subject.valid?).must_equal(false) 128 | _(subject.errors.size).must_equal(1) 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /test/validations/email_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:email) 3 | 4 | describe "Email Validation" do 5 | describe "strict: true" do 6 | 7 | it "accepts valid emails" do 8 | subject = build_email_record({:email => 'franck@verrot.fr'}, {:strict => true}) 9 | _(subject.valid?).must_equal(true) 10 | _(subject.errors.size).must_equal(0) 11 | end 12 | 13 | it "accepts valid emails" do 14 | subject = build_email_record({:email => 'franck@edu.verrot-gouv.fr'}, {:strict => true}) 15 | _(subject.valid?).must_equal(true) 16 | _(subject.errors.size).must_equal(0) 17 | end 18 | 19 | it "accepts complete emails" do 20 | subject = build_email_record({:email => 'Mikel Lindsaar (My email address) '}, {:strict => true}) 21 | _(subject.valid?).must_equal(true) 22 | _(subject.errors.size).must_equal(0) 23 | end 24 | 25 | it "accepts email addresses without TLD" do 26 | subject = build_email_record({:email => 'franck@verrotfr'}, {:strict => true}) 27 | _(subject.valid?).must_equal(false) 28 | _(subject.errors.size).must_equal(1) 29 | end 30 | end 31 | 32 | describe "strict: false (default)" do 33 | it "accepts valid emails" do 34 | subject = build_email_record :email => 'franck@verrot.fr' 35 | _(subject.valid?).must_equal(true) 36 | _(subject.errors.size).must_equal(0) 37 | end 38 | 39 | it "accepts valid emails" do 40 | subject = build_email_record :email => 'franck@edu.verrot-gouv.fr' 41 | _(subject.valid?).must_equal(true) 42 | _(subject.errors.size).must_equal(0) 43 | end 44 | 45 | it "accepts complete emails" do 46 | subject = build_email_record :email => 'Mikel Lindsaar (My email address) ' 47 | _(subject.valid?).must_equal(true) 48 | _(subject.errors.size).must_equal(0) 49 | end 50 | 51 | it "rejects email addresses without TLD" do 52 | subject = build_email_record :email => 'franck@verrotfr' 53 | _(subject.valid?).must_equal(false) 54 | _(subject.errors.size).must_equal(1) 55 | end 56 | end 57 | 58 | describe "with: #" do 59 | it "rejects any false result" do 60 | subject = build_email_record({:email => "franck@verrot.fr"}, {:with => lambda {|e| false }}) 61 | _(subject.valid?).must_equal(false) 62 | _(subject.errors.size).must_equal(1) 63 | end 64 | 65 | it "accepts any true result" do 66 | subject = build_email_record({:email => "franck@verrot.fr"}, {:with => lambda {|e| true }}) 67 | _(subject.valid?).must_equal(true) 68 | _(subject.errors.size).must_equal(0) 69 | end 70 | 71 | it "passes in the parsed email address" do 72 | subject = build_email_record({:email => "franck@hotmail.com"}, {:with => lambda {|email| not email.domain == "hotmail.com" }}) 73 | _(subject.valid?).must_equal(false) 74 | _(subject.errors.size).must_equal(1) 75 | end 76 | 77 | it "rejects a nil result" do 78 | subject = build_email_record({:email => "franck@verrot.fr"}, {:with => lambda {|email| email.domain =~ /\.com\Z/ }}) 79 | _(subject.valid?).must_equal(false) 80 | _(subject.errors.size).must_equal(1) 81 | end 82 | 83 | it "accepts a numerical result" do 84 | subject = build_email_record({:email => "franck@verrot.fr"}, {:with => lambda {|email| email.domain =~ /\.fr\Z/ }}) 85 | _(subject.valid?).must_equal(true) 86 | _(subject.errors.size).must_equal(0) 87 | end 88 | end 89 | 90 | describe "for invalid emails" do 91 | it "rejects invalid emails" do 92 | subject = build_email_record :email => 'franck.fr' 93 | _(subject.valid?).must_equal(false) 94 | _(subject.errors.size).must_equal(1) 95 | end 96 | 97 | it 'rejects local emails' do 98 | subject = build_email_record :email => 'franck.fr' 99 | subject.email = 'franck' 100 | _(subject.valid?).must_equal(false) 101 | _(subject.errors.size).must_equal(1) 102 | end 103 | 104 | it 'generates an error message of type invalid' do 105 | subject = build_email_record :email => 'franck@verrot@fr' 106 | _(subject.valid?).must_equal(false) 107 | 108 | message = subject.errors.generate_message(:email, :invalid) 109 | _(subject.errors[:email].include?(message)).must_equal(true) 110 | end 111 | end 112 | 113 | before { TestRecord.reset_callbacks(:validate) } 114 | it "checks validity of the arguments" do 115 | [3, "foo", 1..6].each do |wrong_argument| 116 | assert_raises(ArgumentError,"Not a callable object #{wrong_argument.inspect}") do 117 | TestRecord.validates :email, :email => { :with => wrong_argument } 118 | end 119 | end 120 | end 121 | 122 | def build_email_record(attrs = {}, opts = {}) 123 | TestRecord.reset_callbacks(:validate) 124 | TestRecord.validates :email, :email => { :strict => opts[:strict], :with => opts[:with] } 125 | TestRecord.new attrs 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/validations/tracking_number_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | ActiveValidators.activate(:tracking_number) 3 | 4 | describe "Tracking Number Validation" do 5 | def build_tracking_number_record carrier_opts, attrs = {} 6 | TestRecord.reset_callbacks(:validate) 7 | TestRecord.validates :tracking_number, :tracking_number => carrier_opts 8 | TestRecord.new(attrs).tap do |record| 9 | yield record if block_given? 10 | end 11 | end 12 | 13 | def assert_valid_tracking_number(carrier_opts, tracking_number, &block) 14 | subject = build_tracking_number_record(carrier_opts, &block) 15 | subject.tracking_number = tracking_number 16 | _(subject.valid?).must_equal(true) 17 | _(subject.errors.size).must_equal(0) 18 | end 19 | 20 | def assert_invalid_tracking_number(carrier_opts, tracking_number, &block) 21 | subject = build_tracking_number_record(carrier_opts, &block) 22 | subject.tracking_number = tracking_number 23 | _(subject.valid?).must_equal(false) 24 | _(subject.errors.size).must_equal(1) 25 | _(subject.errors[:tracking_number]).must_include(subject.errors.generate_message(:tracking_number, :invalid)) 26 | end 27 | 28 | describe "when no carrier parameter is given" do 29 | it "raises an exception" do 30 | assert_raises(RuntimeError, "Carrier option required") { build_tracking_number_record({:carrier=>true}).valid? } 31 | end 32 | end 33 | 34 | describe "when given a ups carrier parameter" do 35 | it 'should validate format of tracking number with 1Z................' do 36 | assert_valid_tracking_number({:carrier => :ups}, '1Z12345E0205271688') 37 | end 38 | 39 | it 'should validate format of tracking number with ............' do 40 | assert_valid_tracking_number({:carrier => :ups}, '9999V999J999') 41 | end 42 | 43 | it 'should validate format of tracking number with T..........' do 44 | assert_valid_tracking_number({:carrier => :ups}, 'T99F99E9999') 45 | end 46 | 47 | it 'should validate format of tracking number with .........' do 48 | assert_valid_tracking_number({:carrier => :ups}, '990728071') 49 | end 50 | 51 | describe "for invalid formats" do 52 | it "rejects invalid formats and generates an error message of type invalid" do 53 | assert_invalid_tracking_number({:carrier => :ups}, '1Z12345E020_271688') 54 | end 55 | 56 | it "rejects injected content" do 57 | assert_invalid_tracking_number({:carrier => :ups}, "injected\n1Z12345E0205271688") 58 | end 59 | end 60 | end 61 | 62 | describe "when given a usps carrier parameter" do 63 | describe "with valid formats" do 64 | it 'USS39 tracking number with valid MOD10 check digit' do 65 | assert_valid_tracking_number({:carrier => :usps}, 'EA123456784US') 66 | end 67 | 68 | it 'USS39 tracking number with valid MOD11 check digit' do 69 | assert_valid_tracking_number({:carrier => :usps}, 'RB123456785US') 70 | end 71 | 72 | it '20 character USS128 tracking number with valid MOD10 check digit' do 73 | assert_valid_tracking_number({:carrier => :usps}, '71123456789123456787') 74 | end 75 | 76 | it '20 character USS128 tracking number with valid MOD10 check digit ending in 0' do 77 | assert_valid_tracking_number({:carrier => :usps}, '03110240000115809160') 78 | end 79 | 80 | it '22 character USS128 tracking number with valid MOD10 check digit' do 81 | assert_valid_tracking_number({:carrier => :usps}, '9171969010756003077385') 82 | end 83 | end 84 | 85 | describe "with invalid formats" do 86 | it 'USS39 tracking number with invalid check digit' do 87 | assert_invalid_tracking_number({:carrier => :usps}, 'EA123456782US') 88 | end 89 | 90 | it 'USS39 tracking number that is too short' do 91 | assert_invalid_tracking_number({:carrier => :usps}, '123456784US') 92 | end 93 | 94 | it 'USS39 tracking number that is too long' do 95 | assert_invalid_tracking_number({:carrier => :usps}, 'EAB123456784US') 96 | end 97 | 98 | it 'USS39 tracking number with non-"US" product id' do 99 | assert_invalid_tracking_number({:carrier => :usps}, 'EA123456784UT') 100 | end 101 | 102 | it 'USS128 tracking number with invalid check-digit' do 103 | assert_invalid_tracking_number({:carrier => :usps}, '71123456789123456788') 104 | end 105 | 106 | it 'USS128 tracking number that is too short' do 107 | assert_invalid_tracking_number({:carrier => :usps}, '7112345678912345678') 108 | end 109 | 110 | it 'USS128 tracking number that is too long' do 111 | assert_invalid_tracking_number({:carrier => :usps}, '711234567891234567879287') 112 | end 113 | 114 | it 'USS128 tracking number with invalid chars' do 115 | assert_invalid_tracking_number({:carrier => :usps}, 'U11234567891234567879') 116 | end 117 | 118 | it 'rejects injected chars in USS39 and others' do 119 | assert_invalid_tracking_number({:carrier => :usps}, "injected\nEA123456784US") 120 | end 121 | end 122 | end 123 | 124 | describe "when given a record-based carrier parameter" do 125 | describe "when record gives 'ups' as carrier" do 126 | it 'with valid ups tracking number' do 127 | assert_valid_tracking_number({:carrier_field => :carrier}, '1Z12345E0205271688') do |subject| 128 | subject.carrier = 'ups' 129 | end 130 | end 131 | 132 | it 'with invalid ups tracking number' do 133 | assert_invalid_tracking_number({:carrier_field => :carrier}, '1Z12__5E0205271688') do |subject| 134 | subject.carrier = 'ups' 135 | end 136 | end 137 | end 138 | 139 | describe "when record gives 'usps' as carrier" do 140 | it 'with valid usps tracking number' do 141 | assert_valid_tracking_number({:carrier_field => :carrier}, 'EA123456784US') do |subject| 142 | subject.carrier = 'usps' 143 | end 144 | end 145 | 146 | it 'with invalid usps tracking number' do 147 | assert_invalid_tracking_number({:carrier_field => :carrier}, 'EA123456784UZ') do |subject| 148 | subject.carrier = 'usps' 149 | end 150 | end 151 | end 152 | 153 | describe "when record gives an unsupported carrier" do 154 | it "raises an error when validating" do 155 | assert_raises(RuntimeError, "Carrier option required") do 156 | build_tracking_number_record({:carrier=>:carrier}, :tracking_number => 'EA123456784US') do |subject| 157 | subject.carrier = 'fedex' 158 | subject.tracking_number = 'EA123456784US' 159 | end.valid? 160 | end 161 | end 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/validations/twitter_test.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require 'test_helper' 3 | ActiveValidators.activate(:twitter) 4 | 5 | describe "Twitter Validation" do 6 | def build_twitter_record format, attrs = {} 7 | TestRecord.reset_callbacks(:validate) 8 | TestRecord.validates :twitter_username, :twitter => format 9 | TestRecord.new attrs 10 | end 11 | 12 | it "rejects invalid urls" do 13 | subject = build_twitter_record true 14 | _(subject.valid?).must_equal(false) 15 | _(subject.errors.size).must_equal(1) 16 | end 17 | 18 | it "generates an error message of type blank" do 19 | subject = build_twitter_record true 20 | _(subject.valid?).must_equal(false) 21 | 22 | message = subject.errors.generate_message(:twitter_username, :blank) 23 | _(subject.errors[:twitter_username].include?(message)).must_equal(true) 24 | end 25 | 26 | describe "for twitter url validator" do 27 | it "validates with http" do 28 | subject = build_twitter_record :format => :url 29 | subject.twitter_username = 'http://twitter.com/garrettb' 30 | _(subject.valid?).must_equal(true) 31 | end 32 | 33 | it "validates with https protocol" do 34 | subject = build_twitter_record :format => :url 35 | subject.twitter_username = 'https://twitter.com/garrettb' 36 | _(subject.valid?).must_equal(true) 37 | end 38 | 39 | it "generate error with ftp protocol" do 40 | subject = build_twitter_record :format => :url 41 | subject.twitter_username = 'ftp://twitter.com/garrettb' 42 | _(subject.valid?).must_equal(false) 43 | _(subject.errors.size).must_equal(1) 44 | end 45 | 46 | it "validates with www and http" do 47 | subject = build_twitter_record :format => :url 48 | subject.twitter_username = 'http://www.twitter.com/garrettb' 49 | _(subject.valid?).must_equal(true) 50 | end 51 | 52 | it "generate error without www dot" do 53 | subject = build_twitter_record :format => :url 54 | subject.twitter_username = 'http://wwwtwitter.com/garrettb' 55 | _(subject.valid?).must_equal(false) 56 | _(subject.errors.size).must_equal(1) 57 | end 58 | 59 | it "generate error without no username" do 60 | subject = build_twitter_record :format => :url 61 | subject.twitter_username = 'http://twitter.com' 62 | _(subject.valid?).must_equal(false) 63 | _(subject.errors.size).must_equal(1) 64 | end 65 | 66 | it "generate error without no username and trailing slash" do 67 | subject = build_twitter_record :format => :url 68 | subject.twitter_username = 'http://twitter.com/' 69 | _(subject.valid?).must_equal(false) 70 | _(subject.errors.size).must_equal(1) 71 | end 72 | 73 | it "generate error with too long of username" do 74 | subject = build_twitter_record :format => :url 75 | subject.twitter_username = 'http://twitter.com/garrettbjerkhoelwashere' 76 | _(subject.valid?).must_equal(false) 77 | _(subject.errors.size).must_equal(1) 78 | end 79 | 80 | it "generate error with invalid character" do 81 | subject = build_twitter_record :format => :url 82 | subject.twitter_username = 'http://twitter.com/garrettbjerkhoé' 83 | _(subject.valid?).must_equal(false) 84 | _(subject.errors.size).must_equal(1) 85 | end 86 | 87 | it "generates error with injected content" do 88 | subject = build_twitter_record :format => :url 89 | subject.twitter_username = "javascript:alert('xss');\nhttp://twitter.com/garrettbjerkhoelwashere" 90 | _(subject.valid?).must_equal(false) 91 | _(subject.errors.size).must_equal(1) 92 | end 93 | end 94 | 95 | describe "for twitter at sign validator" do 96 | it "validate with valid username" do 97 | subject = build_twitter_record :format => :username_with_at 98 | subject.twitter_username = '@garrettb' 99 | _(subject.valid?).must_equal(true) 100 | end 101 | 102 | it "validate with one character" do 103 | subject = build_twitter_record :format => :username_with_at 104 | subject.twitter_username = '@a' 105 | _(subject.valid?).must_equal(true) 106 | end 107 | 108 | it "generate error with too long of username" do 109 | subject = build_twitter_record :format => :username_with_at 110 | subject.twitter_username = '@garrettbjerkhoelwashere' 111 | _(subject.valid?).must_equal(false) 112 | _(subject.errors.size).must_equal(1) 113 | end 114 | 115 | it "generate error with no username" do 116 | subject = build_twitter_record :format => :username_with_at 117 | subject.twitter_username = '@' 118 | _(subject.valid?).must_equal(false) 119 | _(subject.errors.size).must_equal(1) 120 | end 121 | 122 | it "generate error with invalid character" do 123 | subject = build_twitter_record :format => :username_with_at 124 | subject.twitter_username = '@érik' 125 | _(subject.valid?).must_equal(false) 126 | _(subject.errors.size).must_equal(1) 127 | end 128 | 129 | it "generate error with injected content" do 130 | subject = build_twitter_record :format => :username_with_at 131 | subject.twitter_username = "injected\n@erik" 132 | _(subject.valid?).must_equal(false) 133 | _(subject.errors.size).must_equal(1) 134 | end 135 | end 136 | 137 | describe "for twitter without at sign validator" do 138 | it "validate with valid username" do 139 | subject = build_twitter_record true 140 | subject.twitter_username = 'garrettb' 141 | _(subject.valid?).must_equal(true) 142 | end 143 | 144 | it "validate with one character" do 145 | subject = build_twitter_record true 146 | subject.twitter_username = 'a' 147 | _(subject.valid?).must_equal(true) 148 | end 149 | 150 | it "generate error with too long of username" do 151 | subject = build_twitter_record true 152 | subject.twitter_username = 'garrettbjerkhoelwashere' 153 | _(subject.valid?).must_equal(false) 154 | _(subject.errors.size).must_equal(1) 155 | end 156 | 157 | it "generate error with no username" do 158 | subject = build_twitter_record true 159 | subject.twitter_username = '' 160 | _(subject.valid?).must_equal(false) 161 | _(subject.errors.size).must_equal(1) 162 | end 163 | 164 | it "generate error with invalid character" do 165 | subject = build_twitter_record true 166 | subject.twitter_username = 'érik' 167 | _(subject.valid?).must_equal(false) 168 | _(subject.errors.size).must_equal(1) 169 | end 170 | 171 | it "generate error with at sign character" do 172 | subject = build_twitter_record true 173 | subject.twitter_username = '@garrettb' 174 | _(subject.valid?).must_equal(false) 175 | _(subject.errors.size).must_equal(1) 176 | end 177 | 178 | it "generate error with at injected data" do 179 | subject = build_twitter_record true 180 | subject.twitter_username = "something\ngarrettb\nelse" 181 | _(subject.valid?).must_equal(false) 182 | _(subject.errors.size).must_equal(1) 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveValidators [![CircleCI](https://circleci.com/gh/franckverrot/activevalidators.svg?style=svg)](https://circleci.com/gh/franckverrot/activevalidators) 2 | 3 | # Description 4 | 5 | ActiveValidators is a collection of off-the-shelf and tested ActiveModel/ActiveRecord validations. 6 | 7 | ## Verify authenticity of this gem 8 | 9 | ActiveValidators is cryptographically signed. Please make sure the gem you install hasn’t been tampered with. 10 | 11 | Add my public key (if you haven’t already) as a trusted certificate: 12 | 13 | gem cert --add <(curl -Ls https://raw.githubusercontent.com/franckverrot/activevalidators/master/certs/franckverrot.pem) 14 | 15 | gem install activevalidators -P MediumSecurity 16 | 17 | The MediumSecurity trust profile will verify signed gems, but allow the installation of unsigned dependencies. 18 | 19 | This is necessary because not all of ActiveValidators’ dependencies are signed, so we cannot use HighSecurity. 20 | 21 | ## Requirements 22 | 23 | * Rails 5.1+ 24 | * Ruby 2.4+ 25 | 26 | ## Installation 27 | 28 | gem install activevalidators 29 | 30 | This projects follows [Semantic Versioning a.k.a SemVer](http://semver.org). If you use Bundler, you can use the stabby specifier `~>` safely. 31 | 32 | What it means is that you should specify an ActiveValidators version like this : 33 | 34 | ```ruby 35 | gem 'activevalidators', '~> 5.1.0' # <-- mind the patch version 36 | ``` 37 | 38 | Once you have `require`'d the gem, you will have to activate the validators you 39 | want to use as ActiveValidators doesn't force you to use them all : 40 | 41 | ```ruby 42 | # Activate all the validators 43 | ActiveValidators.activate(:all) 44 | 45 | # Activate only the email and slug validators 46 | ActiveValidators.activate(:email, :slug) 47 | 48 | # Activate only the phone 49 | ActiveValidators.activate(:phone) 50 | ``` 51 | 52 | `ActiveValidators.activate` can be called as many times as one wants. It's only 53 | a syntactic sugar on top a normal Ruby `require`. 54 | 55 | In a standard Ruby on Rails application, this line goes either in an initializer 56 | (`config/initializers/active_validators_activation.rb` for example), or directly 57 | within `config/application` right inside your `MyApp::Application` class definition. 58 | 59 | ## Usage 60 | 61 | In your models, the gem provides new validators like `email`, or `url`: 62 | 63 | ```ruby 64 | class User 65 | validates :company_siren, :siren => true 66 | validates :email_address, :email => true # == :email => { :strict => false } 67 | validates :link_url, :url => true # Could be combined with `allow_blank: true` 68 | validates :password, :password => { :strength => :medium } 69 | validates :postal_code, :postal_code => { :country => :us } 70 | validates :twitter, :twitter => true 71 | validates :twitter_at, :twitter => { :format => :username_with_at } 72 | validates :twitter_url, :twitter => { :format => :url } 73 | validates :user_phone, :phone => true 74 | end 75 | 76 | class Identification 77 | validates :nino, :nino => true 78 | validates :sin, :sin => true 79 | validates :ssn, :ssn => true 80 | end 81 | 82 | class Article 83 | validates :slug, :slug => true 84 | validates :expiration_date, :date => { 85 | :after => -> (record) { Time.now }, 86 | :before => -> (record) { Time.now + 1.year } 87 | } 88 | end 89 | 90 | class Device 91 | validates :ipv4, :ip => { :format => :v4 } 92 | validates :ipv6, :ip => { :format => :v6 } 93 | end 94 | 95 | class Account 96 | validates :any_card, :credit_card => true 97 | validates :visa_card, :credit_card => { :type => :visa } 98 | validates :credit_card, :credit_card => { :type => :any } 99 | validates :supported_card, :credit_card => { :type => [:visa, :master_card, :amex] } 100 | end 101 | 102 | class Order 103 | validates :tracking_num, :tracking_number => { :carrier => :ups } 104 | end 105 | 106 | class Product 107 | validates :code, :barcode => { :format => :ean13 } 108 | end 109 | ``` 110 | 111 | Exhaustive list of supported validators and their implementation: 112 | 113 | * `barcode` : based on known formats (:ean13 only for now) 114 | * `credit_card` : based on the [`credit_card_validations`](https://github.com/Fivell/credit_card_validations) gem 115 | * `date` : based on the [`date_validator`](https://github.com/codegram/date_validator) gem 116 | * `email` : based on the [`mail`](https://github.com/mikel/mail) gem 117 | * `hex_color` : based on a regular expression 118 | * `ip` : based on `Resolv::IPv[4|6]::Regex` 119 | * `nino` : National Insurance number (only for UK). Please note that this validation will not accept temporary (such as 63T12345) or administrative numbers (with prefixes like OO, CR, FY, MW, NC, PP, PY, PZ). 120 | * `password` : based on a set of regular expressions 121 | * `phone` : based on a set of predefined masks 122 | * `postal_code`: based on a set of predefined masks 123 | * `regexp` : uses Ruby's [`Regexp.compile`](http://www.ruby-doc.org/core-2.1.1/Regexp.html#method-c-new) method 124 | * `respond_to` : generic Ruby `respond_to` 125 | * `siren` : [SIREN](http://fr.wikipedia.org/wiki/SIREN) company numbers in France 126 | * `slug` : based on `ActiveSupport::String#parameterize` 127 | * `sin` : Social Insurance Number (only for Canada). You may also allow permanent resident cards (such cards start with '9') or business numbers (such numbers start with '8'): `:sin => {:country => :canada, :country_options => {allow_permanent_residents: true, allow_business_numbers: true}}` 128 | * `ssn` : Social Security Number (only for USA). 129 | * `tracking_number`: based on a set of predefined masks 130 | * `twitter` : based on a regular expression 131 | * `url` : based on a regular expression 132 | 133 | 134 | ### Handling error messages 135 | 136 | The validators rely on ActiveModel validations, and will require one to use its i18n-based mechanism. Here is a basic example: 137 | 138 | ```ruby 139 | # user.rb 140 | 141 | class User < ActiveRecord::Base 142 | validates :email, email: {message: :bad_email} 143 | end 144 | ``` 145 | 146 | ```yaml 147 | # en.yml 148 | 149 | en: 150 | activerecord: 151 | errors: 152 | models: 153 | user: 154 | attributes: 155 | email: 156 | bad_email: "your error message" 157 | ``` 158 | 159 | ## Todo 160 | 161 | Lots of improvements can be made: 162 | 163 | * Implement new validators 164 | * ... 165 | 166 | ## Note on Patches/Pull Requests 167 | 168 | * Fork the project. 169 | * Make your feature addition or bug fix. 170 | * Add tests for it. This is important so I don't break it in a 171 | future version unintentionally. 172 | * Commit, do not mess with rakefile, version, or history. 173 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 174 | * Send me a pull request. Bonus points for topic branches. 175 | 176 | 177 | ## Contributors 178 | 179 | Please checkout [AUTHORS.md](authors) to see who contributed. Get involved! 180 | 181 | 182 | ## Copyright 183 | 184 | Copyright (c) 2010-2018 Franck Verrot. MIT LICENSE. See LICENSE for details. 185 | 186 | [authors]: https://github.com/franckverrot/activevalidators/blob/master/AUTHORS.md 187 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # UNRELEASED 2 | 3 | # 6.x.y 4 | 5 | ## BREAKING CHANGES 6 | 7 | ## DEPRECATION 8 | 9 | ## CHANGES 10 | 11 | 12 | # 6.0.0 13 | 14 | ## BREAKING CHANGES 15 | 16 | ## DEPRECATION 17 | 18 | ## CHANGES 19 | 20 | * Add support for Rails 6 21 | * Add support for Ruby 2.7 22 | 23 | 24 | # 5.1.0 25 | 26 | ## BREAKING CHANGES 27 | 28 | ## DEPRECATION 29 | 30 | ## CHANGES 31 | 32 | * Ditch Travis CI in favor or CircleCI 33 | * Disallow business numbers from SIN validation 34 | 35 | 36 | # 5.0.1 37 | 38 | ## CHANGES 39 | 40 | * Unlock Countries gem v3.x 41 | 42 | # 5.0.0 43 | 44 | ## BREAKING CHANGES 45 | 46 | * Supports only Rails 5.1+ 47 | * Stops supporting Rubinius 48 | * Stops supporting JRuby 49 | 50 | ## CHANGES 51 | 52 | * The SIN validator now rejected zero-ed SINs 53 | 54 | 55 | # 4.1.0 56 | 57 | ## CHANGES 58 | 59 | * Relax version constraints around the `countries` gem 60 | 61 | 62 | 63 | # 4.0.3 64 | 65 | ## CHANGES 66 | 67 | * UrlValidator: add feature to support TLD validation 68 | 69 | 70 | # 4.0.2 71 | 72 | ## CHANGES 73 | 74 | * Ensure NINO's prefixes are correct (@alyssais). 75 | 76 | 77 | # 4.0.1 78 | 79 | ## CHANGES 80 | 81 | * Handle integer postal codes (@penman) 82 | 83 | 84 | # 4.0.0 (Rails 5 support, major update!) 85 | 86 | ## BREAKING CHANGES 87 | 88 | * Added support for Rails 5! Big thanks to Ross Penman! (@penman) :tada: 89 | * Dropped support for Ruby < 2.2.2 90 | * Unping most dependencies 91 | 92 | 93 | # 3.3.0 94 | 95 | ## DEPRECATION 96 | 97 | * Credit Cards: `carte_blanche` is going away, please use `dinners` instead 98 | 99 | ## CHANGES 100 | 101 | * Postal code validator: Make country case-insensitive for postal codes 102 | * URL validator: Remove obsolete usage of URI.regexp 103 | * CC validator: Use the `credit_card_validations` gem for CC validations 104 | * README: Details about activating AV w/ Rails 105 | * Spec: Links to dependency gems for convenience; sorting ; code :sparkles:. 106 | * General: Loosen up the dependency on countries 107 | 108 | 109 | # 3.2.0 110 | 111 | ## MAJOR CHANGES 112 | 113 | * Ensure the $LOAD\_PATH could never break ActiveValidators (see #71 for details) 114 | 115 | ## DEPRECATION 116 | 117 | * [SSN validations doesn't support any options anymore][ssn_validation_options] 118 | 119 | ## FEATURES 120 | 121 | * Regexp validator. 122 | 123 | # 3.1.0 124 | 125 | ## FEATURES 126 | 127 | * Remove default requiring 128 | * Remove deprecation messages about `ActiveValidators.activate` 129 | 130 | # 3.0.1 (yanked, it's now 3.1.0) 131 | 132 | # 3.0.0 133 | 134 | *TL;DR : deprecating default require's and introduced a way to activate them independently.* 135 | 136 | ## BREAKING CHANGES for the 3.x versions 137 | 138 | * Validators should be manually required using `ActiveValidators.activate`. See README for details on how to use it. 139 | 140 | ## FEATURES 141 | 142 | * Require all the validators by default until 3.0.1 143 | * EAN13 barcode validation 144 | * NINO validation (UK) 145 | * SIN validation (Canada) 146 | * SSN (Social Security Number) validation (USA) 147 | 148 | # 2.1.0 149 | 150 | * Loosen up the dependency on phony 151 | * Cleaning up the test helper 152 | * Some requires were obsolete and loading nothing. 153 | * .travis.yml : Rework the list of supported Rubies 154 | * Credit\_card\_validator : Change the luhn algorithm & code cleanup 155 | * Email\_validator.rb : Extract method in the email validator 156 | * Update .travis.yml 157 | * Remove Rubies < 1.9.3 158 | * Add activesupport requires needed to make tests pass 159 | * Add tests for IPAddr 160 | * Cast ip value to string before using string methods 161 | * Countries (from 0.8.4 to 0.9.2), phony (from 1.7.12 to 1.9.0) 162 | * Fix: :rubygems is deprecated because HTTP requests are insecure 163 | * SIREN Validator 164 | 165 | # 2.0.2 166 | 167 | * Postal code for the Cayman Islands 168 | * Add option to email validator to allow quick validation via a lambda function 169 | * Rework the postal code validator to support 1.8 again. 170 | * Convert to 1.8 syntax and reindent. 171 | 172 | # 2.0.1 173 | 174 | * Replace ^,$ with \A,\z in TrackingNumberValidator 175 | * Like the fixes before, including all tests. 176 | * Prevent string injection in postal codes via \A,\z 177 | * Just corrected the regular expression to use \A and \z instead of ^ and $. 178 | * Ensure to use \A and \z in twitter regexps 179 | * Regular expressions for the twitter usernames. 180 | * Wrap URL regexp with \A and \z 181 | * Also added a test case to ensure this is not possible. 182 | 183 | # 2.0.0 184 | 185 | * Clean a bit the email validations' tests. 186 | * Fix: phone validator accepts custom message 187 | * Email validator accept only full address 188 | * :strict changed to :only\_address due to :strict is registered word 189 | * Fixed travis for 1.9x 190 | * Fixes phone validator 191 | * Tests for international format 192 | * Phone validation dependency on Phony gem 193 | * Transform @ as word character 194 | * Added postal code validators by geonames.info 195 | * Replace custom url regexp with URI.regexp 196 | 197 | # 1.9.0 198 | 199 | * Update the email validation example 200 | * (Feature) Added strict email notion for email\_validator 201 | * Uenamed duplicate test cases 202 | * Umprove the email validator to be more restrictive 203 | * Update .travis.yml 204 | 205 | # 1.8.1 206 | 207 | * Remove active\_record dependency 208 | * Add Manuel to the list of contributors 209 | * Improve the README a bit 210 | 211 | # 1.8.0 212 | 213 | * TrackingNumberValidator: fix in the USPS computation. 214 | * Tests: removed turn because it was failing everywhere but on 1.9.x. 215 | * PostalCodeValidator: Added Portuguese postal-code format. 216 | * Remove turn. 217 | * Corrected example of use of the postal code validator. 218 | * Added a new option country\_method allowing the country to be obtained by calling a method of the record. Also added support for string in option 'contry'. 219 | * Added Portuguese postal-code format. 220 | * Properly handle checksums of '0' in usps mod10. 221 | * Current implementation returns 10, which does not equal 0. 222 | * Simplify the test and make sure Ruby 1.9x stop complaining 223 | 224 | # 1.7.1 225 | 226 | * Fix the usage of date\_validator 227 | 228 | # 1.7.0 229 | 230 | * Update the URL regex to support Basic Auth and port numbers 231 | * check value#blank? in validations 232 | * Fix the link to Travis 233 | * Implemented :credit\_card => true 234 | * Github's caching the build status image, so using https from now on. 235 | * Let's require all the validations 236 | 237 | # 1.6.0 238 | 239 | * Added two new contributors 240 | * Merged @utahstreetlabs' work on the tracking number validator. 241 | * Added RBX, RBX 2.0 and JRuby to the build matrix 242 | * We don't need to require the whole path here 243 | * Silence some warnings, as we run with Ruby with the -w flag. 244 | * Add 1.9.3 to the build matrix 245 | * Dropped RSpec in favor of MiniTest 246 | * Added Travis' build status logo. 247 | * Added .travis.yml for testing against multiple Ruby VMs 248 | * Added rake as a dependency 249 | 250 | # 1.5.1 251 | 252 | * Fix UPS tracking number's validation 253 | 254 | # 1.5 255 | 256 | * Fix copy-and-paste error with tracking number formats 257 | * Add ups tracking number validation 258 | 259 | # 1.4.0 260 | 261 | * Add Renato and Brian in both README and Gemspec 262 | * Feature: postal\_code validation is now available. 263 | * Fixed Ruby 1.8.7 support 264 | * Remove and ignore Gemfile.lock 265 | * Add postal code validator 266 | * Based on the phone validator, the only country it knows about is :us 267 | * Implemented 'old-school' validator methods dynamically 268 | * DRYed validator loading 269 | * Fix 1.9 compatiblity 270 | * Bump up date\_validator, version number and dependencies in the Gemfile 271 | * Refactored phone validator 272 | * ActiveValidators now supports Twitter urls (both URLs and usernames with @ 273 | * Added twitter username validator 274 | 275 | # 1.2.3 276 | 277 | * Added :blank error message 278 | * Add spec for empty slug 279 | * Fixed NoMethodError when the slug isnt set quite yet, but will still fail validation 280 | 281 | # 1.2.2 282 | 283 | * Added a password validator (based on regexes) 284 | * Added the contributor section in the README 285 | * The Luhn algorithm has been implemented, so no need for the Luhnacy gem 286 | 287 | # 1.2.1 288 | 289 | * Fix bug in Mail Validator when a complete email address was given 290 | * Add spec for https urls 291 | * Refactor URL Validator specs 292 | * Refactor Slug Validator specs 293 | * Refactor Respond To Validator specs 294 | * Refactor Phone Validator specs 295 | * Refactor Email Validator specs 296 | * Refactor Credit Card specs 297 | * Use generic TestRecord for IPValidator 298 | * Add generic TestRecord 299 | * Add validity check standard to IPValidator and refactor 300 | * Fix IPv4 validation for all rubies 301 | * Ignore rbx files 302 | 303 | # 1.2.0 304 | 305 | * Added the test file from DateValidator to make sure that the tests passes, or that at least the validations are available thru ActiveValidator 306 | * Added date\_validator in order to support date validation 307 | * Drop Luhnacy, very simple implementation, supports lots of cards. 308 | * Add credit card validation for American Express, Visa, Switch, and MasterCard 309 | * Added IP validators 310 | 311 | # 1.1.0 312 | 313 | * Add errors with the ActiveModel::Errors#add method -> i18n support 314 | * Add phone format validator 315 | * Public release 316 | 317 | 318 | [ssn_validation_options]: https://github.com/franckverrot/activevalidators/commit/dee076a8e344897f0325747504625285be7da226 319 | --------------------------------------------------------------------------------