Skip to content
Browse files

Cleaner implementation based on with_options

  • Loading branch information...
1 parent c39c219 commit 7d68bbb08cea900329a730f276e06f2ed7322784 @stevehodgkiss committed Mar 3, 2011
Showing with 149 additions and 149 deletions.
  1. +1 −0 Gemfile
  2. +1 −0 Gemfile.lock
  3. +11 −24 README.md
  4. +42 −51 lib/validation_scopes.rb
  5. +94 −74 spec/validation_scopes_spec.rb
View
1 Gemfile
@@ -3,5 +3,6 @@ source "http://rubygems.org"
gemspec
group :test do
+ gem "activemodel"
gem "rspec"
end
View
1 Gemfile.lock
@@ -28,5 +28,6 @@ PLATFORMS
ruby
DEPENDENCIES
+ activemodel
rspec
validation-scopes!
View
35 README.md
@@ -1,19 +1,21 @@
# Validation Scopes
-Validation Scopes allows you to group validations together that share the same conditions. It depends on ActiveModel. Example:
+Validation Scopes allows you to remove duplication in validations by grouping validations that share the same conditions. It works the same way as ActiveSupport's OptionMerger does, except instead of replacing duplicate keys it groups them into an array. This is so that nested if conditions work inside ActiveModel validations. Example, with result shown in comments:
class Car < ActiveRecord::Base
- validation_scope :if => Proc.new { |u| u.step == 2 } do
- validates_presence_of :variant
- validates_presence_of :body
+ validation_scope :if => Proc.new { |u| u.step == 2 } do |v|
+ v.validates_presence_of :variant # , :if => Proc.new { |u| u.step == 2 }
+ v.validation_scope :if => :something? do |s|
+ s.validates_presence_of :body # , :if => [Proc.new { |u| u.step == 2 }, :something?]
+ end
end
- validation_scope :if => Proc.new { |u| i.step == 3 } do
- validates_inclusion_of :outstanding_finance, :in => [true, false], :if => Proc.new { |u| u.finance == true }
+ validation_scope :if => Proc.new { |u| u.step == 3 } do |v|
+ v.validates_inclusion_of :outstanding_finance, :in => [true, false], :if => Proc.new { |u| u.finance == true }
# Duplicate keys are turned into arrays
- # In this case InclusionValidator would get an array containing both Procs in the :if attribute
+ # :if => [Proc.new { |u| u.finance == true }, Proc.new { |u| i.step == 3 }]
- validate do
+ v.validate do # v.validate :if => Proc.new { |u| u.step == 3 }
errors.add(:weight, "Must be greater than 0") unless !@weight.nil? && @weight > 0
end
end
@@ -27,19 +29,4 @@ Add the gem to your Gemfile
gem "validation-scopes"
-It will be included into ActiveRecord::Base if it is defined, if not use `include ValidationScopes` on any ActiveModel object.
-
-# ActiveSupport alternative
-
-ActiveSupport has an OptionMerger class for achieving the same thing generically and can be used anywhere (I noticed this after I wrote this gem). The only downside is that it will not merge option values into an array. In the example below the :if Proc on the `validates_inclusion_of` line would take precedence over the one defined on the `with_options` line. See the source code for [with_options](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/object/with_options.rb) for more details.
-
- class Car < ActiveRecord::Base
- with_options :if => Proc.new { |u| u.step == 2 } do |v|
- v.validates_presence_of :variant
- v.validates_presence_of :body
- end
-
- with_options :if => Proc.new { |u| i.step == 3 } do |v|
- v.validates_inclusion_of :outstanding_finance, :in => [true, false], :if => Proc.new { |u| u.finance == true }
- end
- end
+It will be included into ActiveRecord::Base if it is defined, if not use `include ValidationScopes` on any ActiveModel object.
View
93 lib/validation_scopes.rb
@@ -1,68 +1,59 @@
require 'active_support/concern'
-require 'active_model'
module ValidationScopes
extend ActiveSupport::Concern
- module ClassMethods
-
- def validation_scope(options, &block)
- @_nested_level_count ||= 0
- @_validation_scope_options ||= {}
- parent_options = @_validation_scope_options[@_nested_level_count]
- @_nested_level_count += 1
- @_validation_scope_options[@_nested_level_count] = parent_options.nil? ? options : merge_options(parent_options.dup, options)
- block.call
- @_nested_level_count -= 1
- @_handled_by_with = false
+ class OptionMerger
+ instance_methods.each do |method|
+ undef_method(method) if method !~ /^(__|instance_eval|class|object_id)/
end
-
- def validates_with(*args, &block)
- if in_validation_scope?
- merge_args(args)
- @_handled_by_with = true
- end
- super(*args, &block)
+
+ def initialize(context, options)
+ @context, @options = context, options
end
- def validate(*args, &block)
- if in_validation_scope? & !@_handled_by_with
- merge_args(args)
- end
- @_handled_by_with = false
- super(*args, &block)
+ def validation_scope(options)
+ yield OptionMerger.new(@context, merge_options(@options, options))
end
-
+
private
-
- def in_validation_scope?
- !@_nested_level_count.nil? && @_nested_level_count > 0
- end
-
- def merge_args(args)
- if args.empty?
- args << @_validation_scope_options[@_nested_level_count].dup
- elsif args.last.is_a?(Hash) && args.last.extractable_options?
- options = args.extract_options!
- options = options.dup
- merge_options(options, @_validation_scope_options[@_nested_level_count])
- args << options
- end
- end
-
- def merge_options(options_a, options_b)
- return options_b if options_a.nil?
- options_a = options_a
- options_b.each_key do |key|
- if options_a[key].nil?
- options_a[key] = options_b[key]
+ def method_missing(method, *arguments, &block)
+ arguments << if arguments.last.respond_to?(:to_hash)
+ merge_options(@options, arguments.pop)
else
- options_a[key] = Array.wrap(options_a[key]) unless options_a[key].is_a?(Array)
- options_a[key] << options_b[key]
+ @options.dup
end
+ @context.__send__(method, *arguments, &block)
end
- options_a
+
+ def merge_options(options_b, options_a)
+ return options_b.dup if options_a.nil?
+ options_a = options_a.dup
+ options_b.each_pair do |key, value|
+ if options_a[key].nil?
+ options_a[key] = value
+ else
+ options_a[key] = Array.wrap(options_a[key]) unless options_a[key].is_a?(Array)
+ if value.is_a?(Array)
+ value.each do |v|
+ options_a[key] << v
+ end
+ else
+ options_a[key] << value
+ end
+ end
+ end
+ options_a
+ end
+ end
+
+ module ClassMethods
+
+ def validation_scope(options, &block)
+ raise "Deprecated. See readme for new usage. https://github.com/stevehodgkiss/validation-scopes" if block.arity == 0
+ yield OptionMerger.new(self, options)
end
+
end
end
View
168 spec/validation_scopes_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+require 'active_model'
class TestUser
include ActiveModel::Validations
@@ -13,35 +14,113 @@ class TestUser
def step_2?
step == 2
end
+
+ protected
+
+ def self.method_with_options(options)
+ options
+ end
end
-class TestValidator < ActiveModel::EachValidator
- attr_accessor :options
+describe ValidationScopes do
- def initialize(options)
- @options = options
- super(options)
+ before do
+ TestClass = Class.new do
+ include ValidationScopes
+
+ def self.method_with_options(options)
+ options
+ end
+
+ def self.method_with_multiple_options(*options)
+ options
+ end
+ end
end
- def validate(record)
-
+ it "merges method no options" do
+ TestClass.class_eval do
+ validation_scope :if => 1 do |v|
+ v.method_with_options.should == {:if => 1}
+ end
+ end
+ end
+
+ it "merges method with options" do
+ TestClass.class_eval do
+ validation_scope :if => 1 do |v|
+ v.method_with_options(:if => 2).should == {:if => [2, 1]}
+ end
+ end
+ end
+
+ it "merges with multiple options" do
+ TestClass.class_eval do
+ validation_scope :if => 1 do |v|
+ v.method_with_multiple_options(:name, :if => 2).should == [:name, {:if => [2, 1]}]
+ end
+ end
end
+
+ it "merges nested scope with no method options" do
+ TestClass.class_eval do
+ validation_scope :if => 1 do |v|
+ v.validation_scope :if => 2 do |s|
+ s.method_with_options.should == {:if => [2, 1]}
+ end
+ end
+ end
+ end
+
+ it "merges nested scope with method options" do
+ TestClass.class_eval do
+ validation_scope :if => 1 do |v|
+ v.validation_scope :if => 2 do |s|
+ s.method_with_options(:if => 3, :unless => 6).should == {:if => [3, 2, 1], :unless => 6}
+ end
+ end
+ end
+ end
+
+ it "merges deep nested scope with method options" do
+ TestClass.class_eval do
+ validation_scope :if => 1 do |v|
+ v.validation_scope :if => 2 do |s|
+ s.validation_scope :unless => 6 do |u|
+ u.method_with_options(:if => 3).should == {:if => [3, 2, 1], :unless => 6}
+ end
+ end
+ end
+ end
+ end
+
+ it "fails if called without a block parameter" do
+ expect {
+ TestClass.class_eval do
+ validation_scope :if => 1 do
+
+ end
+ end
+ }.to raise_error
+ end
+
+ after { Object.send(:remove_const, :TestClass) }
end
-describe ".validation_scope" do
+describe ValidationScopes, "with AM" do
context "validates_with" do
before do
User = Class.new(TestUser) do
validates_presence_of :address
- validation_scope :if => :step_2? do
- validates_presence_of :name
+ validation_scope :if => :step_2? do |v|
+ v.validates_presence_of :name
end
- validation_scope :if => Proc.new { |u| u.step == 3 } do
- validation_scope :if => Proc.new { |u| !u.height.nil? && u.height > 6 } do
- validates_presence_of :weight
+ validation_scope :if => Proc.new { |u| u.step == 3 } do |v|
+ v.validation_scope :if => Proc.new { |u| !u.height.nil? && u.height > 6 } do |h|
+ h.validates_presence_of :weight
end
- validates_inclusion_of :eye_colour, :in => ["blue", "brown"], :if => Proc.new { |u| !u.age.nil? && u.age > 20 }
+ v.validates_inclusion_of :eye_colour, :in => ["blue", "brown"], :if => Proc.new { |u| !u.age.nil? && u.age > 20 }
end
end
@user = User.new
@@ -86,66 +165,7 @@ def validate(record)
u.errors[:weight].should be
end
end
-
- it "calls validates_with with merged scope options" do
- presence_validator = User.validators_on(:name).first
- presence_validator.options.should eq({:if => :step_2?})
- end
- end
-
- context "validate method" do
- before do
- User = Class.new(TestUser) do
- validation_scope :unless => :step_2? do
- validate do
- errors.add(:weight, "Must be greater than 0") unless !@weight.nil? && @weight > 0
- end
- end
- end
- end
-
- it "should only validate weight on step 2" do
- user = User.new
- user.weight = 0
- user.should be_invalid
- user.weight = 1
- user.should be_valid
-
- user.step = 2
- user.weight = 0
- user.should be_valid
- end
- end
-
- context "custom validators" do
- before do
- User = Class.new(TestUser) do
- validation_scope :unless => :step_2?, :some_config_var => 5 do
- validates_with TestValidator, {:attributes => [:name], :some_config_var => 6}
- validation_scope :unless => Proc.new { |u| u.step == 3 } do
- validates_with TestValidator, {:attributes => [:weight], :some_config_var => 7}
- end
- end
- end
- @validator = User.validators_on(:name).first
- end
-
- it "passes the options in" do
- @validator.options[:unless].should eq(:step_2?)
- @validator.options[:some_config_var].should be
- end
-
- it "turns the duplicate options into an array" do
- @validator.options[:some_config_var].should eq([6, 5])
- end
-
- it "works for nested scopes" do
- option = User.validators_on(:weight).first.options[:unless]
- option.size.should eq(2)
- option[0].should eq(:step_2?)
- option[1].should be_an_instance_of(Proc)
- end
end
- after { Object.send(:remove_const, :User) if defined?(User) }
+ after { Object.send(:remove_const, :User) }
end

0 comments on commit 7d68bbb

Please sign in to comment.
Something went wrong with that request. Please try again.