From 39c44f02d2b5028d6c65db3b5b2f18c163f1be8d Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Wed, 2 Sep 2009 16:38:34 -0700 Subject: [PATCH] Most of specs pass. Removed :case_sensitive and :allow_blank options because there is no equivalent in validates_is_unique --- .../lib/remarkable_datamapper/base.rb | 13 +- .../lib/remarkable_datamapper/describe.rb | 1 - ...nique.rb => validate_is_unique_matcher.rb} | 65 +++----- remarkable_datamapper/locale/en.yml | 17 +- remarkable_datamapper/spec/describe_spec.rb | 103 ------------ remarkable_datamapper/spec/model_builder.rb | 49 +++--- .../spec/validates_is_unique_matcher_spec.rb | 155 ++++++++++++++++++ 7 files changed, 213 insertions(+), 190 deletions(-) rename remarkable_datamapper/lib/remarkable_datamapper/matchers/{validates_is_unique.rb => validate_is_unique_matcher.rb} (77%) delete mode 100644 remarkable_datamapper/spec/describe_spec.rb create mode 100644 remarkable_datamapper/spec/validates_is_unique_matcher_spec.rb diff --git a/remarkable_datamapper/lib/remarkable_datamapper/base.rb b/remarkable_datamapper/lib/remarkable_datamapper/base.rb index 267166a..4597f1c 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/base.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/base.rb @@ -180,12 +180,11 @@ def assert_bad_value(model, attribute, value, error_message_to_expect=:invalid) # def error_message_from_model(model, attribute, message) #:nodoc: if message.is_a? Symbol - message = if I18N # Rails >= 2.2 - model.errors.generate_message(attribute, message, :count => '12345') - else # Rails <= 2.1 - ::DataMapper::Errors.default_error_messages[message] % '12345' - end - + # TODO: No Internationalization yet. + # TODO: remove debug line + pp attribute + message = ::DataMapper::Validate::ValidationErrors.default_error_message(message, attribute, '12345') + if message =~ /12345/ message = Regexp.escape(message) message.gsub!('12345', '\d+') @@ -240,7 +239,7 @@ def collection_interpolation #:nodoc: # Returns true if the given collection should be translated. # def i18n_collection? #:nodoc: - I18N && I18N_COLLECTION.include?(self.class.matcher_arguments[:collection]) + RAILS_I18N && I18N_COLLECTION.include?(self.class.matcher_arguments[:collection]) end end diff --git a/remarkable_datamapper/lib/remarkable_datamapper/describe.rb b/remarkable_datamapper/lib/remarkable_datamapper/describe.rb index 7564617..2ffcc5e 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/describe.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/describe.rb @@ -130,7 +130,6 @@ def describe(*args, &block) :key => translated_key.downcase, :value => value.inspect) end - pp pieces description << pieces.join(connector) args.unshift(description) diff --git a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb similarity index 77% rename from remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb rename to remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb index 5e25679..a31fd99 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb @@ -1,15 +1,15 @@ module Remarkable module DataMapper module Matchers - class ValidatesIsUniqueMatcher < Remarkable::DataMapper::Base #:nodoc: + class ValidateIsUniqueMatcher < Remarkable::DataMapper::Base #:nodoc: arguments :collection => :attributes, :as => :attribute optional :message optional :scope, :splat => true - optional :case_sensitive, :allow_nil, :allow_blank, :default => true + optional :nullable, :default => true - collection_assertions :find_first_object?, :responds_to_scope?, :is_unique?, :case_sensitive?, - :valid_with_new_scope?, :allow_nil?, :allow_blank? + collection_assertions :find_first_object?, :responds_to_scope?, :is_unique?, + :valid_with_new_scope?, :nullable? default_options :message => :taken @@ -28,24 +28,24 @@ class ValidatesIsUniqueMatcher < Remarkable::DataMapper::Base #:nodoc: # If any of these attempts fail, an error is raised. # def find_first_object? - conditions, message = if @options[:allow_nil] - [ ["#{@attribute} IS NOT NULL"], " with #{@attribute} not nil" ] - elsif @options[:allow_blank] - [ ["#{@attribute} != ''"], " with #{@attribute} not blank" ] - else - [ [], "" ] + conditions, message = [[], ""] + if @options[:nullable] + conditions << {::DataMapper::Query::Operator.new(@attribute, :not) => nil} + message << " with #{@attribute} not nil" end - - unless @subject.new_record? - primary_key = subject_class.primary_key - - message << " which is different from the subject record (the object being validated is the same as the one in the database)" - conditions << "#{subject_class.primary_key} != '#{@subject.send(primary_key)}'" + + unless @subject.new? + key = subject_class.key + + message << " which is different from the subject record (the object being validated is the same as the one in the database)" + conditions << {::DataMapper::Query::Operator.new(subject_class.key.first.name, :not) => @subject.send(key)} + pp conditions end - - options = conditions.empty? ? {} : { :conditions => conditions.join(' AND ') } - - return true if @existing = subject_class.find(:first, options) + + require 'pp' + #pp conditions + + return true if @existing = subject_class.first(conditions) raise ScriptError, "could not find a #{subject_class} record in the database" + message end @@ -70,19 +70,6 @@ def is_unique? return bad?(@value) end - # If :case_sensitive is given and it's false, we swap the case of the - # value used in :is_unique? and see if the test object remains valid. - # - # If :case_sensitive is given and it's true, we swap the case of the - # value used in is_unique? and see if the test object is not valid. - # - # This validation will only occur if the test object is a String. - # - def case_sensitive? - return true unless @value.is_a?(String) - assert_good_or_bad_if_key(:case_sensitive, @value.swapcase) - end - # Now test that the object is valid when changing the scoped attribute. # def valid_with_new_scope? @@ -173,7 +160,7 @@ def new_value_for_stringfiable_scope(scope) conditions = { scope => values, @attribute => @value } # Get values from the database, get the scope attribute and map them to string. - db_values = subject_class.find(:all, :conditions => conditions, :select => scope) + db_values = subject_class.all(:conditions => conditions, :fields => [scope]) db_values.map!{ |r| r.send(scope).to_s } if value_to_return = (values - db_values).first @@ -204,7 +191,6 @@ def new_value_for_stringfiable_scope(scope) # == Options # # * :scope - field(s) to scope the uniqueness to. - # * :case_sensitive - the matcher look for an exact match. # * :allow_nil - when supplied, validates if it allows nil or not. # * :allow_blank - when supplied, validates if it allows blank or not. # * :message - value the test expects to find in errors.on(:attribute). @@ -213,20 +199,19 @@ def new_value_for_stringfiable_scope(scope) # == Examples # # it { should validate_uniqueness_of(:keyword, :username) } - # it { should validate_uniqueness_of(:email, :scope => :name, :case_sensitive => false) } + # it { should validate_uniqueness_of(:email, :scope => :name) } # it { should validate_uniqueness_of(:address, :scope => [:first_name, :last_name]) } # # should_validate_uniqueness_of :keyword, :username - # should_validate_uniqueness_of :email, :scope => :name, :case_sensitive => false + # should_validate_uniqueness_of :email, :scope => :name # should_validate_uniqueness_of :address, :scope => [:first_name, :last_name] # # should_validate_uniqueness_of :email do |m| # m.scope = name - # m.case_sensitive = false # end # - def validates_is_unique(*attributes, &block) - ValidateUniquenessOfMatcher.new(*attributes, &block).spec(self) + def validate_is_unique(*attributes, &block) + ValidateIsUniqueMatcher.new(*attributes, &block).spec(self) end end end diff --git a/remarkable_datamapper/locale/en.yml b/remarkable_datamapper/locale/en.yml index 6ae06aa..cc0d76f 100644 --- a/remarkable_datamapper/locale/en.yml +++ b/remarkable_datamapper/locale/en.yml @@ -248,25 +248,12 @@ en: expectations: allow_nil: "{{subject_name}} to require {{attribute}} to be set" - validate_uniqueness_of: - description: "require unique values for {{attributes}}" - expectations: - responds_to_scope: "{{subject_name}} instance responds to {{method}}" - is_unique: "{{subject_name}} to require unique values for {{attribute}}" - case_sensitive: "{{subject_name}} to {{not}}be case sensitive on {{attribute}} validation" - valid_with_new_scope: "{{subject_name}} to be valid when {{attribute}} scope ({{method}}) change" - optionals: - scope: - positive: "scoped to {{sentence}}" - case_sensitive: - positive: "case sensitive" - negative: "case insensitive" - - validates_is_unique: + validate_is_unique: description: "require unique values for {{attributes}}" expectations: responds_to_scope: "{{subject_name}} instance responds to {{method}}" is_unique: "{{subject_name}} to require unique values for {{attribute}}" + nullable: "{{subject_name}} to require {{attribute}} to be set" case_sensitive: "{{subject_name}} to {{not}}be case sensitive on {{attribute}} validation" valid_with_new_scope: "{{subject_name}} to be valid when {{attribute}} scope ({{method}}) change" optionals: diff --git a/remarkable_datamapper/spec/describe_spec.rb b/remarkable_datamapper/spec/describe_spec.rb deleted file mode 100644 index 66cb2bd..0000000 --- a/remarkable_datamapper/spec/describe_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/spec_helper') - -I18N = true # specs won't run unless false - -class Post - attr_accessor :published, :public, :deleted - - def attributes=(attributes={}, guard=true) - attributes.each do |key, value| - send(:"#{key}=", value) unless guard - end - end - - def self.human_name(*args) - "MyPost" - end -end - -describe Post do - it "should use human name on description" do - self.class.description.should == "MyPost" - end - - describe "default attributes as a hash" do - subject_attributes :deleted => true - - it "should set the subject with deleted equals to true" do - subject.deleted.should be_true - end - - it "should not change the description" do - self.class.description.should == "MyPost default attributes as a hash" - end - end - - describe "default attributes as a proc" do - subject_attributes { my_attributes } - - it "should set the subject with deleted equals to true" do - subject.deleted.should be_true - end - - it "should not change the description" do - self.class.description.should == "MyPost default attributes as a proc" - end - - def my_attributes - { :deleted => true } - end - end - - describe :published => true do - it "should set the subject with published equals to true" do - subject.published.should be_true - end - - it "should generate a readable description" do - self.class.description.should == "MyPost when published is true" - end - - it "should call human name attribute on the described class" do - Post.should_receive(:human_attribute_name).with("comments_count", :locale => :en).and_return("__COMMENTS__COUNT__") - self.class.describe(:comments_count => 5) do - self.description.should == 'MyPost when published is true and __comments__count__ is 5' - end - end - - describe :public => false do - it "should nest subject attributes" do - subject.published.should be_true - subject.public.should be_false - end - - it "should nest descriptions" do - #self.class.describe_subject_attributes.should == "blah" - self.class.description.should == "MyPost when published is true and public is false" - end - - describe "default attributes as a hash" do - subject_attributes :deleted => true - - it "should merge describe attributes with subject attributes" do - subject.published.should be_true - subject.public.should be_false - subject.deleted.should be_true - end - end - end - end - - describe :published => true, :public => false do - it "should set the subject with published equals to true and public equals to false" do - subject.published.should be_true - subject.public.should be_false - end - - it "should include both published and public in descriptions" do - self.class.description.should match(/MyPost/) - self.class.description.should match(/public is false/) - self.class.description.should match(/published is true/) - end - end -end diff --git a/remarkable_datamapper/spec/model_builder.rb b/remarkable_datamapper/spec/model_builder.rb index e1c32e1..36e2769 100644 --- a/remarkable_datamapper/spec/model_builder.rb +++ b/remarkable_datamapper/spec/model_builder.rb @@ -1,7 +1,7 @@ # This is based on Shoulda model builder for Test::Unit. # -# TODO: !!! These functions are +# TODO: !!! These functions are not all updated yet module ModelBuilder def self.included(base) return unless base.name =~ /^Spec/ @@ -14,29 +14,30 @@ def self.included(base) end end - DataMapper.auto_migrate! - #if @created_tables - # @created_tables.each do |table_name| - # ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS #{table_name}") - # end - #end + if @created_tables + @created_tables.each do |table_name| + DataMapper::Repository.adapters[:default].execute("DROP TABLE IF EXISTS #{table_name}") + end + end end end base.extend ClassMethods end - def create_table(table_name, &block) - connection = ActiveRecord::Base.connection + def create_table(model) + adapter = DataMapper::Repository.adapters[:default] + table_name = model.to_s.tableize + command = "DROP TABLE IF EXISTS #{table_name}" begin - connection.execute("DROP TABLE IF EXISTS #{table_name}") - connection.create_table(table_name, &block) + adapter.execute(command) + adapter.create_model_storage(model) @created_tables ||= [] @created_tables << table_name - connection + adapter rescue Exception => e - connection.execute("DROP TABLE IF EXISTS #{table_name}") + adapter.execute(command) raise e end end @@ -45,8 +46,8 @@ def define_constant(class_name, base, &block) class_name = class_name.to_s.camelize klass = Class.new - klass.include base - Object.const_set(class_name, klass) + klass.send :include, base + Object.const_set(class_name, klass) #unless klass klass.class_eval(&block) if block_given? @@ -63,18 +64,18 @@ def define_model_class(class_name, &block) def define_model(name, columns = {}, &block) class_name = name.to_s.pluralize.classify table_name = class_name.tableize - - table = columns.delete(:table) || lambda {|table| - columns.each do |name, type| - table.column name, *type - end - } - - create_table(table_name, &table) - klass = define_model_class(class_name, &block) + pp columns # TODO: REMOVE debug line + columns.each do |name, type| + options = {} + type, options = type if type.class == Array + klass.property(name, type, options) + end + instance = klass.new + create_table(klass) + self.class.subject { instance } if self.class.respond_to?(:subject) instance end diff --git a/remarkable_datamapper/spec/validates_is_unique_matcher_spec.rb b/remarkable_datamapper/spec/validates_is_unique_matcher_spec.rb new file mode 100644 index 0000000..fb82781 --- /dev/null +++ b/remarkable_datamapper/spec/validates_is_unique_matcher_spec.rb @@ -0,0 +1,155 @@ +require File.expand_path(File.dirname(__FILE__) + '/spec_helper') + +describe 'validate_is_unique' do + include ModelBuilder + + # Defines a model, create a validation and returns a raw matcher + def define_and_validate(options={}) + @model = define_model :user, :id => DataMapper::Types::Serial, :username => String, :email => String, :public => DataMapper::Types::Boolean, :deleted_at => DateTime do + validates_is_unique :username, options + end + + # Create a model + User.create(:username => 'jose', :deleted_at => 1.day.ago, :public => false) + + validate_is_unique(:username) + end + + describe 'messages' do + before(:each){ @matcher = define_and_validate } + + it 'should contain a description' do + @matcher.description.should == 'require unique values for username' + + @matcher.nullable + @matcher.description.should == 'require unique values for username allowing nil values' + + @matcher = validate_is_unique(:username, :scope => :email) + @matcher.description.should == 'require unique values for username scoped to :email' + + @matcher = validate_is_unique(:username) + @matcher.scope(:email) + @matcher.scope(:public) + @matcher.description.should == 'require unique values for username scoped to :email and :public' + end + + it 'should set responds_to_scope? message' do + @matcher.scope(:title).matches?(@model) + @matcher.failure_message.should == 'Expected User instance responds to title=' + end + + it 'should set is_unique? message' do + @matcher = validate_is_unique(:email) + @matcher.matches?(@model) + @matcher.failure_message.should == 'Expected User to require unique values for email' + end + + it 'should valid with new scope' do + @matcher.scope(:email).matches?(@model) + @matcher.failure_message.should == 'Expected User to be valid when username scope (email) change' + end + end + + describe 'matcher' do + + describe 'without options' do + before(:each){ define_and_validate } + + it { should validate_is_unique(:username) } + it { should_not validate_is_unique(:email) } + end + + describe 'scoped to' do + it { should define_and_validate(:scope => :email).scope(:email) } + it { should define_and_validate(:scope => :public).scope(:public) } + it { should define_and_validate(:scope => :deleted_at).scope(:deleted_at) } + it { should define_and_validate(:scope => [:email, :public]).scope(:email, :public) } + it { should define_and_validate(:scope => [:email, :public, :deleted_at]).scope(:email, :public, :deleted_at) } + it { should_not define_and_validate(:scope => :email).scope(:title) } + it { should_not define_and_validate(:scope => :email).scope(:public) } + end + + create_message_specs(self) + + # Those are macros to test optionals which accept only boolean values + create_optional_boolean_specs(:nullable, self) + end + + describe 'errors' do + it 'should raise an error if no object is found' do + @matcher = define_and_validate + User.all.destroy + + proc { @matcher.matches?(@model) }.should raise_error(ScriptError) + end + + it 'should raise an error if no object with not nil attribute is found' do + @matcher = define_and_validate.nullable + User.all.destroy + + User.create(:username => nil) + proc { @matcher.matches?(@model) }.should raise_error(ScriptError) + + User.create(:username => 'jose') + proc { @matcher.matches?(@model) }.should_not raise_error(ScriptError) + end + + it 'should raise an error if @existing record is the same as @subject' do + @matcher = define_and_validate + proc { @matcher.matches?(User.first) }.should raise_error(ScriptError, /which is different from the subject record/) + end + + it 'should raise an error if cannot find a new scope value' do + @matcher = define_and_validate(:scope => :email).scope(:email) + + User.stub!(:find).and_return do |many, conditions| + if many == :all + 1000.upto(1100).map{|i| User.new(:email => i) } + else + User.new(:username => 'jose') + end + end + lambda { @matcher.matches?(@model) }.should raise_error(ScriptError) + + User.stub!(:find).and_return do |many, conditions| + if many == :all + 1000.upto(1099).map{|i| User.new(:email => i) } + else + User.new(:username => 'jose') + end + end + lambda { @matcher.matches?(@model) }.should_not raise_error(ScriptError) + end + + describe 'when null values are not allowed' do + def define_and_validate(options={}) + @model = define_model :user, :id => DataMapper::Types::Serial, :username => [String, {:nullable => false}] do + validates_is_unique :username, options + end + + User.create(:username => 'jose') + validate_is_unique(:username) + end + + it { should define_and_validate } + it { should define_and_validate(:nullable => false).nullable(false) } + + it 'should raise an error if nullable is true but we cannot save nil values in the database'do + lambda { should define_and_validate.nullable }.should raise_error(ScriptError, /You declared that username accepts nil values in validate_is_unique, but I cannot save nil values in the database, got/) + end + end + end + + describe 'macros' do + before(:each){ define_and_validate(:scope => :email) } + + should_validate_is_unique :username + should_validate_is_unique :username, :scope => :email + should_not_validate_is_unique :email + should_not_validate_is_unique :username, :scope => :access_code + + should_validate_is_unique :username do |m| + m.scope :email + end + end +end