├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── features ├── errors.feature ├── rules.feature ├── step_definitions │ ├── errors.rb │ └── rules.rb └── support │ └── env.rb ├── lib ├── validation.rb └── validation │ ├── rule │ ├── email.rb │ ├── length.rb │ ├── matches.rb │ ├── not_empty.rb │ ├── numeric.rb │ ├── phone.rb │ ├── regular_expression.rb │ ├── uri.rb │ └── uuid.rb │ ├── validator.rb │ └── version.rb ├── spec ├── self_contained_validator_spec.rb ├── spec_helper.rb └── validation │ ├── rule │ ├── email_spec.rb │ ├── length_spec.rb │ ├── matches_spec.rb │ ├── not_empty_spec.rb │ ├── numeric_spec.rb │ ├── phone_spec.rb │ ├── regular_expression_spec.rb │ ├── uri_spec.rb │ └── uuid_spec.rb │ └── validator_spec.rb └── valid.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.gem 3 | 4 | # IDE files 5 | .idea 6 | 7 | # documentation 8 | doc 9 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | lannguage: ruby 2 | rvm: 3 | - 1.9.2 4 | - 1.9.3 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :test do 4 | gem 'rake', '0.9.2.2' 5 | gem 'rspec', '3.4.0' 6 | end 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | diff-lcs (1.2.5) 5 | rake (0.9.2.2) 6 | rspec (3.4.0) 7 | rspec-core (~> 3.4.0) 8 | rspec-expectations (~> 3.4.0) 9 | rspec-mocks (~> 3.4.0) 10 | rspec-core (3.4.4) 11 | rspec-support (~> 3.4.0) 12 | rspec-expectations (3.4.0) 13 | diff-lcs (>= 1.2.0, < 2.0) 14 | rspec-support (~> 3.4.0) 15 | rspec-mocks (3.4.1) 16 | diff-lcs (>= 1.2.0, < 2.0) 17 | rspec-support (~> 3.4.0) 18 | rspec-support (3.4.1) 19 | 20 | PLATFORMS 21 | ruby 22 | 23 | DEPENDENCIES 24 | rake (= 0.9.2.2) 25 | rspec (= 3.4.0) 26 | 27 | BUNDLED WITH 28 | 1.12.5 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Jeremy Bush 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Validator 2 | 3 | [![Build Status](https://secure.travis-ci.org/zombor/Validator.png)](http://travis-ci.org/zombor/Validator) 4 | 5 | Validator is a simple ruby validation class. You don't use it directly inside your classes like just about every other ruby validation class out there. I chose to implement it in this way so I didn't automatically pollute the namespace of the objects I wanted to validate. 6 | 7 | This also solves the problem of validating forms very nicely. Frequently you will have a form that represents many different data objects in your system, and you can pre-validate everything before doing any saving. 8 | 9 | ## Usage 10 | 11 | Validator is useful for validating the state of any existing ruby object. 12 | 13 | ```ruby 14 | object = OpenStruct.new(:email => 'foo@bar.com', :password => 'foobar') 15 | validator = Validation::Validator.new(object) 16 | validator.rule(:email, [:email, :not_empty]) # multiple rules in one line 17 | validator.rule(:password, :not_empty) # a single rule on a line 18 | validator.rule(:password, :length => { :minimum => 3 }) # a rule that takes parameters 19 | 20 | if validator.valid? 21 | # save the data somewhere 22 | else 23 | @errors = validator.errors 24 | end 25 | ``` 26 | 27 | The first paramater can be any message that the object responds to. 28 | 29 | ### Writing your own rules 30 | 31 | If you have a custom rule you need to write, you can create a custom rule class for it: 32 | 33 | ```ruby 34 | class MyCustomRule 35 | def error_key 36 | :my_custom_rule 37 | end 38 | 39 | def valid_value?(value) 40 | # Logic for determining the validity of the value 41 | end 42 | 43 | def params 44 | {} 45 | end 46 | end 47 | ``` 48 | 49 | A rule class should have the following methods on it: 50 | 51 | - `error_key` a symbol to represent the error. This shows up in the errors hash. Must be an underscored_version of the class name 52 | - `valid_value?(value)` the beef of the rule. This is where you determine if the value is valid or not 53 | - `params` the params hash that was passed into the constructor 54 | 55 | If you add your custom rule class to the `Validation::Rule` namespace, you can reference it using a symbol: 56 | 57 | ```ruby 58 | validator.rule(:field, :my_custom_rule) # resolves to Validation::Rule::MyCustomRule 59 | validator.rule(:field, :my_custom_rule => { :param => :value }) 60 | ``` 61 | 62 | Otherwise, just pass in the rule class itself: 63 | 64 | ```ruby 65 | validator.rule(:field, MyProject::CustomRule) 66 | validator.rule(:field, MyProject::CustomRule => { :param => :value }) 67 | ``` 68 | 69 | ### Writing self-contained validators 70 | 71 | You can also create self-contained validation classes if you don't like the dynamic creation approach: 72 | 73 | ```ruby 74 | require 'validation' 75 | require 'validation/rule/not_empty' 76 | 77 | class MyFormValidator < Validation::Validator 78 | include Validation 79 | 80 | rule :email, :not_empty 81 | end 82 | ``` 83 | 84 | Now you can use this anywhere in your code: 85 | 86 | ```ruby 87 | form_validator = MyFormValidator.new(OpenStruct.new(params)) 88 | form_validator.valid? 89 | ``` 90 | 91 | # Semantic Versioning 92 | 93 | This project conforms to [semver](http://semver.org/). 94 | 95 | # Contributing 96 | 97 | Have an improvement? Have an awesome rule you want included? Simple! 98 | 99 | 1. Fork the repository 100 | 2. Create a branch off of the `master` branch 101 | 3. Write specs for the change 102 | 4. Add your change 103 | 5. Submit a pull request to merge against the `master` branch 104 | 105 | Don't change any version files or gemspec files in your change. 106 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | task :default => :spec 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | -------------------------------------------------------------------------------- /features/errors.feature: -------------------------------------------------------------------------------- 1 | Feature: Errors 2 | 3 | Background: 4 | Given I have a validation object with the following data: 5 | | id | 1 | 6 | | email | foo@bar.com | 7 | | password | foobar | 8 | | first_name | | 9 | | last_name | bar | 10 | 11 | Scenario: Errors are empty when the validation object is valid 12 | When I add a "not_empty" rule for the "email" field 13 | Then the errors should be empty 14 | 15 | Scenario: Errors are not empty when the validation object is invalid 16 | When I add a "not_empty" rule for the "first_name" field 17 | Then the errors should contain: 18 | | first_name | {:rule=>:not_empty, :params=>{}} | 19 | -------------------------------------------------------------------------------- /features/rules.feature: -------------------------------------------------------------------------------- 1 | Feature: Rules 2 | 3 | Background: 4 | Given I have a validation object with the following data: 5 | | id | 1 | 6 | | email | foo@bar.com | 7 | | password | foobar | 8 | | first_name | | 9 | | last_name | bar | 10 | 11 | Scenario: Passes validation when a rule passes 12 | When I add a "not_empty" rule for the "email" field 13 | Then the validation object should be valid 14 | 15 | Scenario: Passes validation when multiple rules pass 16 | When I add the following rules: 17 | | email | not_empty | | 18 | | password | length | {:minimum => 3} | 19 | Then the validation object should be valid 20 | 21 | Scenario: Fails validation when a rule fails 22 | When I add a "not_empty" rule for the "first_name" field 23 | Then the validation object should be invalid 24 | 25 | Scenario: Fails validation when a rule fails and others pass 26 | When I add a "not_empty" rule for the "first_name" field 27 | And I add a "not_empty" rule for the "last_name" field 28 | Then the validation object should be invalid 29 | -------------------------------------------------------------------------------- /features/step_definitions/errors.rb: -------------------------------------------------------------------------------- 1 | Then /^the errors should be empty$/ do 2 | @validator.valid? 3 | @validator.errors.should be_empty 4 | end 5 | 6 | Then /^the errors should contain:$/ do |table| 7 | @validator.valid? 8 | table.raw.each do |line| 9 | @validator.errors[line[0].to_sym].should == eval(line[1]) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /features/step_definitions/rules.rb: -------------------------------------------------------------------------------- 1 | Given /^I have a validation object with the following data:$/ do |table| 2 | data = {} 3 | table.raw.each do |key, value| 4 | data[key] = value 5 | end 6 | @validator = Validation::Validator.new(OpenStruct.new(data)) 7 | end 8 | 9 | When /^I add a "([^"]*)" rule for the "([^"]*)" field$/ do |rule, field| 10 | @validator.rule(field.to_sym, rule.to_sym) 11 | end 12 | 13 | When /^I add the following rules:$/ do |table| 14 | table.raw.each do |row| 15 | params = eval(row[2]) 16 | if params.nil? 17 | @validator.rule(row[0], row[1]) 18 | else 19 | @validator.rule(row[0], row[1] => params) 20 | end 21 | end 22 | end 23 | 24 | Then /^the validation object should be valid$/ do 25 | @validator.valid?.should be_true 26 | end 27 | 28 | Then /^the validation object should be invalid$/ do 29 | @validator.valid?.should be_false 30 | end 31 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << './lib' 2 | 3 | require 'validation' 4 | require 'validation/rule/not_empty' 5 | require 'validation/rule/length' 6 | -------------------------------------------------------------------------------- /lib/validation.rb: -------------------------------------------------------------------------------- 1 | require 'validation/validator' 2 | 3 | module Validation 4 | 5 | class << self 6 | private 7 | 8 | def included(mod) 9 | mod.module_eval do 10 | extend Validation::Rules 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/validation/rule/email.rb: -------------------------------------------------------------------------------- 1 | module Validation 2 | module Rule 3 | # Email rule class. This rule was adapted from https://github.com/emmanuel/aequitas/blob/master/lib/aequitas/rule/format/email_address.rb 4 | 5 | class Email 6 | EMAIL_ADDRESS = begin 7 | letter = 'a-zA-Z' 8 | digit = '0-9' 9 | atext = "[#{letter}#{digit}\!\#\$\%\&\'\*+\/\=\?\^\_\`\{\|\}\~\-]" 10 | dot_atom_text = "#{atext}+([.]#{atext}*)+" 11 | dot_atom = dot_atom_text 12 | no_ws_ctl = '\x01-\x08\x11\x12\x14-\x1f\x7f' 13 | qtext = "[^#{no_ws_ctl}\\x0d\\x22\\x5c]" # Non-whitespace, non-control character except for \ and " 14 | text = '[\x01-\x09\x11\x12\x14-\x7f]' 15 | quoted_pair = "(\\x5c#{text})" 16 | qcontent = "(?:#{qtext}|#{quoted_pair})" 17 | quoted_string = "[\"]#{qcontent}+[\"]" 18 | atom = "#{atext}+" 19 | word = "(?:#{atom}|#{quoted_string})" 20 | obs_local_part = "#{word}([.]#{word})*" 21 | local_part = "(?:#{dot_atom}|#{quoted_string}|#{obs_local_part})" 22 | dtext = "[#{no_ws_ctl}\\x21-\\x5a\\x5e-\\x7e]" 23 | dcontent = "(?:#{dtext}|#{quoted_pair})" 24 | domain_literal = "\\[#{dcontent}+\\]" 25 | obs_domain = "#{atom}([.]#{atom})+" 26 | domain = "(?:#{dot_atom}|#{domain_literal}|#{obs_domain})" 27 | addr_spec = "#{local_part}\@#{domain}" 28 | pattern = /\A#{addr_spec}\z/u 29 | end 30 | 31 | # Determines if value is a valid email 32 | def valid_value?(value) 33 | !!EMAIL_ADDRESS.match(value) 34 | end 35 | 36 | # The error key for this rule 37 | def error_key 38 | :email 39 | end 40 | 41 | # This rule has no params 42 | def params 43 | {} 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/validation/rule/length.rb: -------------------------------------------------------------------------------- 1 | module Validation 2 | module Rule 3 | # Length rule 4 | class Length 5 | # params can be any of the following: 6 | # 7 | # - :minimum - at least this many chars 8 | # - :maximum - at most this many chars 9 | # - :exact - exactly this many chars 10 | # 11 | # Example: 12 | # 13 | # {:minimum => 3, :maximum => 10} 14 | # {:exact => 10} 15 | def initialize(params) 16 | @params = params 17 | end 18 | 19 | # returns the params given in the constructor 20 | def params 21 | @params 22 | end 23 | 24 | # determines if value is valid according to the constructor params 25 | def valid_value?(value) 26 | @params.each_pair do |key, param| 27 | return false if key == :minimum && (value.nil? || value.length < param) 28 | return false if key == :maximum && !value.nil? && value.length > param 29 | return false if key == :exact && (value.nil? || value.length != param) 30 | end 31 | 32 | true 33 | end 34 | 35 | def error_key 36 | :length 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/validation/rule/matches.rb: -------------------------------------------------------------------------------- 1 | module Validation 2 | module Rule 3 | # Matches rule 4 | class Matches 5 | attr_writer :obj 6 | 7 | # This class should take the field to match with in the constructor: 8 | # 9 | # rule = Validation::Rule::Matches(:password) 10 | # rule.obj = OpenStruct.new(:password => 'foo') 11 | # rule.valid_value?('foo') 12 | def initialize(matcher_field) 13 | @matcher_field = matcher_field 14 | end 15 | 16 | # The error key for this rule 17 | def error_key 18 | :matches 19 | end 20 | 21 | # Params is the matcher_field given in the constructor 22 | def params 23 | @matcher_field 24 | end 25 | 26 | # Determines if value matches the field given in the constructor 27 | def valid_value?(value) 28 | matcher_value = @obj.send(@matcher_field) 29 | matcher_value == value 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/validation/rule/not_empty.rb: -------------------------------------------------------------------------------- 1 | module Validation 2 | module Rule 3 | # Rule for not empty 4 | class NotEmpty 5 | # This rule has no params 6 | def params 7 | {} 8 | end 9 | 10 | # Determines if value is empty or not. In this rule, nil is empty 11 | def valid_value?(value) 12 | ! (value.nil? || value.empty?) 13 | end 14 | 15 | # The error key for this field 16 | def error_key 17 | :not_empty 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/validation/rule/numeric.rb: -------------------------------------------------------------------------------- 1 | module Validation 2 | module Rule 3 | # rule for numeric values 4 | class Numeric 5 | # Determines if value is numeric. It can only contain whole numbers 6 | def valid_value?(value) 7 | !!/^[0-9]+$/.match(value.to_s) 8 | end 9 | 10 | # The error key for this rule 11 | def error_key 12 | :numeric 13 | end 14 | 15 | # this rule has no params 16 | def params 17 | {} 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/validation/rule/phone.rb: -------------------------------------------------------------------------------- 1 | module Validation 2 | module Rule 3 | # Phone rule 4 | class Phone 5 | # params can be any of the following: 6 | # 7 | # - :format - the phone number format 8 | # 9 | # Example: 10 | # 11 | # {:format => :america} 12 | def initialize(params = {:format => :america}) 13 | @params = params 14 | end 15 | 16 | # returns the params given in the constructor 17 | def params 18 | @params 19 | end 20 | 21 | # determines if value is valid according to the constructor params 22 | def valid_value?(value) 23 | send(@params[:format], value) 24 | end 25 | 26 | def error_key 27 | :phone 28 | end 29 | 30 | protected 31 | 32 | def america(value) 33 | digits = value.gsub(/\D/, '').split(//) 34 | 35 | digits.length == 10 || digits.length == 11 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/validation/rule/regular_expression.rb: -------------------------------------------------------------------------------- 1 | module Validation 2 | module Rule 3 | class RegularExpression 4 | 5 | def initialize(params) 6 | @params = params 7 | end 8 | 9 | def error_key 10 | :regular_expression 11 | end 12 | 13 | def valid_value?(value) 14 | value.nil? || !!@params[:regex].match(value) 15 | end 16 | 17 | def params 18 | @params 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/validation/rule/uri.rb: -------------------------------------------------------------------------------- 1 | module Validation 2 | module Rule 3 | class URI 4 | def initialize(parts=[:host]) 5 | @required_parts = parts 6 | end 7 | 8 | def error_key 9 | :uri 10 | end 11 | 12 | def params 13 | {:required_elements => @required_parts} 14 | end 15 | 16 | def valid_value?(uri_string) 17 | return true if uri_string.nil? 18 | 19 | uri = URI(uri_string) 20 | @required_parts.each do |part| 21 | if uri.send(part).nil? || uri.send(part).empty? 22 | return false 23 | end 24 | end 25 | true 26 | rescue ::URI::InvalidURIError 27 | return false 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/validation/rule/uuid.rb: -------------------------------------------------------------------------------- 1 | module Validation 2 | # UUID rule 3 | module Rule 4 | class Uuid 5 | class UnknownVersion < StandardError; end 6 | # params can be any of the following: 7 | # 8 | # - :version - v4 9 | # v5 10 | # uuid (Any valid uuid) 11 | # 12 | # Example: 13 | # 14 | # {:version => v4} 15 | def initialize(params) 16 | @params = params 17 | end 18 | 19 | def params 20 | @params 21 | end 22 | 23 | def valid_value?(value) 24 | value.nil? || !!uuid_regex.match(value.to_s) 25 | rescue UnknownVersion 26 | false 27 | end 28 | 29 | def error_key 30 | :uuid 31 | end 32 | 33 | private 34 | 35 | VERSION_REGEX = { 36 | 'uuid' => /^[0-9A-F]{8}-[0-9A-F]{4}-[1-5][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, 37 | 'v4' => /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, 38 | 'v5' => /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, 39 | } 40 | 41 | def uuid_regex 42 | VERSION_REGEX.fetch(params[:version]) { raise UnknownVersion } 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/validation/validator.rb: -------------------------------------------------------------------------------- 1 | module Validation 2 | module Rules 3 | # A hash of rules for this object 4 | def rules 5 | @rules ||= {} 6 | end 7 | 8 | # A hash of errors for this object 9 | def errors 10 | @errors ||= {} 11 | end 12 | 13 | # Define a rule for this object 14 | # 15 | # The rule parameter can be one of the following: 16 | # 17 | # * a symbol that matches to a class in the Validation::Rule namespace 18 | # * e.g. rule(:field, :not_empty) 19 | # * a hash containing the rule as the key and it's parameters as the values 20 | # * e.g. rule(:field, :length => { :minimum => 3, :maximum => 5 }) 21 | # * an array combining the two previous types 22 | def rule(field, definition) 23 | field = field.to_sym 24 | rules[field] = [] if rules[field].nil? 25 | 26 | begin 27 | if definition.respond_to?(:each_pair) 28 | add_parameterized_rules(field, definition) 29 | elsif definition.respond_to?(:each) 30 | definition.each do |item| 31 | if item.respond_to?(:each_pair) 32 | add_parameterized_rules(field, item) 33 | else 34 | add_single_rule(field, item) 35 | end 36 | end 37 | else 38 | add_single_rule(field, definition) 39 | end 40 | rescue NameError => e 41 | raise InvalidRule.new(e) 42 | end 43 | self 44 | end 45 | 46 | # Determines if this object is valid. When a rule fails for a field, 47 | # this will stop processing further rules. In this way, you'll only get 48 | # one error per field 49 | def valid? 50 | valid = true 51 | 52 | rules.each_pair do |field, rules| 53 | if ! @obj.respond_to?(field) 54 | raise InvalidKey, "cannot validate non-existent field '#{field}'" 55 | end 56 | 57 | rules.each do |r| 58 | if ! r.valid_value?(@obj.send(field)) 59 | valid = false 60 | errors[field] = {:rule => r.error_key, :params => r.params} 61 | break 62 | end 63 | end 64 | end 65 | 66 | @valid = valid 67 | end 68 | 69 | protected 70 | 71 | # Adds a single rule to this object 72 | def add_single_rule(field, key_or_klass, params = nil) 73 | klass = if key_or_klass.respond_to?(:new) 74 | key_or_klass 75 | else 76 | get_rule_class_by_name(key_or_klass) 77 | end 78 | 79 | args = [params].compact 80 | rule = klass.new(*args) 81 | rule.obj = @obj if rule.respond_to?(:obj=) 82 | rules[field] << rule 83 | end 84 | 85 | # Adds a set of parameterized rules to this object 86 | def add_parameterized_rules(field, rules) 87 | rules.each_pair do |key, params| 88 | add_single_rule(field, key, params) 89 | end 90 | end 91 | 92 | # Resolves the specified rule name to a rule class 93 | def get_rule_class_by_name(klass) 94 | klass = camelize(klass) 95 | Validation::Rule.const_get(klass) 96 | rescue NameError => e 97 | raise InvalidRule.new(e) 98 | end 99 | 100 | # Converts a symbol to a class name, taken from rails 101 | def camelize(term) 102 | string = term.to_s 103 | string = string.sub(/^[a-z\d]*/) { $&.capitalize } 104 | string.gsub(/(?:_|(\/))([a-z\d]*)/i) { $2.capitalize }.gsub('/', '::') 105 | end 106 | end 107 | 108 | # Validator is a simple ruby validation class. You don't use it directly 109 | # inside your classes like just about every other ruby validation class out 110 | # there. I chose to implement it in this way so I didn't automatically 111 | # pollute the namespace of the objects I wanted to validate. 112 | # 113 | # This also solves the problem of validating forms very nicely. Frequently 114 | # you will have a form that represents many different data objects in your 115 | # system, and you can pre-validate everything before doing any saving. 116 | class Validator 117 | include Validation::Rules 118 | 119 | def initialize(obj) 120 | @rules = self.class.rules if self.class.is_a?(Validation::Rules) 121 | @obj = obj 122 | end 123 | end 124 | 125 | # InvalidKey is raised if a rule is added to a field that doesn't exist 126 | class InvalidKey < RuntimeError 127 | end 128 | 129 | # InvalidRule is raised if a rule is added that doesn't exist 130 | class InvalidRule < RuntimeError 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/validation/version.rb: -------------------------------------------------------------------------------- 1 | module Validation 2 | VERSION = '1.2.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/self_contained_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'validation' 3 | require 'validation/rule/not_empty' 4 | require 'validation/rule/email' 5 | require 'validation/rule/length' 6 | require 'ostruct' 7 | 8 | class SelfContainedValidator < Validation::Validator 9 | include Validation 10 | 11 | rule :test_mail, :email 12 | rule :test_string, [:not_empty, 13 | :length => { :maximum => 5 }] 14 | end 15 | 16 | describe SelfContainedValidator do 17 | let(:success_data) { OpenStruct.new(:test_mail => 'test@email.com', :test_string => 'test') } 18 | let(:fail_data) { OpenStruct.new(:test_mail => 'not an email', :test_string => '') } 19 | 20 | context 'behaves like a validator' do 21 | subject { SelfContainedValidator.new(success_data) } 22 | 23 | it { is_expected.to respond_to('valid?') } 24 | it { is_expected.to respond_to(:errors) } 25 | end 26 | 27 | it 'passes validation for correct data' do 28 | foo = SelfContainedValidator.new(success_data) 29 | expect(foo).to be_valid 30 | expect(foo.errors).to be_empty 31 | end 32 | 33 | it 'fails validation for wrong data' do 34 | foo = SelfContainedValidator.new(fail_data) 35 | expect(foo).not_to be_valid 36 | expect(foo.errors).to include(:test_mail, :test_string) 37 | end 38 | 39 | context 'when adding new rules' do 40 | let(:data) { OpenStruct.new(:test_mail => 'test@email.com', :test_string => '') } 41 | subject { SelfContainedValidator.new(data) } 42 | before { subject.rule(:test_mail, :length => { :maximum => 3}) } 43 | 44 | it 'keeps the old rules' do 45 | expect(subject).not_to be_valid 46 | expect(subject.errors).to include(:test_string) 47 | end 48 | 49 | it 'validates both old and new rules' do 50 | expect(subject).not_to be_valid 51 | expect(subject.errors).to include(:test_mail) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | $LOAD_PATH << '../lib' 5 | -------------------------------------------------------------------------------- /spec/validation/rule/email_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'validation/rule/email' 3 | 4 | describe Validation::Rule::Email do 5 | it 'passes with a valid email' do 6 | expect(subject.valid_value?('foo@bar.com')).to eq(true) 7 | end 8 | 9 | it 'fails with an invalid email' do 10 | ['bad-email', '', nil].each do |value| 11 | expect(subject.valid_value?(value)).to eq(false) 12 | end 13 | end 14 | 15 | it 'has an error key' do 16 | expect(subject.error_key).to eq(:email) 17 | end 18 | 19 | it 'returns its parameters' do 20 | expect(subject.params).to eq({}) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/validation/rule/length_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'validation/rule/length' 3 | 4 | describe Validation::Rule::Length do 5 | subject { Validation::Rule::Length } 6 | 7 | it 'has an error key' do 8 | expect(subject.new('foo').error_key).to eq(:length) 9 | end 10 | 11 | it 'returns its parameters' do 12 | rule = subject.new(:minimum => 5) 13 | expect(rule.params).to eq(:minimum => 5) 14 | end 15 | 16 | context :minimum do 17 | let(:rule) { subject.new(:minimum => 5) } 18 | 19 | it 'does not allow nil' do 20 | expect(rule.valid_value?(nil)).to eq(false) 21 | end 22 | 23 | it 'is valid' do 24 | expect(rule.valid_value?('foobarbar')).to eq(true) 25 | end 26 | 27 | it 'is invalid' do 28 | expect(rule.valid_value?('foo')).to eq(false) 29 | end 30 | end 31 | 32 | context :maximum do 33 | let(:rule) { subject.new(:maximum => 5) } 34 | 35 | it 'allows nil' do 36 | expect(rule.valid_value?(nil)).to eq(true) 37 | end 38 | 39 | it 'is valid' do 40 | expect(rule.valid_value?('foo')).to eq(true) 41 | end 42 | 43 | it 'is invalid' do 44 | expect(rule.valid_value?('foobarbar')).to eq(false) 45 | end 46 | end 47 | 48 | context :exact do 49 | let(:rule) { subject.new(:exact => 5) } 50 | 51 | it 'does not allow nil' do 52 | expect(rule.valid_value?(nil)).to eq(false) 53 | end 54 | 55 | it 'is valid' do 56 | expect(rule.valid_value?('fooba')).to eq(true) 57 | end 58 | 59 | it 'is valid' do 60 | expect(rule.valid_value?('foobar')).to eq(false) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/validation/rule/matches_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ostruct' 3 | require 'validation/rule/matches' 4 | 5 | describe Validation::Rule::Matches do 6 | let(:field) { :password_repeat } 7 | let(:obj) { OpenStruct.new(:password => 'foo', :password_repeat => 'bar') } 8 | subject { Validation::Rule::Matches.new(field) } 9 | 10 | it 'has an error key' do 11 | expect(subject.error_key).to eq(:matches) 12 | end 13 | 14 | it 'returns its parameters' do 15 | expect(subject.params).to eq(field) 16 | end 17 | 18 | it 'accepts a data object' do 19 | expect { subject.obj = obj }.not_to raise_error 20 | end 21 | 22 | it 'passes on valid data' do 23 | subject.obj = obj 24 | expect(subject.valid_value?('bar')).to eq(true) 25 | end 26 | 27 | it 'fails on invalid data' do 28 | subject.obj = obj 29 | expect(subject.valid_value?('foo')).to eq(false) 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /spec/validation/rule/not_empty_spec.rb: -------------------------------------------------------------------------------- 1 | require 'validation/rule/not_empty' 2 | 3 | describe Validation::Rule::NotEmpty do 4 | it 'passes when a value exists' do 5 | expect(subject.valid_value?('foo')).to eq(true) 6 | end 7 | 8 | it 'fails when a value does not exist' do 9 | ['', nil].each do |value| 10 | expect(subject.valid_value?(value)).to eq(false) 11 | end 12 | end 13 | 14 | it 'has an error key' do 15 | expect(subject.error_key).to eq(:not_empty) 16 | end 17 | 18 | it 'returns its parameters' do 19 | expect(subject.params).to eq({}) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/validation/rule/numeric_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'validation/rule/numeric' 3 | 4 | describe Validation::Rule::Numeric do 5 | it 'passes when a value is numeric' do 6 | expect(subject.valid_value?(10)).to eq(true) 7 | end 8 | 9 | it 'fails when a value is not numeric' do 10 | ['', nil, 'foo', 10.5].each do |value| 11 | expect(subject.valid_value?(value)).to eq(false) 12 | end 13 | end 14 | 15 | it 'has an error key' do 16 | expect(subject.error_key).to eq(:numeric) 17 | end 18 | 19 | it 'returns its parameters' do 20 | expect(subject.params).to eq({}) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/validation/rule/phone_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'validation/rule/phone' 3 | 4 | describe Validation::Rule::Phone do 5 | subject { Validation::Rule::Phone } 6 | 7 | it 'has an error key' do 8 | expect(subject.new.error_key).to eq(:phone) 9 | end 10 | 11 | it 'defaults to america format' do 12 | expect(subject.new.params).to eq(:format => :america) 13 | end 14 | 15 | context :america do 16 | let(:rule) { subject.new } 17 | it 'is valid' do 18 | [ 19 | '1234567890', 20 | '11234567890' 21 | ].each do |phone| 22 | expect(rule.valid_value?(phone)).to eq(true) 23 | end 24 | end 25 | 26 | it 'is invalid' do 27 | [ 28 | 'asdfghjklp', 29 | '123456789', 30 | '123456789012' 31 | ].each do |phone| 32 | expect(rule.valid_value?(phone)).to eq(false) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/validation/rule/regular_expression_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'validation/rule/regular_expression' 3 | 4 | describe Validation::Rule::RegularExpression do 5 | subject { Validation::Rule::RegularExpression } 6 | 7 | it 'has an error key' do 8 | expect(subject.new('foo').error_key).to eq(:regular_expression) 9 | end 10 | 11 | it 'returns its parameters' do 12 | rule = subject.new(:regex => /\A.+\Z/) 13 | expect(rule.params).to eq(:regex => /\A.+\Z/) 14 | end 15 | 16 | context :regex do 17 | let(:rule) { subject.new(:regex => /\A[0-9]+\Z/) } 18 | 19 | it 'is valid' do 20 | expect(rule.valid_value?('0123456789')).to eq(true) 21 | end 22 | 23 | it 'is invalid' do 24 | expect(rule.valid_value?('a')).to eq(false) 25 | expect(rule.valid_value?('2b')).to eq(false) 26 | expect(rule.valid_value?('c3')).to eq(false) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/validation/rule/uri_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'validation/rule/uri' 3 | 4 | describe Validation::Rule::URI do 5 | subject { described_class.new } 6 | 7 | it 'has an error key' do 8 | expect(subject.error_key).to eq(:uri) 9 | end 10 | 11 | it 'passes when given a valid uri' do 12 | expect(subject.valid_value?('http://uri.com')).to eq(true) 13 | end 14 | 15 | it 'has params' do 16 | expect(subject.params).to eq(:required_elements => [:host]) 17 | end 18 | 19 | it 'passes with nil' do 20 | expect(subject.valid_value?(nil)).to eq(true) 21 | end 22 | 23 | it 'fails when given an invalid uri' do 24 | expect(subject.valid_value?('foo:/%urim')).to eq(false) 25 | end 26 | 27 | context "part validation" do 28 | it 'fails to validate when given a uri without a host' do 29 | expect(subject.valid_value?('http:foo@')).to eq(false) 30 | end 31 | 32 | it 'fails to validate when given a uri without a scheme' do 33 | described_class.new([:host, :scheme]) 34 | expect(subject.valid_value?('foo.com')).to eq(false) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/validation/rule/uuid_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'validation/rule/uuid' 3 | 4 | describe Validation::Rule::Uuid do 5 | params = { :version => 'uuid' } 6 | subject { described_class.new(params) } 7 | 8 | it 'has params' do 9 | expect(subject.params).to eq(params) 10 | end 11 | 12 | it 'has an error key' do 13 | expect(subject.error_key).to eq(:uuid) 14 | end 15 | 16 | it 'passes when given a valid uuid' do 17 | expect(subject.valid_value?("05369729-3e2d-4cc1-88ea-c7ad8665a5da")).to eq(true) 18 | end 19 | 20 | it 'passes when given a valid v4 uuid' do 21 | params = { :version => 'v4' } 22 | expect(subject.valid_value?("05369729-3e2d-4cc1-88ea-c7ad8665a5da")).to eq(true) 23 | end 24 | 25 | it 'passes when given a valid v5 uuid' do 26 | params = { :version => 'v5' } 27 | expect(subject.valid_value?("05369729-3e2d-5cc1-88ea-c7ad8665a5da")).to eq(true) 28 | end 29 | 30 | it 'fails when version does not match' do 31 | params = { :version => 'v4' } 32 | expect(subject.valid_value?("05369729-3e2d-5cc1-88ea-c7ad8665a5da")).to eq(false) 33 | end 34 | 35 | it 'fails when given an invalid uuid' do 36 | expect(subject.valid_value?('not-a-uuid')).to eq(false) 37 | end 38 | 39 | it 'fails when given a blank string' do 40 | expect(subject.valid_value?('')).to eq(false) 41 | end 42 | 43 | it 'fails when given a non-string' do 44 | expect(subject.valid_value?(5)).to eq(false) 45 | end 46 | 47 | it 'fails when given an unknown uuid version' do 48 | params = { :version => 'v6' } 49 | expect(subject.valid_value?("05369729-3e2d-4cc1-88ea-c7ad8665a5da")).to eq(false) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/validation/validator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'validation/validator' 3 | require 'ostruct' 4 | 5 | describe Validation::Validator do 6 | it 'accepts a plain ruby object' do 7 | validator = Validation::Validator.new(OpenStruct.new) 8 | end 9 | 10 | context :rules do 11 | let(:data_object) { OpenStruct.new(:id => 1, :email => 'foo@bar.com') } 12 | let(:rule_klass) { Validation::Rule::NotEmpty } 13 | 14 | subject { Validation::Validator.new(data_object) } 15 | 16 | it 'accepts a rule name' do 17 | subject.rule(:email, :not_empty) 18 | 19 | expect( 20 | subject.instance_variable_get(:@rules)[:email].map(&:class) 21 | ).to eq([Validation::Rule::NotEmpty]) 22 | end 23 | 24 | it 'accepts a rule class' do 25 | subject.rule(:email, rule_klass) 26 | 27 | expect( 28 | subject.instance_variable_get(:@rules)[:email].map(&:class) 29 | ).to eq([rule_klass]) 30 | end 31 | 32 | it 'accepts multiple rules for the same field' do 33 | stub_const("Validation::Rule::NotEmpty", not_empty_class = Class.new) 34 | stub_const("Validation::Rule::Length", length_class = Class.new) 35 | subject.rule(:email, [:not_empty, :length]) 36 | 37 | expect( 38 | subject.instance_variable_get(:@rules)[:email].map(&:class) 39 | ).to eq([not_empty_class, length_class]) 40 | end 41 | 42 | it 'accepts rules with name + parameters' do 43 | rule = double 44 | expect(Validation::Rule::Length).to receive(:new).with({:maximum => 5, :minimum => 3}).and_return(rule) 45 | subject.rule(:email, :length => {:maximum => 5, :minimum => 3}) 46 | 47 | expect(subject.instance_variable_get(:@rules)[:email]).to eq([rule]) 48 | end 49 | 50 | it 'accepts rules with class + parameters' do 51 | rule_class = Class.new 52 | rule = double 53 | expect(rule_class).to receive(:new).with({:maximum => 5, :minimum => 3}).and_return(rule) 54 | subject.rule(:email, rule_class => {:maximum => 5, :minimum => 3}) 55 | 56 | expect(subject.instance_variable_get(:@rules)[:email]).to eq([rule]) 57 | end 58 | 59 | it 'does something with invalid rules' do 60 | expect { subject.rule(:email, :foobar) }.to raise_error(Validation::InvalidRule) 61 | end 62 | 63 | context 'sends the data object to the rule' do 64 | before :each do 65 | length = double 66 | expect(Validation::Rule::Length).to receive(:new).with({:maximum => 5, :minimum => 3}).and_return(length) 67 | 68 | not_empty = double 69 | expect(not_empty).to receive(:obj=).with(data_object) 70 | expect(Validation::Rule::NotEmpty).to receive(:new).and_return(not_empty) 71 | end 72 | 73 | it :single_rule do 74 | subject.rule(:email, :not_empty) 75 | subject.rule(:email, :length => {:minimum => 3, :maximum => 5}) 76 | end 77 | 78 | it :multiple_rules do 79 | subject.rule(:email, [:not_empty, :length => {:minimum => 3, :maximum => 5}]) 80 | end 81 | end 82 | 83 | it 'returns self so rules can be chained' do 84 | expect do 85 | subject 86 | .rule(:email, :not_empty) 87 | .rule(:email, :length => {:minimum => 3, :maximum => 5}) 88 | end.not_to raise_error 89 | end 90 | end 91 | 92 | context :valid? do 93 | subject { Validation::Validator.new(OpenStruct.new(:id => 1, :email => 'foo@bar.com')) } 94 | 95 | context :true do 96 | before :each do 97 | rule = double(:rule, :valid_value? => true) 98 | expect(Validation::Rule::NotEmpty).to receive(:new).and_return(rule) 99 | end 100 | 101 | it 'returns true when the object is valid' do 102 | subject.rule(:email, :not_empty) 103 | expect(subject.valid?).to eq(true) 104 | end 105 | end 106 | 107 | context :false do 108 | before :each do 109 | rule = double(:rule, :valid_value? => false, :error_key => :not_empty, :params => nil) 110 | expect(Validation::Rule::NotEmpty).to receive(:new).and_return(rule) 111 | end 112 | 113 | it 'returns false when the object is not valid' do 114 | subject.rule(:email, :not_empty) 115 | expect(subject.valid?).to eq(false) 116 | end 117 | end 118 | 119 | context 'invalid rule key' do 120 | before :each do 121 | rule = double(:rule, :valid_value? => false) 122 | expect(Validation::Rule::NotEmpty).to receive(:new).and_return(rule) 123 | end 124 | 125 | it 'raises a descriptive error if a rule exists for an invalid object key' do 126 | subject.rule(:foobar, :not_empty) 127 | expect { subject.valid? }.to raise_error( 128 | Validation::InvalidKey, 129 | "cannot validate non-existent field 'foobar'" 130 | ) 131 | end 132 | end 133 | 134 | context 'invalid rule' do 135 | before :each do 136 | rule = double(:rule, :valid_value? => false) 137 | end 138 | 139 | it 'raises a descriptive error if an invalid rule is attempted' do 140 | expect { 141 | subject.rule(:foobar, :invalid_rule) 142 | }.to raise_error( 143 | Validation::InvalidRule, 144 | "uninitialized constant Validation::Rule::InvalidRule" 145 | ) 146 | end 147 | end 148 | end 149 | 150 | context :errors do 151 | subject { Validation::Validator.new(OpenStruct.new(:id => 1, :email => 'foo@bar.com', :foobar => '')) } 152 | 153 | it 'has no errors when the object is valid' do 154 | rule = double(:rule, :valid_value? => true, :error_key => :not_empty) 155 | expect(Validation::Rule::NotEmpty).to receive(:new).and_return(rule) 156 | 157 | subject.rule(:foobar, :not_empty) 158 | subject.valid? 159 | expect(subject.errors).to be_empty 160 | end 161 | 162 | it 'has errors when the object is invalid' do 163 | rule = double(:rule, :valid_value? => false, :error_key => :not_empty, :params => nil) 164 | expect(Validation::Rule::NotEmpty).to receive(:new).and_return(rule) 165 | 166 | subject.rule(:foobar, :not_empty) 167 | subject.valid? 168 | expect(subject.errors).to eq(:foobar => { :rule => :not_empty, :params => nil }) 169 | end 170 | 171 | it 'shows the first error when there are multiple errors' do 172 | not_empty = double(:not_empty, :valid_value? => false, :error_key => :not_empty, :params => nil) 173 | expect(Validation::Rule::NotEmpty).to receive(:new).and_return(not_empty) 174 | length = double(:length, :valid_value? => false, :error_key => :length) 175 | expect(Validation::Rule::Length).to receive(:new).and_return(length) 176 | 177 | subject.rule(:foobar, :not_empty) 178 | subject.rule(:foobar, :length) 179 | subject.valid? 180 | expect(subject.errors).to eq(:foobar => { :rule => :not_empty, :params => nil }) 181 | end 182 | end 183 | end 184 | 185 | module Validation 186 | module Rule 187 | class Length 188 | end 189 | class NotEmpty 190 | end 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /valid.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib/', __FILE__) 2 | $:.unshift lib unless $:.include?(lib) 3 | 4 | require 'validation/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'valid' 8 | s.version = Validation::VERSION 9 | s.authors = ['Jeremy Bush'] 10 | s.email = ['contractfrombelow@gmail.com'] 11 | s.summary = 'A standalone, generic object validator for ruby' 12 | s.homepage = %q{https://github.com/zombor/Validator} 13 | s.license = 'ISC' 14 | 15 | s.files = Dir.glob("lib/**/*") + ["README.md"] 16 | s.require_path = 'lib' 17 | end 18 | --------------------------------------------------------------------------------