├── .github └── workflows │ └── tests.yml ├── .gitignore ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── VERSION ├── gemfiles ├── rails_3.2.gemfile ├── rails_4.0.gemfile ├── rails_4.1.gemfile ├── rails_4.2.gemfile ├── rails_5.0.gemfile ├── rails_5.1.gemfile ├── rails_5.2.gemfile ├── rails_6.0.gemfile └── rails_6.1.gemfile ├── lib └── validation_scopes.rb ├── test ├── db │ └── schema.rb ├── fixtures │ ├── ar_internal_metadata.yml │ ├── books.yml │ └── users.yml ├── helper.rb ├── models │ ├── book.rb │ └── user.rb ├── test_memory_leak.rb └── test_validation_scopes.rb └── validation_scopes.gemspec /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push] 3 | 4 | jobs: 5 | tests: 6 | name: Tests 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ruby-version: 11 | - 2.7 12 | - 2.6 13 | - 2.5 14 | - 2.4 15 | - 2.3 16 | - 2.2 17 | - 2.1 18 | rails-version: 19 | - rails-3.2 20 | - rails-4.0 21 | - rails-4.1 22 | - rails-4.2 23 | - rails-5.0 24 | - rails-5.1 25 | - rails-5.2 26 | - rails-6.0 27 | - rails-6.1 28 | exclude: 29 | - rails-version: rails-3.2 30 | ruby-version: 2.4 31 | - rails-version: rails-3.2 32 | ruby-version: 2.5 33 | - rails-version: rails-3.2 34 | ruby-version: 2.6 35 | - rails-version: rails-3.2 36 | ruby-version: 2.7 37 | - rails-version: rails-4.0 38 | ruby-version: 2.4 39 | - rails-version: rails-4.0 40 | ruby-version: 2.5 41 | - rails-version: rails-4.0 42 | ruby-version: 2.6 43 | - rails-version: rails-4.0 44 | ruby-version: 2.7 45 | - rails-version: rails-4.1 46 | ruby-version: 2.4 47 | - rails-version: rails-4.1 48 | ruby-version: 2.5 49 | - rails-version: rails-4.1 50 | ruby-version: 2.6 51 | - rails-version: rails-4.1 52 | ruby-version: 2.7 53 | - rails-version: rails-4.2 54 | ruby-version: 2.7 55 | - rails-version: rails-5.0 56 | ruby-version: 2.1 57 | - rails-version: rails-5.1 58 | ruby-version: 2.1 59 | - rails-version: rails-5.2 60 | ruby-version: 2.1 61 | - rails-version: rails-6.0 62 | ruby-version: 2.1 63 | - rails-version: rails-6.0 64 | ruby-version: 2.2 65 | - rails-version: rails-6.0 66 | ruby-version: 2.3 67 | - rails-version: rails-6.0 68 | ruby-version: 2.4 69 | - rails-version: rails-6.1 70 | ruby-version: 2.1 71 | - rails-version: rails-6.1 72 | ruby-version: 2.2 73 | - rails-version: rails-6.1 74 | ruby-version: 2.3 75 | - rails-version: rails-6.1 76 | ruby-version: 2.4 77 | 78 | steps: 79 | - name: Checkout code 80 | uses: actions/checkout@v2 81 | - name: Set up Ruby ${{ matrix.ruby-version }} 82 | uses: ruby/setup-ruby@v1 83 | with: 84 | ruby-version: ${{ matrix.ruby-version }} 85 | - name: Install bundler 86 | run: gem install bundler -v 1.17.3 87 | - name: Install dependendies for build 88 | run: bundle install 89 | - name: Install dependencies for ${{ matrix.rails-version }} 90 | run: bundle exec appraisal ${{ matrix.rails-version }} bundle install 91 | - name: Run tests 92 | run: bundle exec appraisal ${{ matrix.rails-version }} rake 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | *.gem 3 | gemfiles/*.lock 4 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails-3.2" do 2 | gem "sqlite3", "~> 1.3.6" 3 | gem "activerecord", "~> 3.2.22" 4 | gem "minitest" 5 | end 6 | 7 | appraise "rails-4.0" do 8 | gem 'sqlite3', '~> 1.3.6' 9 | gem "activerecord", "~> 4.0.13" 10 | end 11 | 12 | appraise "rails-4.1" do 13 | gem 'sqlite3', '~> 1.3.6' 14 | gem "activerecord", "~> 4.1.11" 15 | end 16 | 17 | appraise "rails-4.2" do 18 | gem 'sqlite3', '~> 1.3.6' 19 | gem "activerecord", "~> 4.2.2" 20 | end 21 | 22 | appraise "rails-5.0" do 23 | gem 'sqlite3', '~> 1.3.6' 24 | gem "activerecord", "~> 5.0.0" 25 | end 26 | 27 | appraise "rails-5.1" do 28 | gem 'sqlite3', '~> 1.3.6' 29 | gem "activerecord", "~> 5.1.0" 30 | end 31 | 32 | appraise "rails-5.2" do 33 | gem 'sqlite3', '~> 1.3.6' 34 | gem "activerecord", "~> 5.2.0" 35 | end 36 | 37 | appraise "rails-6.0" do 38 | gem "activerecord", "~> 6.0.0" 39 | end 40 | 41 | appraise "rails-6.1" do 42 | gem "activerecord", "~> 6.1.0" 43 | end 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.6.3 - 2021-06-19 2 | 3 | * Allow Rails 6.1 (James Adam) 4 | 5 | # 0.6.2 - 2019-08-28 6 | 7 | * Allow Rails 6 (James Adam) 8 | 9 | # 0.6.1 - 2017-01-06 10 | 11 | * Allow Rails 5 (Marc van Eyken, James Adam) 12 | 13 | # 0.6.0 - 2015-07-14 14 | 15 | * Add support for STI classes (Jeremy Mickelson) 16 | * Add support for validates_associated (Jeremy Mickelson) 17 | 18 | # 0.5.2 - 2015-06-18 19 | 20 | * Fix Rails 4.2 deprecations and breakages (Tamás Michelberger) 21 | * Loosen version constraints to allow Rails 4 (Ivan Tkalin) 22 | 23 | # 0.5.1 - 2013-02-05 24 | 25 | * Requires 1.9.2 26 | * Fix for memory leak described at http://siliconsenthil.in/blog/2013/01/19/validation-scopes-leaks-memory/ 27 | * Cleaned up .gemspec and removed unnecessary files from distribution 28 | * Set up Gemfile for development 29 | * Simplified test suite to use pure minitest 30 | 31 | # 0.4.1 - 2012-04-15 32 | 33 | * Rails 3.1 and 3.2 compatibility 34 | * Added the method all_scopes that return all the scopes in a model 35 | 36 | # 0.4.0 - 2011-05-23 37 | 38 | * Fixed problem with #model_name on proxy class (dynamic_form plugin) 39 | * Fixed problem with scoped errors being lost by error_messages_for (dynamic_form plugin) 40 | * Fixed Rails3 deprecation warnings 41 | 42 | # 0.3.1 - 2011-02-24 43 | 44 | * Added Rails3 compatibility 45 | 46 | # 0.3.0 - 2009-01-04 47 | 48 | * Fixed problem with DelegateClass not picking up method definitions that come after the validation_scope block. 49 | 50 | # 0.2.0 - 2009-01-04 51 | 52 | * Added basic unit tests using in-memory sqlite3 53 | 54 | # 0.1.0 - 2009-01-03 55 | 56 | * Initial release. Only tested within an app. Probably has lots of bugs. Test suite forthcoming. 57 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://www.rubygems.org" 2 | 3 | gem 'appraisal' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Gabe da Silveira 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Validation Scopes ![Tests](https://github.com/gtd/validation_scopes/actions/workflows/tests.yml/badge.svg) 2 | 3 | This gem adds a simple class method `validation_scope` to ActiveRecord. This generates a new collection of 4 | `ActiveRecord::Errors` that can be manipulated independently of the standard `errors`, `valid?` and `save` methods. The 5 | full power of ActiveRecord validations are preserved in these distinct error collections, including all the macros. 6 | 7 | For example, in addition to standard errors that prevent an object from being saved to the database, you may want 8 | a second collection of warnings that you display to the user or otherwise shape the control flow: 9 | 10 | class Film < ActiveRecord::Base 11 | validates_presence_of :title # Standard errors 12 | 13 | validation_scope :warnings do |s| 14 | s.validate :ensure_title_is_capitalized 15 | s.validate { |r| r.warnings.add_to_base("Inline warning") } 16 | s.validates_presence_of… 17 | s.validates_inclusion_of… 18 | s.validates_each… 19 | s.validates_on_create… 20 | end 21 | 22 | def ensure_title_is_capitalized 23 | warnings.add(:title, "should be capitalized") unless title =~ %r{\A[A-Z]} 24 | end 25 | end 26 | 27 | The generated scope produces 3 helper methods based on the symbol passed to the validation_scope method. Continuing the 28 | previous example: 29 | 30 | film = Film.new(:title => 'lowercase title') 31 | film.valid? 32 | => true 33 | 34 | film.no_warnings? # analagous to valid? 35 | => false 36 | 37 | film.has_warnings? # analagous to invalid? 38 | => true 39 | 40 | film.warnings # analagous to film.errors 41 | => # 42 | 43 | film.warnings.full_messages 44 | => ["Title should be capitalized", "Inline warning"] 45 | 46 | film.errors.full_messages 47 | => [] 48 | 49 | film.class.all_scopes 50 | => [:warnings] 51 | 52 | film.save 53 | => true 54 | 55 | One rough edge at the moment is when you want to use the builtin `error_messages_for` helper in your views. That helper 56 | does not accept an `ActiveRecord::Errors` object directly. Instead you need to pass it the proxy object that 57 | `ValidationScopes` creates to encapsulate the generated error set: 58 | 59 | error_messages_for :object => film.validation_scope_proxy_for_warnings 60 | 61 | ## Compatibility 62 | 63 | The current version should work for Rails >= 3.0 and Ruby >= 1.9.2. 64 | 65 | For Rails 3 and Ruby 1.8.x use version 0.4.x, however **beware there is a memory leak in this version** as described 66 | [here](http://siliconsenthil.in/blog/2013/01/19/validation-scopes-leaks-memory/) 67 | 68 | For Rails 2 see the 0.3.x version of the gem which is maintained on the [rails2 69 | branch](https://github.com/gtd/validation_scopes/tree/rails2) 70 | 71 | 72 | ## Installation 73 | 74 | The usual: 75 | 76 | gem install validation_scopes 77 | 78 | In your Gemfile: 79 | 80 | gem 'validation_scopes' 81 | 82 | Or without Bundler: 83 | 84 | require 'validation_scopes' 85 | 86 | 87 | ### Don't use private methods 88 | 89 | Because the any validation method supplied as a symbol (eg. `validate :verify_something`) is actually running in the 90 | context of a delegate class, private methods won't work as they would in standard validations. 91 | 92 | 93 | ## TODO 94 | 95 | * In Rails 3 validations are no longer coupled to ActiveRecord. Although the current version of the gem uses 96 | ActiveModel, it hasn't been tested against arbitrary objects. 97 | 98 | 99 | ## Copyright 100 | 101 | Copyright (c) 2010-2021 Gabe da Silveira. See LICENSE for details. 102 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require 'rake/testtask' 4 | Rake::TestTask.new(:test) do |test| 5 | test.libs << %w(lib test) 6 | test.pattern = 'test/**/test_*.rb' 7 | test.verbose = true 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.6.3 2 | -------------------------------------------------------------------------------- /gemfiles/rails_3.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://www.rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "sqlite3", "~> 1.3.6" 7 | gem "activerecord", "~> 3.2.22" 8 | gem "minitest" 9 | 10 | gemspec :path => "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_4.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://www.rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "sqlite3", "~> 1.3.6" 7 | gem "activerecord", "~> 4.0.13" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails_4.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://www.rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "sqlite3", "~> 1.3.6" 7 | gem "activerecord", "~> 4.1.11" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails_4.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://www.rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "sqlite3", "~> 1.3.6" 7 | gem "activerecord", "~> 4.2.2" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails_5.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://www.rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "sqlite3", "~> 1.3.6" 7 | gem "activerecord", "~> 5.0.0" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails_5.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://www.rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "sqlite3", "~> 1.3.6" 7 | gem "activerecord", "~> 5.1.0" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails_5.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://www.rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "sqlite3", "~> 1.3.6" 7 | gem "activerecord", "~> 5.2.0" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails_6.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://www.rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "activerecord", "~> 6.0.0" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails_6.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://www.rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "activerecord", "~> 6.1.0" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /lib/validation_scopes.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | require 'active_record' 3 | 4 | module ValidationScopes 5 | def self.included(base) # :nodoc: 6 | base.extend ClassMethods 7 | end 8 | 9 | # Based on AssociatedValidator from ActiveRecord, see: 10 | # activerecord-4.2.0/lib/active_record/validations/associated.rb @ line 3 11 | class AssociatedValidator < ActiveModel::EachValidator 12 | def validate_each(record, attribute, value) 13 | all_valid = Array.wrap(value).all? do |r| 14 | r.marked_for_destruction? || r.send("no_#{options[:scope]}?") 15 | end 16 | unless all_valid 17 | record.errors.add(attribute, 'is invalid') 18 | end 19 | end 20 | end 21 | 22 | module ClassMethods 23 | def validation_scopes 24 | @validation_scopes ||= [] 25 | end 26 | 27 | def validation_proxies 28 | @validation_proxies ||= {} 29 | end 30 | 31 | def validation_scope(scope) 32 | validation_scopes << scope 33 | 34 | base_class = self 35 | 36 | superclass = self.superclass.validation_proxies[scope] || DelegateClass(base_class) 37 | proxy_class = Class.new(superclass) do 38 | include ActiveModel::Validations 39 | 40 | @scope = scope 41 | def self.validates_associated(*attr_names) 42 | validates_with AssociatedValidator, 43 | _merge_attributes(attr_names).reverse_merge(scope: @scope) 44 | end 45 | 46 | # Hacks to support dynamic_model helpers 47 | def to_model 48 | self 49 | end 50 | 51 | class << self; self; end.class_eval do 52 | # Rails 3 default implementation of model_name blows up for anonymous 53 | # classes 54 | define_method(:model_name) do 55 | base_class.model_name 56 | end 57 | # Before Rails 4.1 callback functions were created using the class 58 | # name, so we must name our anonymous classes. 59 | define_method(:name) do 60 | "#{base_class.name}#{scope.to_s.classify}ValidationProxy" 61 | end 62 | end 63 | end 64 | 65 | validation_proxies[scope] = proxy_class 66 | 67 | yield proxy_class 68 | 69 | define_method(scope) do 70 | send("validation_scope_proxy_for_#{scope}").errors 71 | end 72 | 73 | define_method("no_#{scope}?") do 74 | send("validation_scope_proxy_for_#{scope}").valid? 75 | end 76 | 77 | define_method("has_#{scope}?") do 78 | send("validation_scope_proxy_for_#{scope}").invalid? 79 | end 80 | 81 | define_method("init_validation_scope_for_#{scope}") do 82 | unless instance_variable_defined?("@validation_#{scope}") 83 | instance_variable_set("@validation_#{scope}", proxy_class.new(self)) 84 | end 85 | end 86 | 87 | define_method("validation_scope_proxy_for_#{scope}") do 88 | send "init_validation_scope_for_#{scope}" 89 | instance_variable_get("@validation_#{scope}") 90 | end 91 | end 92 | end 93 | end 94 | 95 | ActiveRecord::Base.send(:include, ValidationScopes) 96 | -------------------------------------------------------------------------------- /test/db/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "users" do |t| 3 | t.column "name", :string 4 | t.column "email", :string 5 | t.column "age", :integer 6 | t.column "bio", :text 7 | t.column "sponsor_id", :integer 8 | t.column "type", :string 9 | end 10 | 11 | create_table "books" do |t| 12 | t.column "title", :string 13 | t.column "author", :string 14 | t.column "isbn", :string 15 | t.column "user_id", :integer 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/fixtures/ar_internal_metadata.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtd/validation_scopes/bc65e57bd338b4143e5e1f07fece780a893299b4/test/fixtures/ar_internal_metadata.yml -------------------------------------------------------------------------------- /test/fixtures/books.yml: -------------------------------------------------------------------------------- 1 | one: 2 | id: 1 3 | title: Feature Selection for Knowledge Discovery and Data Mining 4 | author: Huan Liu 5 | isbn: 9780792381983 6 | user_id: 1 7 | two: 8 | id: 2 9 | title: SSL and TLS - theory and practice 10 | author: Rolf Oppliger 11 | isbn: 9781596934474 12 | user_id: 1 13 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | one: 2 | id: 1 3 | name: Gabe da Silveira 4 | email: gabe@websaviour.com 5 | age: 31 6 | bio: Technical web creative 7 | sponsored: 8 | id: 2 9 | name: Sven Pulpello 10 | email: sven@funk.com 11 | age: 3 12 | bio: Alter-ego extraordinaire 13 | sponsor_id: 1 14 | important: 15 | id: 3 16 | name: Jeremy Mickelson 17 | email: jeremy@mickelson.com 18 | age: 27 19 | bio: Super Hacker 20 | type: ImportantUser 21 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 4 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 5 | require 'validation_scopes' 6 | 7 | puts "Testing against ActiveRecord #{ActiveRecord::VERSION::STRING}" 8 | 9 | ActiveRecord::Base.establish_connection( 10 | :adapter => "sqlite3", 11 | :database => ":memory:" 12 | ) 13 | 14 | require 'db/schema.rb' 15 | 16 | Dir['./test/models/*.rb'].each { |f| require f } 17 | 18 | require 'active_record/fixtures' 19 | 20 | fixtures_constant = if defined?(ActiveRecord::FixtureSet) 21 | ActiveRecord::FixtureSet 22 | elsif defined?(ActiveRecord::Fixtures) 23 | ActiveRecord::Fixtures 24 | else 25 | Fixtures 26 | end 27 | 28 | base_test_class = defined?(Minitest::Test) ? Minitest::Test : MiniTest::Unit::TestCase 29 | class TestCase < base_test_class; end 30 | 31 | fixtures_constant.create_fixtures('test/fixtures/', ActiveRecord::Base.connection.tables) 32 | -------------------------------------------------------------------------------- /test/models/book.rb: -------------------------------------------------------------------------------- 1 | class Book < ActiveRecord::Base 2 | belongs_to :user 3 | 4 | validates_presence_of :title 5 | 6 | validation_scope :warnings_book do |s| 7 | s.validates_presence_of :author 8 | end 9 | 10 | validation_scope :alerts_book do |s| 11 | s.validates_presence_of :isbn 12 | end 13 | 14 | validation_scope :alerts do |s| 15 | s.validates_presence_of :isbn 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | belongs_to :sponsor, :class_name => 'User' 3 | has_many :books 4 | 5 | validates_presence_of :name 6 | 7 | validation_scope :warnings do |s| 8 | s.validates_presence_of :email 9 | s.validates_format_of :email, :with => %r{\A.+@.+\Z} 10 | s.validates_inclusion_of :age, :in => 0..99 11 | s.validates_associated :books, scope: :warnings_book 12 | s.validate do |r| 13 | if r.sponsor_id.present? && r.sponsor.nil? 14 | r.warnings.add(:sponsor_id, "Sponsor ID was defined but record not present") 15 | end 16 | end 17 | end 18 | 19 | validation_scope :alerts do |s| 20 | s.validate :age_under_100 21 | s.validates_associated :books 22 | s.validate { |r| r.alerts.add(:email, "We have a hotmail user.") if r.email =~ %r{@hotmail\.com\Z} } 23 | end 24 | 25 | def age_under_100 26 | alerts.add(:base, "We have a centenarian on our hands") if age && age >= 100 27 | end 28 | end 29 | 30 | class ImportantUser < User 31 | validation_scope :warnings do |s| 32 | s.validates_presence_of :bio 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/test_memory_leak.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestMemoryLeak < TestCase 4 | def test_should_not_leak_proxy_class 5 | ids = 2.times.map do 6 | user = User.new 7 | user.has_warnings? 8 | user.instance_variable_get(:@warnings).class.object_id 9 | end 10 | 11 | assert_equal ids[0], ids[1] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_validation_scopes.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestValidationScopes < TestCase 4 | def setup 5 | @user = User.find(1) 6 | end 7 | 8 | def test_warnings_definition 9 | assert @user.warnings, "warnings was not defined" 10 | end 11 | 12 | def test_empty_warnings 13 | assert ! @user.has_warnings? 14 | assert @user.no_warnings? 15 | end 16 | 17 | def test_a_macro 18 | @user.age = -1 19 | assert @user.has_warnings? 20 | assert @user.warnings[:age].any? 21 | end 22 | 23 | def test_an_inline_error 24 | @user.sponsor_id = 12345 25 | assert @user.has_warnings? 26 | assert @user.warnings[:sponsor_id].any? 27 | end 28 | 29 | def test_a_symbol_error 30 | @user.age = 100 31 | assert @user.has_alerts? 32 | assert @user.alerts[:base] 33 | end 34 | 35 | def test_that_warnings_do_not_impact_main_errors 36 | @user.email = '' 37 | assert @user.has_warnings? 38 | assert @user.valid? 39 | assert @user.errors.empty? 40 | end 41 | 42 | def test_that_errors_do_not_impact_warnings 43 | @user.name = '' 44 | assert @user.invalid? 45 | assert @user.warnings.empty? 46 | end 47 | 48 | def test_multiple_scopes 49 | @user.age = 100 50 | @user.email = "invalidemail" 51 | 52 | assert @user.has_alerts?, "no alerts raised" 53 | assert @user.has_warnings?, "no warnings raised" 54 | assert_equal 1, @user.alerts.size 55 | assert_equal 2, @user.warnings.size 56 | assert @user.alerts[:base], "centenarian alert not raised" 57 | assert @user.warnings[:email], "email warning not raised" 58 | assert @user.valid?, "user not valid" 59 | assert @user.errors.empty?, "user errors not empty" 60 | end 61 | 62 | def test_proxy_returns_self_for_to_model 63 | # Because error_messages_for in dynamic_model gem calls this and delegation 64 | # causes base object to be returned, thus losing scoped errors. 65 | assert_equal @user.validation_scope_proxy_for_warnings.class, 66 | @user.validation_scope_proxy_for_warnings.to_model.class 67 | end 68 | 69 | def test_proxy_returns_model_name_of_base_class 70 | assert_equal 'User', 71 | @user.validation_scope_proxy_for_warnings.class.model_name 72 | end 73 | 74 | def test_validation_scopes 75 | assert_equal [:warnings, :alerts], User.validation_scopes 76 | assert_equal [:warnings_book, :alerts_book, :alerts], Book.validation_scopes 77 | end 78 | 79 | def test_validates_associated 80 | assert !@user.has_warnings? 81 | assert @user.books.none? { |b| b.has_warnings_book? } 82 | @user.books.first.author = nil 83 | assert @user.books.first.has_warnings_book? 84 | assert @user.has_warnings? 85 | end 86 | 87 | def test_validates_associated_default_scope 88 | assert !@user.has_alerts? 89 | assert @user.books.none? { |b| b.has_alerts? } 90 | @user.books.first.isbn = nil 91 | assert @user.books.first.has_alerts? 92 | assert @user.has_alerts? 93 | end 94 | 95 | def test_sti_inheritance_of_scopes 96 | user = User.find(3) 97 | assert_instance_of ImportantUser, user 98 | assert user.no_warnings? 99 | # Make sure super class validations work 100 | user.email = nil 101 | assert user.has_warnings? 102 | user.email = 'a@b.com' 103 | assert user.no_warnings? 104 | # Make sure child class validations work 105 | user.bio = nil 106 | assert user.has_warnings? 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /validation_scopes.gemspec: -------------------------------------------------------------------------------- 1 | version = File.read(File.expand_path("../VERSION",__FILE__)).strip 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "validation_scopes" 5 | s.version = version 6 | 7 | s.authors = ["Gabe da Silveira"] 8 | s.description = "Define additional sets of validations beyond the standard \"errors\" that is tied to the ActiveRecord life-cycle. These additional sets can be defined with all the standard ActiveRecord::Validation macros, and the resulting collection is a standard ActiveRecord::Errors object." 9 | s.email = "gabe@websaviour.com" 10 | s.extra_rdoc_files = [ 11 | "LICENSE", 12 | "README.md" 13 | ] 14 | s.files = [ 15 | "CHANGELOG.md", 16 | "LICENSE", 17 | "README.md", 18 | "lib/validation_scopes.rb" 19 | ] 20 | s.homepage = "http://github.com/gtd/validation_scopes" 21 | s.require_paths = ["lib"] 22 | s.rubygems_version = "1.8.22" 23 | 24 | s.summary = "Create sets of validations independent of the life-cycle of an ActiveRecord object" 25 | 26 | s.required_ruby_version = '>= 1.9.2' 27 | 28 | s.add_dependency 'activerecord', '>= 3', '< 6.2' 29 | 30 | s.add_development_dependency 'rake' 31 | s.add_development_dependency 'sqlite3' 32 | end 33 | --------------------------------------------------------------------------------