diff --git a/lib/couchrest/model/validations.rb b/lib/couchrest/model/validations.rb index 20c95c2..525b2cc 100644 --- a/lib/couchrest/model/validations.rb +++ b/lib/couchrest/model/validations.rb @@ -1,6 +1,11 @@ # encoding: utf-8 require "couchrest/model/validations/casted_model" +require "couchrest/model/validations/uniqueness" + +I18n.load_path << File.join( + File.dirname(__FILE__), "validations", "locale", "en.yml" +) module CouchRest module Model @@ -23,8 +28,32 @@ module CouchRest validates_with(CastedModelValidator, _merge_attributes(args)) end - # TODO: Here will lie validates_uniqueness_of - + # Validates if the field is unique for this type of document. Automatically creates + # a view if one does not already exist and performs a search for all matching + # documents. + # + # Example: + # + # class Person < CouchRest::Model::Base + # property :title, String + # + # validates_uniqueness_of :title + # end + # + # Asside from the standard options, a +:proxy+ parameter is also accepted if you would + # like to call a method on the document on which the view should be performed. + # + # Examples: + # + # # Same as not including proxy: + # validates_uniqueness_of :title, :proxy => 'class' + # + # # Person#company.people provides a proxy object for people + # validates_uniqueness_of :title, :proxy => 'company.people' + # + def validates_uniqueness_of(*args) + validates_with(UniquenessValidator, _merge_attributes(args)) + end end end diff --git a/lib/couchrest/model/validations/locale/en.yml b/lib/couchrest/model/validations/locale/en.yml new file mode 100644 index 0000000..942261f --- /dev/null +++ b/lib/couchrest/model/validations/locale/en.yml @@ -0,0 +1,5 @@ +en: + errors: + messages: + taken: "is already taken" + diff --git a/lib/couchrest/model/validations/uniqueness.rb b/lib/couchrest/model/validations/uniqueness.rb new file mode 100644 index 0000000..81d9392 --- /dev/null +++ b/lib/couchrest/model/validations/uniqueness.rb @@ -0,0 +1,42 @@ +# encoding: urf-8 + +module CouchRest + module Model + module Validations + + # Validates if a field is unique + class UniquenessValidator < ActiveModel::EachValidator + + # Ensure we have a class available so we can check for a usable view + # or add one if necessary. + def setup(klass) + @klass = klass + end + + + def validate_each(document, attribute, value) + unless @klass.has_view?("by_#{attribute}") + @klass.view_by attribute + end + + # Determine the base of the search + base = options[:proxy].nil? ? @klass : document.send(options[:proxy]) + + docs = base.view("by_#{attribute}", :key => value, :limit => 2, :include_docs => false)['rows'] + return if docs.empty? + + unless document.new? + return if docs.find{|doc| doc['id'] == document.id} + end + + if docs.length > 0 + document.errors.add(attribute, :taken, :default => options[:message], :value => value) + end + end + + + end + + end + end +end diff --git a/spec/couchrest/validations.rb b/spec/couchrest/validations.rb new file mode 100644 index 0000000..3e3dd83 --- /dev/null +++ b/spec/couchrest/validations.rb @@ -0,0 +1,58 @@ +require File.expand_path("../../spec_helper", __FILE__) + +require File.join(FIXTURE_PATH, 'more', 'cat') +require File.join(FIXTURE_PATH, 'more', 'article') +require File.join(FIXTURE_PATH, 'more', 'course') +require File.join(FIXTURE_PATH, 'more', 'card') +require File.join(FIXTURE_PATH, 'base') + +# TODO Move validations from other specs to here + +describe "Validations" do + + describe "Uniqueness" do + + before(:all) do + @objs = ['title 1', 'title 2', 'title 3'].map{|t| WithUniqueValidation.create(:title => t)} + end + + it "should validate a new unique document" do + @obj = WithUniqueValidation.create(:title => 'title 4') + @obj.new?.should_not be_true + @obj.should be_valid + end + + it "should not validate a non-unique document" do + @obj = WithUniqueValidation.create(:title => 'title 1') + @obj.should_not be_valid + @obj.errors[:title].should eql(['is already taken']) + end + + it "should save already created document" do + @obj = @objs.first + @obj.save.should_not be_false + @obj.should be_valid + end + + context "with a proxy parameter" do + it "should be used" do + @obj = @objs.first + proxy = @obj.should_receive('proxy').and_return(@obj.class) + @obj.class.validates_uniqueness_of :title, :proxy => 'proxy' + @obj.valid?.should be_true + end + end + + context "with a pre-defined view" do + it "should no try to create new view" do + @obj = @objs.first + @obj.class.should_not_receive('view_by') + @obj.class.should_receive('has_view?').and_return(true) + @obj.class.should_receive('view').and_return({'rows' => [ ]}) + @obj.valid? + end + end + + end + +end diff --git a/spec/fixtures/base.rb b/spec/fixtures/base.rb index 66c32a5..406ca0b 100644 --- a/spec/fixtures/base.rb +++ b/spec/fixtures/base.rb @@ -114,4 +114,12 @@ class WithAfterInitializeMethod < CouchRest::Model::Base end +class WithUniqueValidation < CouchRest::Model::Base + use_database DB + + property :title + + validates_uniqueness_of :title +end +