From c59c15123d30d4775ddcacfa0e67aa4991089ac5 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Tue, 1 Sep 2009 12:54:05 -0700 Subject: [PATCH 01/14] Initial commit for remarkable_datamapper. --- remarkable_datamapper/Rakefile | 30 +++ .../lib/remarkable_datamapper.rb | 30 +++ .../lib/remarkable_datamapper/base.rb | 248 ++++++++++++++++++ .../lib/remarkable_datamapper/describe.rb | 199 ++++++++++++++ .../lib/remarkable_datamapper/human_names.rb | 37 +++ 5 files changed, 544 insertions(+) create mode 100644 remarkable_datamapper/Rakefile create mode 100644 remarkable_datamapper/lib/remarkable_datamapper.rb create mode 100644 remarkable_datamapper/lib/remarkable_datamapper/base.rb create mode 100644 remarkable_datamapper/lib/remarkable_datamapper/describe.rb create mode 100644 remarkable_datamapper/lib/remarkable_datamapper/human_names.rb diff --git a/remarkable_datamapper/Rakefile b/remarkable_datamapper/Rakefile new file mode 100644 index 0000000..ab4aa94 --- /dev/null +++ b/remarkable_datamapper/Rakefile @@ -0,0 +1,30 @@ +# encoding: utf-8 +PROJECT_SUMMARY = "Remarkable DataMapper: collection of matchers and macros with I18n for DataMapper" +PROJECT_DESCRIPTION = PROJECT_SUMMARY + +GEM_NAME = "remarkable_datamapper" +GEM_AUTHOR = [ "Carlos Brando", "José Valim", "Diego Carrion", "Blake Gentry" ] +GEM_EMAIL = [ "eduardobrando@gmail.com", "jose.valim@gmail.com", "dc.rec1@gmail.com", "blakesgentry@gmail.com" ] + +EXTRA_RDOC_FILES = ["README", "LICENSE", "CHANGELOG"] + +require File.join(File.dirname(__FILE__), "..", "rake_helpers.rb") + +########### Package && release + +configure_gemspec! do |s| + s.add_dependency('remarkable', "~> #{GEM_VERSION}") +end + +########### Specs + +RAILS_VERSIONS = ['2.1.2', '2.2.2', '2.3.2', '2.3.3'] + +desc "Run the specs under spec with supported Rails versions" +task :pre_commit do + RAILS_VERSIONS.each do |version| + ENV['RAILS_VERSION'] = version + puts "\n=> #{GEM_NAME}: rake spec RAILS_VERSION=#{version}" + Rake::Task[:spec].execute + end +end diff --git a/remarkable_datamapper/lib/remarkable_datamapper.rb b/remarkable_datamapper/lib/remarkable_datamapper.rb new file mode 100644 index 0000000..d8ad50e --- /dev/null +++ b/remarkable_datamapper/lib/remarkable_datamapper.rb @@ -0,0 +1,30 @@ +# Load Remarkable +unless Object.const_defined?('Remarkable') + begin + require 'remarkable' + rescue LoadError + require 'rubygems' + gem 'remarkable' + require 'remarkable' + end +end + +# Load Remarkable ActiveRecord files +dir = File.dirname(__FILE__) +require File.join(dir, 'remarkable_datamapper', 'base') +require File.join(dir, 'remarkable_datamapper', 'describe') +require File.join(dir, 'remarkable_datamapper', 'human_names') + +# Add locale +Remarkable.add_locale File.join(dir, '..', 'locale', 'en.yml') + +# Add matchers +Dir[File.join(dir, 'remarkable_datamapper', 'matchers', '*.rb')].each do |file| + require file +end + +# By default, ActiveRecord matchers are not included in any example group. +# The responsable for this is RemarkableRails. If you are using ActiveRecord +# without Rails, put the line below in your spec_helper to include ActiveRecord +# matchers into rspec globally. +# Remarkable.include_matchers!(Remarkable::ActiveRecord, Spec::Example::ExampleGroup) diff --git a/remarkable_datamapper/lib/remarkable_datamapper/base.rb b/remarkable_datamapper/lib/remarkable_datamapper/base.rb new file mode 100644 index 0000000..9736e6d --- /dev/null +++ b/remarkable_datamapper/lib/remarkable_datamapper/base.rb @@ -0,0 +1,248 @@ +module Remarkable + module DataMapper + class Base < Remarkable::Base + I18N_COLLECTION = [ :attributes, :associations ] + + # Provides a way to send options to all DataMapper matchers. + # + # validates_presence_of(:name).with_options(:allow_nil => false) + # + # Is equivalent to: + # + # validates_presence_of(:name, :allow_nil => false) + # + def with_options(opts={}) + @options.merge!(opts) + self + end + + protected + + # Overwrite subject_name to provide I18n. + # + def subject_name + nil unless @subject + if subject_class.respond_to?(:human_name) + subject_class.human_name(:locale => Remarkable.locale) + else + subject_class.name + end + end + + # Checks for the given key in @options, if it exists and it's true, + # tests that the value is bad, otherwise tests that the value is good. + # + # It accepts the key to check for, the value that is used for testing + # and an @options key where the message to search for is. + # + def assert_bad_or_good_if_key(key, value, message_key=:message) #:nodoc: + return positive? unless @options.key?(key) + + if @options[key] + return bad?(value, message_key), :not => not_word + else + return good?(value, message_key), :not => '' + end + end + + # Checks for the given key in @options, if it exists and it's true, + # tests that the value is good, otherwise tests that the value is bad. + # + # It accepts the key to check for, the value that is used for testing + # and an @options key where the message to search for is. + # + def assert_good_or_bad_if_key(key, value, message_key=:message) #:nodoc: + return positive? unless @options.key?(key) + + if @options[key] + return good?(value, message_key), :not => '' + else + return bad?(value, message_key), :not => not_word + end + end + + # Default allow_nil? validation. It accepts the message_key which is + # the key which contain the message in @options. + # + # It also gets an allow_nil message on remarkable.active_record.allow_nil + # to be used as default. + # + def allow_nil?(message_key=:message) #:nodoc: + assert_good_or_bad_if_key(:allow_nil, nil, message_key) + end + + # Default allow_blank? validation. It accepts the message_key which is + # the key which contain the message in @options. + # + # It also gets an allow_blank message on remarkable.active_record.allow_blank + # to be used as default. + # + def allow_blank?(message_key=:message) #:nodoc: + assert_good_or_bad_if_key(:allow_blank, '', message_key) + end + + # Shortcut for assert_good_value. + # + def good?(value, message_sym=:message) #:nodoc: + assert_good_value(@subject, @attribute, value, @options[message_sym]) + end + + # Shortcut for assert_bad_value. + # + def bad?(value, message_sym=:message) #:nodoc: + assert_bad_value(@subject, @attribute, value, @options[message_sym]) + end + + # Asserts that an Active Record model validates with the passed + # value by making sure the error_message_to_avoid is not + # contained within the list of errors for that attribute. + # + # assert_good_value(User.new, :email, "user@example.com") + # assert_good_value(User.new, :ssn, "123456789", /length/) + # + # If a class is passed as the first argument, a new object will be + # instantiated before the assertion. If an instance variable exists with + # the same name as the class (underscored), that object will be used + # instead. + # + # assert_good_value(User, :email, "user@example.com") + # + # @product = Product.new(:tangible => false) + # assert_good_value(Product, :price, "0") + # + def assert_good_value(model, attribute, value, error_message_to_avoid=//) # :nodoc: + model.send("#{attribute}=", value) + + return true if model.valid? + + error_message_to_avoid = error_message_from_model(model, attribute, error_message_to_avoid) + assert_does_not_contain(model.errors.on(attribute), error_message_to_avoid) + end + + # Asserts that an Active Record model invalidates the passed + # value by making sure the error_message_to_expect is + # contained within the list of errors for that attribute. + # + # assert_bad_value(User.new, :email, "invalid") + # assert_bad_value(User.new, :ssn, "123", /length/) + # + # If a class is passed as the first argument, a new object will be + # instantiated before the assertion. If an instance variable exists with + # the same name as the class (underscored), that object will be used + # instead. + # + # assert_bad_value(User, :email, "invalid") + # + # @product = Product.new(:tangible => true) + # assert_bad_value(Product, :price, "0") + # + def assert_bad_value(model, attribute, value, error_message_to_expect=:invalid) #:nodoc: + model.send("#{attribute}=", value) + + return false if model.valid? || model.errors.on(attribute).blank? + + error_message_to_expect = error_message_from_model(model, attribute, error_message_to_expect) + assert_contains(model.errors.on(attribute), error_message_to_expect) + end + + # Return the error message to be checked. If the message is not a Symbol + # neither a Hash, it returns the own message. + # + # But the nice thing is that when the message is a Symbol we get the error + # messsage from within the model, using already existent structure inside + # DataMapper. + # + # This allows a couple things from the user side: + # + # 1. Specify symbols in their tests: + # + # should_allow_values_for(:shirt_size, 'S', 'M', 'L', :message => :inclusion) + # + # As we know, allow_values_for searches for a :invalid message. So if we + # were testing a validates_inclusion_of with allow_values_for, previously + # we had to do something like this: + # + # should_allow_values_for(:shirt_size, 'S', 'M', 'L', :message => 'not included in list') + # + # Now everything gets resumed to a Symbol. + # + # 2. Do not worry with specs if their are using I18n API properly. + # + # As we know, I18n API provides several interpolation options besides + # fallback when creating error messages. If the user changed the message, + # macros would start to pass when they shouldn't. + # + # Using the underlying mechanism inside DataMapper makes us free from + # all thos errors. + # + # We replace {{count}} interpolation for 12345 which later is replaced + # by a regexp which contains \d+. + # + def error_message_from_model(model, attribute, message) #:nodoc: + if message.is_a? Symbol + message = if RAILS_I18N # Rails >= 2.2 + model.errors.generate_message(attribute, message, :count => '12345') + else # Rails <= 2.1 + ::DataMapper::Errors.default_error_messages[message] % '12345' + end + + if message =~ /12345/ + message = Regexp.escape(message) + message.gsub!('12345', '\d+') + message = /#{message}/ + end + end + + message + end + + # Asserts that the given collection does not contain item x. If x is a + # regular expression, ensure that none of the elements from the collection + # match x. + # + def assert_does_not_contain(collection, x) #:nodoc: + !assert_contains(collection, x) + end + + # Changes how collection are interpolated to provide localized names + # whenever is possible. + # + def collection_interpolation #:nodoc: + described_class = if @subject + subject_class + elsif @spec + @spec.send(:described_class) + end + + if i18n_collection? && described_class.respond_to?(:human_attribute_name) + options = {} + + collection_name = self.class.matcher_arguments[:collection].to_sym + if collection = instance_variable_get("@#{collection_name}") + collection = collection.map do |attr| + described_class.human_attribute_name(attr.to_s, :locale => Remarkable.locale).downcase + end + options[collection_name] = array_to_sentence(collection) + end + + object_name = self.class.matcher_arguments[:as] + if object = instance_variable_get("@#{object_name}") + object = described_class.human_attribute_name(object.to_s, :locale => Remarkable.locale).downcase + options[object_name] = object + end + + options + else + super + end + end + + # Returns true if the given collection should be translated. + # + def i18n_collection? #:nodoc: + RAILS_I18N && I18N_COLLECTION.include?(self.class.matcher_arguments[:collection]) + end + + end + end +end diff --git a/remarkable_datamapper/lib/remarkable_datamapper/describe.rb b/remarkable_datamapper/lib/remarkable_datamapper/describe.rb new file mode 100644 index 0000000..2ffcc5e --- /dev/null +++ b/remarkable_datamapper/lib/remarkable_datamapper/describe.rb @@ -0,0 +1,199 @@ +module Remarkable + module DataMapper + + def self.after_include(target) #:nodoc: + target.class_inheritable_reader :describe_subject_attributes, :default_subject_attributes + target.send :include, Describe + end + + # Overwrites describe to provide quick way to configure your subject: + # + # describe Post + # should_validate_presente_of :title + # + # describe :published => true do + # should_validate_presence_of :published_at + # end + # end + # + # This is the same as: + # + # describe Post + # should_validate_presente_of :title + # + # describe "when published is true" do + # subject { Post.new(:published => true) } + # should_validate_presence_of :published_at + # end + # end + # + # The string can be localized using I18n. An example yml file is: + # + # locale: + # remarkable: + # data_mapper: + # describe: + # each: "{{key}} is {{value}}" + # prepend: "when " + # connector: " and " + # + # You can also call subject attributes to set the default attributes for a + # subject. You can even mix with a fixture replacement tool: + # + # describe Post + # # Fixjour example + # subject_attributes { valid_post_attributes } + # + # describe :published => true do + # should_validate_presence_of :published_at + # end + # end + # + # You can retrieve the merged result of all attributes given using the + # subject_attributes instance method: + # + # describe Post + # # Fixjour example + # subject_attributes { valid_post_attributes } + # + # describe :published => true do + # it "should have default subject attributes" do + # subject_attributes.should == { :title => 'My title', :published => true } + # end + # end + # end + # + module Describe + + def self.included(base) #:nodoc: + base.extend ClassMethods + end + + module ClassMethods + + # Overwrites describe to provide quick way to configure your subject: + # + # describe Post + # should_validate_presente_of :title + # + # describe :published => true do + # should_validate_presence_of :published_at + # end + # end + # + # This is the same as: + # + # describe Post + # should_validate_presente_of :title + # + # describe "when published is true" do + # subject { Post.new(:published => true) } + # should_validate_presence_of :published_at + # end + # end + # + # The string can be localized using I18n. An example yml file is: + # + # locale: + # remarkable: + # data_mapper: + # describe: + # each: "{{key}} is {{value}}" + # prepend: "when " + # connector: " and " + # + # See also subject_attributes instance and class methods for more + # information. + # + def describe(*args, &block) + if described_class && args.first.is_a?(Hash) + attributes = args.shift + + connector = Remarkable.t "remarkable.data_mapper.describe.connector", :default => " and " + + description = if self.describe_subject_attributes.blank? + Remarkable.t("remarkable.data_mapper.describe.prepend", :default => "when ") + else + connector.lstrip + end + + pieces = [] + attributes.each do |key, value| + translated_key = if described_class.respond_to?(:human_attribute_name) + described_class.human_attribute_name(key.to_s, :locale => Remarkable.locale) + else + key.to_s.humanize + end + + pieces << Remarkable.t("remarkable.data_mapper.describe.each", + :default => "{{key}} is {{value}}", + :key => translated_key.downcase, :value => value.inspect) + end + + description << pieces.join(connector) + args.unshift(description) + + # Creates an example group, set the subject and eval the given block. + # + example_group = super(*args) do + write_inheritable_hash(:describe_subject_attributes, attributes) + set_described_subject! + instance_eval(&block) + end + else + super(*args, &block) + end + end + + # Sets default attributes for the subject. You can use this to set up + # your subject with valid attributes. You can even mix with a fixture + # replacement tool and still use quick subjects: + # + # describe Post + # # Fixjour example + # subject_attributes { valid_post_attributes } + # + # describe :published => true do + # should_validate_presence_of :published_at + # end + # end + # + def subject_attributes(options=nil, &block) + write_inheritable_attribute(:default_subject_attributes, options || block) + set_described_subject! + end + + def set_described_subject! + subject { + record = self.class.described_class.new + record.send(:attributes=, subject_attributes, false) + record + } + end + end + + # Returns a hash with the subject attributes declared using the + # subject_attributes class method and the attributes given using the + # describe method. + # + # describe Post + # subject_attributes { valid_post_attributes } + # + # describe :published => true do + # it "should have default subject attributes" do + # subject_attributes.should == { :title => 'My title', :published => true } + # end + # end + # end + # + def subject_attributes + default = self.class.default_subject_attributes + default = self.instance_eval(&default) if default.is_a?(Proc) + default ||= {} + + default.merge(self.class.describe_subject_attributes || {}) + end + + end + end +end diff --git a/remarkable_datamapper/lib/remarkable_datamapper/human_names.rb b/remarkable_datamapper/lib/remarkable_datamapper/human_names.rb new file mode 100644 index 0000000..3d1c8a4 --- /dev/null +++ b/remarkable_datamapper/lib/remarkable_datamapper/human_names.rb @@ -0,0 +1,37 @@ +if defined?(Spec) + module Spec #:nodoc: + module Example #:nodoc: + module ExampleGroupMethods #:nodoc: + + # This allows "describe User" to use the I18n human name of User. + # + def self.build_description_with_i18n(*args) + args.inject("") do |description, arg| + arg = if arg.respond_to?(:human_name) + arg.human_name(:locale => Remarkable.locale) + else + arg.to_s + end + + description << " " unless (description == "" || arg =~ /^(\s|\.|#)/) + description << arg + end + end + + # This is for rspec <= 1.1.12. + # + def self.description_text(*args) + self.build_description_with_i18n(*args) + end + + # This is for rspec >= 1.2.0. + # + def self.build_description_from(*args) + text = ExampleGroupMethods.build_description_with_i18n(*args) + text == "" ? nil : text + end + + end + end + end +end From 61bd7d91051fbb958478e2cb018bad677d397f0e Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Tue, 1 Sep 2009 16:02:21 -0700 Subject: [PATCH 02/14] More initial changes. --- .../lib/remarkable_datamapper/base.rb | 10 +- .../matchers/validates_is_unique.rb | 233 ++++++++++++++++ remarkable_datamapper/locale/en.yml | 264 ++++++++++++++++++ remarkable_datamapper/spec/model_builder.rb | 104 +++++++ remarkable_datamapper/spec/rcov.opts | 2 + remarkable_datamapper/spec/spec.opts | 4 + remarkable_datamapper/spec/spec_helper.rb | 54 ++++ 7 files changed, 666 insertions(+), 5 deletions(-) create mode 100644 remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb create mode 100644 remarkable_datamapper/locale/en.yml create mode 100644 remarkable_datamapper/spec/model_builder.rb create mode 100644 remarkable_datamapper/spec/rcov.opts create mode 100644 remarkable_datamapper/spec/spec.opts create mode 100644 remarkable_datamapper/spec/spec_helper.rb diff --git a/remarkable_datamapper/lib/remarkable_datamapper/base.rb b/remarkable_datamapper/lib/remarkable_datamapper/base.rb index 9736e6d..ca5d73d 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/base.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/base.rb @@ -64,7 +64,7 @@ def assert_good_or_bad_if_key(key, value, message_key=:message) #:nodoc: # Default allow_nil? validation. It accepts the message_key which is # the key which contain the message in @options. # - # It also gets an allow_nil message on remarkable.active_record.allow_nil + # It also gets an allow_nil message on remarkable.data_mapper.allow_nil # to be used as default. # def allow_nil?(message_key=:message) #:nodoc: @@ -74,7 +74,7 @@ def allow_nil?(message_key=:message) #:nodoc: # Default allow_blank? validation. It accepts the message_key which is # the key which contain the message in @options. # - # It also gets an allow_blank message on remarkable.active_record.allow_blank + # It also gets an allow_blank message on remarkable.data_mapper.allow_blank # to be used as default. # def allow_blank?(message_key=:message) #:nodoc: @@ -93,7 +93,7 @@ def bad?(value, message_sym=:message) #:nodoc: assert_bad_value(@subject, @attribute, value, @options[message_sym]) end - # Asserts that an Active Record model validates with the passed + # Asserts that a DataMapper model validates with the passed # value by making sure the error_message_to_avoid is not # contained within the list of errors for that attribute. # @@ -119,7 +119,7 @@ def assert_good_value(model, attribute, value, error_message_to_avoid=//) # :nod assert_does_not_contain(model.errors.on(attribute), error_message_to_avoid) end - # Asserts that an Active Record model invalidates the passed + # Asserts that a DataMapper model invalidates the passed # value by making sure the error_message_to_expect is # contained within the list of errors for that attribute. # @@ -173,7 +173,7 @@ def assert_bad_value(model, attribute, value, error_message_to_expect=:invalid) # macros would start to pass when they shouldn't. # # Using the underlying mechanism inside DataMapper makes us free from - # all thos errors. + # all those errors. # # We replace {{count}} interpolation for 12345 which later is replaced # by a regexp which contains \d+. diff --git a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb new file mode 100644 index 0000000..2aba3ca --- /dev/null +++ b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb @@ -0,0 +1,233 @@ +module Remarkable + module DataMapper + module Matchers + class ValidatesIsUniqueMatcher < Remarkable::DataMapper::Base #:nodoc: + arguments :collection => :attributes, :as => :attribute + + optional :message + optional :scope, :splat => true + optional :case_sensitive, :allow_nil, :allow_blank, :default => true + + collection_assertions :find_first_object?, :responds_to_scope?, :is_unique?, :case_sensitive?, + :valid_with_new_scope?, :allow_nil?, :allow_blank? + + default_options :message => :taken + + before_assert do + @options[:scope] = [*@options[:scope]].compact if @options[:scope] + end + + private + + # Tries to find an object in the database. If allow_nil and/or allow_blank + # is given, we must find a record which is not nil or not blank. + # + # We should also ensure that the object retrieved from the database + # is not the @subject. + # + # 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 + [ [], "" ] + 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)}'" + end + + options = conditions.empty? ? {} : { :conditions => conditions.join(' AND ') } + + return true if @existing = subject_class.find(:first, options) + raise ScriptError, "could not find a #{subject_class} record in the database" + message + end + + # Set subject scope to be equal to the object found. + # + def responds_to_scope? + (@options[:scope] || []).each do |scope| + setter = :"#{scope}=" + + return false, :method => setter unless @subject.respond_to?(setter) + return false, :method => scope unless @existing.respond_to?(scope) + + @subject.send(setter, @existing.send(scope)) + end + true + end + + # Check if the attribute given is valid and if the validation fails for equal values. + # + def is_unique? + @value = @existing.send(@attribute) + 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? + (@options[:scope] || []).each do |scope| + setter = :"#{scope}=" + + previous_scope_value = @subject.send(scope) + @subject.send(setter, new_value_for_scope(scope)) + return false, :method => scope unless good?(@value) + + @subject.send(setter, previous_scope_value) + end + true + end + + # Change the existing object attribute to nil to run allow nil + # validations. If we find any problem while updating the @existing + # record, it's because we can't save nil values in the database. So it + # passes when :allow_nil is false, but should raise an error when + # :allow_nil is true + # + def allow_nil? + return true unless @options.key?(:allow_nil) + + begin + @existing.update_attribute(@attribute, nil) + rescue ::ActiveRecord::StatementInvalid => e + raise ScriptError, "You declared that #{@attribute} accepts nil values in validate_uniqueness_of, " << + "but I cannot save nil values in the database, got: #{e.message}" if @options[:allow_nil] + return true + end + + super + end + + # Change the existing object attribute to blank to run allow blank + # validation. It uses the same logic as :allow_nil. + # + def allow_blank? + return true unless @options.key?(:allow_blank) + + begin + @existing.update_attribute(@attribute, '') + rescue ::ActiveRecord::StatementInvalid => e + raise ScriptError, "You declared that #{@attribute} accepts blank values in validate_uniqueness_of, " << + "but I cannot save blank values in the database, got: #{e.message}" if @options[:allow_blank] + return true + end + + super + end + + # Returns a value to be used as new scope. It deals with four different + # cases: date, time, boolean and stringfiable (everything that can be + # converted to a string and the next value makes sense) + # + def new_value_for_scope(scope) + column_type = if @existing.respond_to?(:column_for_attribute) + @existing.column_for_attribute(scope) + else + nil + end + + case column_type.type + when :int, :integer, :float, :decimal + new_value_for_stringfiable_scope(scope) + when :datetime, :timestamp, :time + Time.now + 10000 + when :date + Date.today + 100 + when :boolean + !@existing.send(scope) + else + new_value_for_stringfiable_scope(scope) + end + end + + # Returns a value to be used as scope by generating a range of values + # and searching for them in the database. + # + def new_value_for_stringfiable_scope(scope) + values = [(@existing.send(scope) || 999).next.to_s] + + # Generate a range of values to search in the database + 100.times do + values << values.last.next + end + 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.map!{ |r| r.send(scope).to_s } + + if value_to_return = (values - db_values).first + value_to_return + else + raise ScriptError, "Tried to find an unique scope value for #{scope} but I could not. " << + "The conditions hash was #{conditions.inspect} and it returned all records." + end + end + end + + # Ensures that the model cannot be saved if one of the attributes listed + # is not unique. + # + # Requires an existing record in the database. If you supply :allow_nil as + # option, you need to have in the database a record which is not nil in the + # given attributes. The same is required for allow_blank option. + # + # Notice that the record being validate should not be the same as in the + # database. In other words, you can't do this: + # + # subject { Post.create!(@valid_attributes) } + # should_validate_uniqueness_of :title + # + # But don't worry, if you eventually do that, a helpful error message + # will be raised. + # + # == 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). + # Regexp, string or symbol. Default = I18n.translate('activerecord.errors.messages.taken') + # + # == Examples + # + # it { should validate_uniqueness_of(:keyword, :username) } + # it { should validate_uniqueness_of(:email, :scope => :name, :case_sensitive => false) } + # 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 :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) + end + end + end +end diff --git a/remarkable_datamapper/locale/en.yml b/remarkable_datamapper/locale/en.yml new file mode 100644 index 0000000..fd0e3b9 --- /dev/null +++ b/remarkable_datamapper/locale/en.yml @@ -0,0 +1,264 @@ +en: + remarkable: + data_mapper: + describe: + each: "{{key}} is {{value}}" + prepend: "when " + connector: " and " + expectations: + allow_nil: "{{subject_name}} to {{not}}allow nil values for {{attribute}}" + allow_blank: "{{subject_name}} to {{not}}allow blank values for {{attribute}}" + optionals: + allow_nil: + positive: "allowing nil values" + negative: "not allowing nil values" + allow_blank: + positive: "allowing blank values" + negative: "not allowing blank values" + + accept_nested_attributes_for: + description: "accept nested attributes for {{associations}}" + expectations: + association_exists: "{{subject_name}} to have association {{association}}, but does not" + is_autosave: "{{subject_name}} to have association {{association}} with autosave true, got false" + responds_to_attributes: "{{subject_name}} to respond to :{{association}}_attributes=, but does not" + allows_destroy: "{{subject_name}} with allow destroy equals to {{allow_destroy}}, got {{actual}}" + accepts: "{{subject_name}} to accept attributes {{attributes}} for {{association}}, but does not" + rejects: "{{subject_name}} to reject attributes {{attributes}} for {{association}}, but does not" + optionals: + allow_destroy: + positive: "allowing destroy" + negative: "not allowing destroy" + accept: + positive: "accepting {{sentence}}" + reject: + positive: "rejecting {{sentence}}" + + allow_values_for: + description: "allow {{in}} as values for {{attributes}}" + expectations: + is_valid: "{{subject_name}} to be valid when {{attribute}} is set to {{value}}" + + allow_mass_assignment_of: + description: "allow mass assignment of {{attributes}}" + expectations: + allows: "{{subject_name}} to allow mass assignment ({{subject_name}} is protecting {{protected_attributes}})" + is_protected: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} is protecting {{attribute}})" + is_accessible: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} has not made {{attribute}} accessible)" + negative_expectations: + allows: "{{subject_name}} to allow mass assignment ({{subject_name}} made {{accessible_attributes}} accessible)" + is_protected: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} is not protecting {{attribute}})" + is_accessible: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} has made {{attribute}} accessible)" + + association: + belongs_to: belong to + has_many: have many + has_and_belongs_to_many: have and belong to many + has_one: have one + description: "{{macro}} {{associations}}" + expectations: + association_exists: "{{subject_name}} records {{macro}} {{association}}, but the association does not exist" + macro_matches: "{{subject_name}} records {{macro}} {{association}}, got {{subject_name}} records {{actual_macro}} {{association}}" + through_exists: "{{subject_name}} records {{macro}} {{association}} through {{through}}, through association does not exist" + source_exists: "{{subject_name}} records {{macro}} {{association}} through {{through}}, source association does not exist" + klass_exists: "{{subject_name}} records {{macro}} {{association}}, but the association class does not exist" + join_table_exists: "join table {{join_table}} to exist, but does not" + foreign_key_exists: "foreign key {{foreign_key}} to exist on {{foreign_key_table}}, but does not" + polymorphic_exists: "{{subject_table}} to have {{polymorphic_column}} as column, but does not" + counter_cache_exists: "{{reflection_table}} to have {{counter_cache_column}} as column, but does not" + options_match: "{{subject_name}} records {{macro}} {{association}} with options {{options}}, got {{actual}}" + optionals: + through: + positive: "through {{value}}" + source: + positive: "with source {{inspect}}" + source_type: + positive: "with source type {{inspect}}" + class_name: + positive: "with class name {{inspect}}" + foreign_key: + positive: "with foreign key {{inspect}}" + dependent: + positive: "with dependent {{inspect}}" + join_table: + positive: "with join table {{inspect}}" + uniq: + positive: "with unique records" + negative: "without unique records" + readonly: + positive: "with readonly records" + negative: "without readonly records" + validate: + positive: "validating associated records" + negative: "not validating associated records" + autosave: + positive: "autosaving associated records" + negative: "not autosaving associated records" + as: + positive: "through the polymorphic interface {{inspect}}" + counter_cache: + positive: "with counter cache {{inspect}}" + negative: "without counter cache" + select: + positive: "selecting {{inspect}}" + conditions: + positive: "with conditions {{inspect}}" + include: + positive: "including {{inspect}}" + group: + positive: "grouping by {{inspect}}" + having: + positive: "having {{inspect}}" + order: + positive: "with order {{inspect}}" + limit: + positive: "with limit {{inspect}}" + offset: + positive: "with offset {{inspect}}" + + have_column: + description: "have column(s) named {{columns}}" + expectations: + column_exists: "{{subject_name}} to have column named {{column}}" + options_match: "{{subject_name}} to have column {{column}} with options {{options}}, got {{actual}}" + optionals: + type: + positive: "with type {{inspect}}" + null: + positive: "allowing null values" + negative: "not allowing null values" + default: + positive: "with default value {{inspect}}" + negative: "with default value {{inspect}}" + limit: + positive: "with limit {{inspect}}" + + have_default_scope: + description: "have a default scope with {{options}}" + expectations: + options_match: "default scope with {{options}}, got {{actual}}" + + have_index: + description: "have index for column(s) {{columns}}" + expectations: + index_exists: "index {{column}} to exist on table {{table_name}}" + is_unique: "index on {{column}} with unique equals to {{unique}}, got {{actual}}" + optionals: + unique: + positive: "with unique values" + negative: "with non unique values" + table_name: + positive: "on table {{value}}" + + have_readonly_attributes: + description: "make {{attributes}} read-only" + expectations: + is_readonly: "{{subject_name}} to make {{attribute}} read-only, got {{actual}}" + + have_scope: + description: "have to scope itself to {{options}} when {{scope_name}} is called" + expectations: + is_scope: "{{scope_name}} when called on {{subject_name}} return an instance of ActiveRecord::NamedScope::Scope" + options_match: "{{scope_name}} when called on {{subject_name}} scope to {{options}}, got {{actual}}" + optionals: + with: + positive: "with {{inspect}} as argument" + + validate_acceptance_of: + description: "require {{attributes}} to be accepted" + expectations: + requires_acceptance: "{{subject_name}} to be invalid if {{attribute}} is not accepted" + accept_is_valid: "{{subject_name}} to be valid when {{attribute}} is accepted with value {{accept}}" + optionals: + accept: + positive: "with value {{inspect}}" + + validate_associated: + description: "require associated {{associations}} to be valid" + expectations: + is_valid: "{{subject_name}} to be invalid when {{association}} is invalid" + + validate_confirmation_of: + description: "require {{attributes}} to be confirmed" + expectations: + responds_to_confirmation: "{{subject_name}} instance responds to {{attribute}}_confirmation" + confirms: "{{subject_name}} to be valid only when {{attribute}} is confirmed" + + validate_exclusion_of: + description: "ensure exclusion of {{attributes}} in {{in}}" + expectations: + is_valid: "{{subject_name}} to be valid when {{attribute}} is set to {{value}}" + is_invalid: "{{subject_name}} to be invalid when {{attribute}} is set to {{value}}" + + validate_inclusion_of: + description: "ensure inclusion of {{attributes}} in {{in}}" + expectations: + is_valid: "{{subject_name}} to be valid when {{attribute}} is set to {{value}}" + is_invalid: "{{subject_name}} to be invalid when {{attribute}} is set to {{value}}" + + validate_length_of: + description: "ensure length of {{attributes}}" + expectations: + less_than_min_length: "{{subject_name}} to be invalid when {{attribute}} length is less than {{minimum}} characters" + exactly_min_length: "{{subject_name}} to be valid when {{attribute}} length is {{minimum}} characters" + more_than_max_length: "{{subject_name}} to be invalid when {{attribute}} length is more than {{maximum}} characters" + exactly_max_length: "{{subject_name}} to be valid when {{attribute}} length is {{maximum}} characters" + optionals: + within: + positive: "is within {{inspect}} characters" + maximum: + positive: "is maximum {{inspect}} characters" + minimum: + positive: "is minimum {{inspect}} characters" + is: + positive: "is equal to {{inspect}} characters" + with_kind_of: + positive: "with kind of {{value}}" + + validate_numericality_of: + description: "ensure numericality of {{attributes}}" + expectations: + only_numeric_values: "{{subject_name}} to allow only numeric values for {{attribute}}" + only_integer: "{{subject_name}} to {{not}}allow only integer values for {{attribute}}" + only_even: "{{subject_name}} to allow only even values for {{attribute}}" + only_odd: "{{subject_name}} to allow only odd values for {{attribute}}" + equals_to: "{{subject_name}} to be valid only when {{attribute}} is equal to {{count}}" + more_than_maximum: "{{subject_name}} to be invalid when {{attribute}} is greater than {{count}}" + less_than_minimum: "{{subject_name}} to be invalid when {{attribute}} is less than {{count}}" + optionals: + only_integer: + positive: "allowing only integer values" + odd: + positive: "allowing only odd values" + even: + positive: "allowing only even values" + equal_to: + positive: "is equal to {{inspect}}" + less_than: + positive: "is less than {{inspect}}" + greater_than: + positive: "is greater than {{inspect}}" + less_than_or_equal_to: + positive: "is less than or equal to {{inspect}}" + greater_than_or_equal_to: + positive: "is greater than or equal to {{inspect}}" + + validate_presence_of: + description: "require {{attributes}} to be set" + 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" + diff --git a/remarkable_datamapper/spec/model_builder.rb b/remarkable_datamapper/spec/model_builder.rb new file mode 100644 index 0000000..628f030 --- /dev/null +++ b/remarkable_datamapper/spec/model_builder.rb @@ -0,0 +1,104 @@ +# This is based on Shoulda model builder for Test::Unit. +# + +# TODO: !!! These functions are +module ModelBuilder + def self.included(base) + return unless base.name =~ /^Spec/ + + base.class_eval do + after(:each) do + if @defined_constants + @defined_constants.each do |class_name| + Object.send(:remove_const, class_name) + 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 + end + end + + base.extend ClassMethods + end + + def create_table(table_name, &block) + connection = ActiveRecord::Base.connection + + begin + connection.execute("DROP TABLE IF EXISTS #{table_name}") + connection.create_table(table_name, &block) + @created_tables ||= [] + @created_tables << table_name + connection + rescue Exception => e + connection.execute("DROP TABLE IF EXISTS #{table_name}") + raise e + end + end + + def define_constant(class_name, base, &block) + class_name = class_name.to_s.camelize + + klass = Class.new(base) + Object.const_set(class_name, klass) + + klass.class_eval(&block) if block_given? + + @defined_constants ||= [] + @defined_constants << class_name + + klass + end + + def define_model_class(class_name, &block) + define_constant(class_name, ActiveRecord::Base, &block) + end + + 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) + instance = klass.new + + self.class.subject { instance } if self.class.respond_to?(:subject) + instance + end + + module ClassMethods + # This is a macro to run validations of boolean optionals such as :allow_nil + # and :allow_blank. This macro tests all scenarios. The specs must have a + # define_and_validate method defined. + # + def create_optional_boolean_specs(optional, base, options={}) + base.describe "with #{optional} option" do + it { should define_and_validate(options.merge(optional => true)).send(optional) } + it { should define_and_validate(options.merge(optional => false)).send(optional, false) } + it { should_not define_and_validate(options.merge(optional => true)).send(optional, false) } + it { should_not define_and_validate(options.merge(optional => false)).send(optional) } + end + end + + def create_message_specs(base) + base.describe "with message option" do + it { should define_and_validate(:message => 'valid_message').message('valid_message') } + it { should_not define_and_validate(:message => 'not_valid').message('valid_message') } + end + end + end + +end + diff --git a/remarkable_datamapper/spec/rcov.opts b/remarkable_datamapper/spec/rcov.opts new file mode 100644 index 0000000..baf694c --- /dev/null +++ b/remarkable_datamapper/spec/rcov.opts @@ -0,0 +1,2 @@ +--exclude "spec/*,gems/*" +--rails \ No newline at end of file diff --git a/remarkable_datamapper/spec/spec.opts b/remarkable_datamapper/spec/spec.opts new file mode 100644 index 0000000..391705b --- /dev/null +++ b/remarkable_datamapper/spec/spec.opts @@ -0,0 +1,4 @@ +--colour +--format progress +--loadby mtime +--reverse diff --git a/remarkable_datamapper/spec/spec_helper.rb b/remarkable_datamapper/spec/spec_helper.rb new file mode 100644 index 0000000..ab0103c --- /dev/null +++ b/remarkable_datamapper/spec/spec_helper.rb @@ -0,0 +1,54 @@ +# encoding: utf-8 +require 'rubygems' + +RAILS_VERSION = ENV['RAILS_VERSION'] || '2.3.3' +DM_VERSION = '0.10.0' + +gem 'activesupport', RAILS_VERSION +require 'active_support' + +gem 'addressable' +require 'addressable/uri' + +gem 'data_objects', DM_VERSION +require 'data_objects' + +gem 'do_sqlite3', DM_VERSION +require 'do_sqlite3' + +gem 'rails_datamapper', DM_VERSION +require 'rails_datamapper' + +gem 'dm-validations', DM_VERSION +require 'dm-validations' + +ENV['SQLITE3_SPEC_URI'] ||= 'sqlite3::memory:' +ENV['ADAPTER'] = 'sqlite3' + +# Configure DataMapper Adapter +def setup_adapter(name, default_uri = nil) + begin + DataMapper.setup(name, ENV["#{ENV['ADAPTER'].to_s.upcase}_SPEC_URI"] || default_uri) + Object.const_set('ADAPTER', ENV['ADAPTER'].to_sym) if name.to_s == ENV['ADAPTER'] + true + rescue Exception => e + if name.to_s == ENV['ADAPTER'] + Object.const_set('ADAPTER', nil) + warn "Could not load do_#{name}: #{e}" + end + false + end +end + +setup_adapter(:default) + +# Load Remarkable core on place to avoid gem to be loaded +dir = File.dirname(__FILE__) +require File.join(dir, '..', '..', 'remarkable', 'lib', 'remarkable') + +# Load Remarkable DataMapper +require File.join(dir, 'model_builder') +require File.join(dir, '..', 'lib', 'remarkable_datamapper') + +# Include matchers +Remarkable.include_matchers!(Remarkable::DataMapper, Spec::Example::ExampleGroup) From e812d8a4ee00297def62fbe06ff18e9cf8040317 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Tue, 1 Sep 2009 16:15:22 -0700 Subject: [PATCH 03/14] directly copied describe_spec.rb --- remarkable_datamapper/spec/describe_spec.rb | 102 ++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 remarkable_datamapper/spec/describe_spec.rb diff --git a/remarkable_datamapper/spec/describe_spec.rb b/remarkable_datamapper/spec/describe_spec.rb new file mode 100644 index 0000000..96f0d22 --- /dev/null +++ b/remarkable_datamapper/spec/describe_spec.rb @@ -0,0 +1,102 @@ +require File.expand_path(File.dirname(__FILE__) + '/spec_helper') + +RAILS_I18n = true + +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.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 From 051c06b1192c1f68f3caaa9d74b841f9f06759dc Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Tue, 1 Sep 2009 19:13:57 -0700 Subject: [PATCH 04/14] Able to get specs running without i18n enabled. 8 failing specs in Describe. --- Rakefile | 1 + remarkable_activerecord/spec/describe_spec.rb | 2 +- .../lib/remarkable_datamapper/base.rb | 4 +- .../matchers/validates_is_unique.rb | 4 +- remarkable_datamapper/locale/en.yml | 14 + remarkable_datamapper/spec/describe_spec.rb | 2 +- remarkable_datamapper/spec/model_builder.rb | 5 +- remarkable_datamapper/spec/spec_helper.rb | 4 +- remarkable_i18n/en.yml | 262 ++++++++++++++++++ 9 files changed, 288 insertions(+), 10 deletions(-) diff --git a/Rakefile b/Rakefile index e249cd7..7b380b6 100644 --- a/Rakefile +++ b/Rakefile @@ -9,6 +9,7 @@ include FileUtils REMARKABLE_GEMS = [ :remarkable, :remarkable_activerecord, + :remarkable_datamapper, :remarkable_rails ] diff --git a/remarkable_activerecord/spec/describe_spec.rb b/remarkable_activerecord/spec/describe_spec.rb index 96f0d22..4fe921d 100644 --- a/remarkable_activerecord/spec/describe_spec.rb +++ b/remarkable_activerecord/spec/describe_spec.rb @@ -1,6 +1,6 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper') -RAILS_I18n = true +RAILS_I18N = true class Post attr_accessor :published, :public, :deleted diff --git a/remarkable_datamapper/lib/remarkable_datamapper/base.rb b/remarkable_datamapper/lib/remarkable_datamapper/base.rb index ca5d73d..6c4a3b8 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/base.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/base.rb @@ -67,8 +67,8 @@ def assert_good_or_bad_if_key(key, value, message_key=:message) #:nodoc: # It also gets an allow_nil message on remarkable.data_mapper.allow_nil # to be used as default. # - def allow_nil?(message_key=:message) #:nodoc: - assert_good_or_bad_if_key(:allow_nil, nil, message_key) + def nullable?(message_key=:message) #:nodoc: + assert_good_or_bad_if_key(:nullable, nil, message_key) end # Default allow_blank? validation. It accepts the message_key which is diff --git a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb index 2aba3ca..8dab30f 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb @@ -109,7 +109,7 @@ def allow_nil? begin @existing.update_attribute(@attribute, nil) - rescue ::ActiveRecord::StatementInvalid => e + rescue ::DataMapper::StatementInvalid => e raise ScriptError, "You declared that #{@attribute} accepts nil values in validate_uniqueness_of, " << "but I cannot save nil values in the database, got: #{e.message}" if @options[:allow_nil] return true @@ -126,7 +126,7 @@ def allow_blank? begin @existing.update_attribute(@attribute, '') - rescue ::ActiveRecord::StatementInvalid => e + rescue ::DataMapper::StatementInvalid => e raise ScriptError, "You declared that #{@attribute} accepts blank values in validate_uniqueness_of, " << "but I cannot save blank values in the database, got: #{e.message}" if @options[:allow_blank] return true diff --git a/remarkable_datamapper/locale/en.yml b/remarkable_datamapper/locale/en.yml index fd0e3b9..6ae06aa 100644 --- a/remarkable_datamapper/locale/en.yml +++ b/remarkable_datamapper/locale/en.yml @@ -262,3 +262,17 @@ en: positive: "case sensitive" negative: "case insensitive" + validates_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}}" + 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" + diff --git a/remarkable_datamapper/spec/describe_spec.rb b/remarkable_datamapper/spec/describe_spec.rb index 96f0d22..4e828f0 100644 --- a/remarkable_datamapper/spec/describe_spec.rb +++ b/remarkable_datamapper/spec/describe_spec.rb @@ -1,6 +1,6 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper') -RAILS_I18n = true +RAILS_I18N = false # specs won't run unless true class Post attr_accessor :published, :public, :deleted diff --git a/remarkable_datamapper/spec/model_builder.rb b/remarkable_datamapper/spec/model_builder.rb index 628f030..e1c32e1 100644 --- a/remarkable_datamapper/spec/model_builder.rb +++ b/remarkable_datamapper/spec/model_builder.rb @@ -44,7 +44,8 @@ def create_table(table_name, &block) def define_constant(class_name, base, &block) class_name = class_name.to_s.camelize - klass = Class.new(base) + klass = Class.new + klass.include base Object.const_set(class_name, klass) klass.class_eval(&block) if block_given? @@ -56,7 +57,7 @@ def define_constant(class_name, base, &block) end def define_model_class(class_name, &block) - define_constant(class_name, ActiveRecord::Base, &block) + define_constant(class_name, DataMapper::Resource, &block) end def define_model(name, columns = {}, &block) diff --git a/remarkable_datamapper/spec/spec_helper.rb b/remarkable_datamapper/spec/spec_helper.rb index ab0103c..ea7fde1 100644 --- a/remarkable_datamapper/spec/spec_helper.rb +++ b/remarkable_datamapper/spec/spec_helper.rb @@ -16,8 +16,8 @@ gem 'do_sqlite3', DM_VERSION require 'do_sqlite3' -gem 'rails_datamapper', DM_VERSION -require 'rails_datamapper' +gem 'dm-core', DM_VERSION +require 'dm-core' gem 'dm-validations', DM_VERSION require 'dm-validations' diff --git a/remarkable_i18n/en.yml b/remarkable_i18n/en.yml index b10a33b..40113b7 100644 --- a/remarkable_i18n/en.yml +++ b/remarkable_i18n/en.yml @@ -383,3 +383,265 @@ en: positive: "case sensitive" negative: "case insensitive" + data_mapper: + describe: + each: "{{key}} is {{value}}" + prepend: "when " + connector: " and " + expectations: + allow_nil: "{{subject_name}} to {{not}}allow nil values for {{attribute}}" + allow_blank: "{{subject_name}} to {{not}}allow blank values for {{attribute}}" + optionals: + allow_nil: + positive: "allowing nil values" + negative: "not allowing nil values" + allow_blank: + positive: "allowing blank values" + negative: "not allowing blank values" + + accept_nested_attributes_for: + description: "accept nested attributes for {{associations}}" + expectations: + association_exists: "{{subject_name}} to have association {{association}}, but does not" + is_autosave: "{{subject_name}} to have association {{association}} with autosave true, got false" + responds_to_attributes: "{{subject_name}} to respond to :{{association}}_attributes=, but does not" + allows_destroy: "{{subject_name}} with allow destroy equals to {{allow_destroy}}, got {{actual}}" + accepts: "{{subject_name}} to accept attributes {{attributes}} for {{association}}, but does not" + rejects: "{{subject_name}} to reject attributes {{attributes}} for {{association}}, but does not" + optionals: + allow_destroy: + positive: "allowing destroy" + negative: "not allowing destroy" + accept: + positive: "accepting {{sentence}}" + reject: + positive: "rejecting {{sentence}}" + + allow_values_for: + description: "allow {{in}} as values for {{attributes}}" + expectations: + is_valid: "{{subject_name}} to be valid when {{attribute}} is set to {{value}}" + + allow_mass_assignment_of: + description: "allow mass assignment of {{attributes}}" + expectations: + allows: "{{subject_name}} to allow mass assignment ({{subject_name}} is protecting {{protected_attributes}})" + is_protected: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} is protecting {{attribute}})" + is_accessible: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} has not made {{attribute}} accessible)" + negative_expectations: + allows: "{{subject_name}} to allow mass assignment ({{subject_name}} made {{accessible_attributes}} accessible)" + is_protected: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} is not protecting {{attribute}})" + is_accessible: "{{subject_name}} to allow mass assignment of {{attribute}} ({{subject_name}} has made {{attribute}} accessible)" + + association: + belongs_to: belong to + has_many: have many + has_and_belongs_to_many: have and belong to many + has_one: have one + description: "{{macro}} {{associations}}" + expectations: + association_exists: "{{subject_name}} records {{macro}} {{association}}, but the association does not exist" + macro_matches: "{{subject_name}} records {{macro}} {{association}}, got {{subject_name}} records {{actual_macro}} {{association}}" + through_exists: "{{subject_name}} records {{macro}} {{association}} through {{through}}, through association does not exist" + source_exists: "{{subject_name}} records {{macro}} {{association}} through {{through}}, source association does not exist" + klass_exists: "{{subject_name}} records {{macro}} {{association}}, but the association class does not exist" + join_table_exists: "join table {{join_table}} to exist, but does not" + foreign_key_exists: "foreign key {{foreign_key}} to exist on {{foreign_key_table}}, but does not" + polymorphic_exists: "{{subject_table}} to have {{polymorphic_column}} as column, but does not" + counter_cache_exists: "{{reflection_table}} to have {{counter_cache_column}} as column, but does not" + options_match: "{{subject_name}} records {{macro}} {{association}} with options {{options}}, got {{actual}}" + optionals: + through: + positive: "through {{value}}" + source: + positive: "with source {{inspect}}" + source_type: + positive: "with source type {{inspect}}" + class_name: + positive: "with class name {{inspect}}" + foreign_key: + positive: "with foreign key {{inspect}}" + dependent: + positive: "with dependent {{inspect}}" + join_table: + positive: "with join table {{inspect}}" + uniq: + positive: "with unique records" + negative: "without unique records" + readonly: + positive: "with readonly records" + negative: "without readonly records" + validate: + positive: "validating associated records" + negative: "not validating associated records" + autosave: + positive: "autosaving associated records" + negative: "not autosaving associated records" + as: + positive: "through the polymorphic interface {{inspect}}" + counter_cache: + positive: "with counter cache {{inspect}}" + negative: "without counter cache" + select: + positive: "selecting {{inspect}}" + conditions: + positive: "with conditions {{inspect}}" + include: + positive: "including {{inspect}}" + group: + positive: "grouping by {{inspect}}" + having: + positive: "having {{inspect}}" + order: + positive: "with order {{inspect}}" + limit: + positive: "with limit {{inspect}}" + offset: + positive: "with offset {{inspect}}" + + have_column: + description: "have column(s) named {{columns}}" + expectations: + column_exists: "{{subject_name}} to have column named {{column}}" + options_match: "{{subject_name}} to have column {{column}} with options {{options}}, got {{actual}}" + optionals: + type: + positive: "with type {{inspect}}" + null: + positive: "allowing null values" + negative: "not allowing null values" + default: + positive: "with default value {{inspect}}" + negative: "with default value {{inspect}}" + limit: + positive: "with limit {{inspect}}" + + have_default_scope: + description: "have a default scope with {{options}}" + expectations: + options_match: "default scope with {{options}}, got {{actual}}" + + have_index: + description: "have index for column(s) {{columns}}" + expectations: + index_exists: "index {{column}} to exist on table {{table_name}}" + is_unique: "index on {{column}} with unique equals to {{unique}}, got {{actual}}" + optionals: + unique: + positive: "with unique values" + negative: "with non unique values" + table_name: + positive: "on table {{value}}" + + have_readonly_attributes: + description: "make {{attributes}} read-only" + expectations: + is_readonly: "{{subject_name}} to make {{attribute}} read-only, got {{actual}}" + + have_scope: + description: "have to scope itself to {{options}} when {{scope_name}} is called" + expectations: + is_scope: "{{scope_name}} when called on {{subject_name}} return an instance of ActiveRecord::NamedScope::Scope" + options_match: "{{scope_name}} when called on {{subject_name}} scope to {{options}}, got {{actual}}" + optionals: + with: + positive: "with {{inspect}} as argument" + + validate_acceptance_of: + description: "require {{attributes}} to be accepted" + expectations: + requires_acceptance: "{{subject_name}} to be invalid if {{attribute}} is not accepted" + accept_is_valid: "{{subject_name}} to be valid when {{attribute}} is accepted with value {{accept}}" + optionals: + accept: + positive: "with value {{inspect}}" + + validate_associated: + description: "require associated {{associations}} to be valid" + expectations: + is_valid: "{{subject_name}} to be invalid when {{association}} is invalid" + + validate_confirmation_of: + description: "require {{attributes}} to be confirmed" + expectations: + responds_to_confirmation: "{{subject_name}} instance responds to {{attribute}}_confirmation" + confirms: "{{subject_name}} to be valid only when {{attribute}} is confirmed" + + validate_exclusion_of: + description: "ensure exclusion of {{attributes}} in {{in}}" + expectations: + is_valid: "{{subject_name}} to be valid when {{attribute}} is set to {{value}}" + is_invalid: "{{subject_name}} to be invalid when {{attribute}} is set to {{value}}" + + validate_inclusion_of: + description: "ensure inclusion of {{attributes}} in {{in}}" + expectations: + is_valid: "{{subject_name}} to be valid when {{attribute}} is set to {{value}}" + is_invalid: "{{subject_name}} to be invalid when {{attribute}} is set to {{value}}" + + validate_length_of: + description: "ensure length of {{attributes}}" + expectations: + less_than_min_length: "{{subject_name}} to be invalid when {{attribute}} length is less than {{minimum}} characters" + exactly_min_length: "{{subject_name}} to be valid when {{attribute}} length is {{minimum}} characters" + more_than_max_length: "{{subject_name}} to be invalid when {{attribute}} length is more than {{maximum}} characters" + exactly_max_length: "{{subject_name}} to be valid when {{attribute}} length is {{maximum}} characters" + optionals: + within: + positive: "is within {{inspect}} characters" + maximum: + positive: "is maximum {{inspect}} characters" + minimum: + positive: "is minimum {{inspect}} characters" + is: + positive: "is equal to {{inspect}} characters" + with_kind_of: + positive: "with kind of {{value}}" + + validate_numericality_of: + description: "ensure numericality of {{attributes}}" + expectations: + only_numeric_values: "{{subject_name}} to allow only numeric values for {{attribute}}" + only_integer: "{{subject_name}} to {{not}}allow only integer values for {{attribute}}" + only_even: "{{subject_name}} to allow only even values for {{attribute}}" + only_odd: "{{subject_name}} to allow only odd values for {{attribute}}" + equals_to: "{{subject_name}} to be valid only when {{attribute}} is equal to {{count}}" + more_than_maximum: "{{subject_name}} to be invalid when {{attribute}} is greater than {{count}}" + less_than_minimum: "{{subject_name}} to be invalid when {{attribute}} is less than {{count}}" + optionals: + only_integer: + positive: "allowing only integer values" + odd: + positive: "allowing only odd values" + even: + positive: "allowing only even values" + equal_to: + positive: "is equal to {{inspect}}" + less_than: + positive: "is less than {{inspect}}" + greater_than: + positive: "is greater than {{inspect}}" + less_than_or_equal_to: + positive: "is less than or equal to {{inspect}}" + greater_than_or_equal_to: + positive: "is greater than or equal to {{inspect}}" + + validate_presence_of: + description: "require {{attributes}} to be set" + 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" + From 2644d50c57fd3fc54d62e520ddc53b014df27e49 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Tue, 1 Sep 2009 21:58:04 -0700 Subject: [PATCH 05/14] Trying to fix up specs. --- remarkable_datamapper/lib/remarkable_datamapper/base.rb | 4 ++-- remarkable_datamapper/lib/remarkable_datamapper/describe.rb | 1 + remarkable_datamapper/spec/describe_spec.rb | 3 ++- remarkable_datamapper/spec/spec_helper.rb | 5 +++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/remarkable_datamapper/lib/remarkable_datamapper/base.rb b/remarkable_datamapper/lib/remarkable_datamapper/base.rb index 6c4a3b8..267166a 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/base.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/base.rb @@ -180,7 +180,7 @@ 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 RAILS_I18N # Rails >= 2.2 + 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' @@ -240,7 +240,7 @@ def collection_interpolation #:nodoc: # Returns true if the given collection should be translated. # def i18n_collection? #:nodoc: - RAILS_I18N && I18N_COLLECTION.include?(self.class.matcher_arguments[:collection]) + 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 2ffcc5e..7564617 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/describe.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/describe.rb @@ -130,6 +130,7 @@ 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/spec/describe_spec.rb b/remarkable_datamapper/spec/describe_spec.rb index 4e828f0..66cb2bd 100644 --- a/remarkable_datamapper/spec/describe_spec.rb +++ b/remarkable_datamapper/spec/describe_spec.rb @@ -1,6 +1,6 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper') -RAILS_I18N = false # specs won't run unless true +I18N = true # specs won't run unless false class Post attr_accessor :published, :public, :deleted @@ -72,6 +72,7 @@ def my_attributes 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 diff --git a/remarkable_datamapper/spec/spec_helper.rb b/remarkable_datamapper/spec/spec_helper.rb index ea7fde1..34cf6f0 100644 --- a/remarkable_datamapper/spec/spec_helper.rb +++ b/remarkable_datamapper/spec/spec_helper.rb @@ -22,6 +22,11 @@ gem 'dm-validations', DM_VERSION require 'dm-validations' +gem 'svenfuchs-i18n' +require 'i18n' + +require 'pp' # DEBUG ONLY + ENV['SQLITE3_SPEC_URI'] ||= 'sqlite3::memory:' ENV['ADAPTER'] = 'sqlite3' From f2b79b5a62bdd54a1e6d6bc3ff11686c0c7659a9 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Tue, 1 Sep 2009 22:22:50 -0700 Subject: [PATCH 06/14] Changed comments to DataMapper instead of ActiveRecord. --- remarkable_datamapper/lib/remarkable_datamapper.rb | 2 +- .../lib/remarkable_datamapper/matchers/validates_is_unique.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/remarkable_datamapper/lib/remarkable_datamapper.rb b/remarkable_datamapper/lib/remarkable_datamapper.rb index d8ad50e..e2c6658 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper.rb @@ -9,7 +9,7 @@ end end -# Load Remarkable ActiveRecord files +# Load Remarkable DataMapper files dir = File.dirname(__FILE__) require File.join(dir, 'remarkable_datamapper', 'base') require File.join(dir, 'remarkable_datamapper', 'describe') diff --git a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb index 8dab30f..5e25679 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validates_is_unique.rb @@ -208,7 +208,7 @@ def new_value_for_stringfiable_scope(scope) # * :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). - # Regexp, string or symbol. Default = I18n.translate('activerecord.errors.messages.taken') + # Regexp, string or symbol. Default = I18n.translate('datamapper.errors.messages.taken') # # == Examples # From 39c44f02d2b5028d6c65db3b5b2f18c163f1be8d Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Wed, 2 Sep 2009 16:38:34 -0700 Subject: [PATCH 07/14] 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 From 5479daf9acfea4b2e40bce5460cb06c584068f95 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Wed, 2 Sep 2009 16:39:41 -0700 Subject: [PATCH 08/14] Removed 'pp' debug lines. --- remarkable_datamapper/lib/remarkable_datamapper/base.rb | 2 -- .../matchers/validate_is_unique_matcher.rb | 4 ---- 2 files changed, 6 deletions(-) diff --git a/remarkable_datamapper/lib/remarkable_datamapper/base.rb b/remarkable_datamapper/lib/remarkable_datamapper/base.rb index 4597f1c..673ca50 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/base.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/base.rb @@ -181,8 +181,6 @@ 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 # TODO: No Internationalization yet. - # TODO: remove debug line - pp attribute message = ::DataMapper::Validate::ValidationErrors.default_error_message(message, attribute, '12345') if message =~ /12345/ diff --git a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb index a31fd99..5742eb0 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb @@ -39,12 +39,8 @@ def find_first_object? 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 - 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 From d16f4a1a98c8b81c050c3ebacb12af444ae919be Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Wed, 2 Sep 2009 16:54:37 -0700 Subject: [PATCH 09/14] Fixed many old :allow_nil references to :nullable --- .../matchers/validate_is_unique_matcher.rb | 33 +++++-------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb index 5742eb0..de9d7bd 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb @@ -19,7 +19,7 @@ class ValidateIsUniqueMatcher < Remarkable::DataMapper::Base #:nodoc: private - # Tries to find an object in the database. If allow_nil and/or allow_blank + # Tries to find an object in the database. If nullable and/or allow_blank # is given, we must find a record which is not nil or not blank. # # We should also ensure that the object retrieved from the database @@ -84,34 +84,17 @@ def valid_with_new_scope? # Change the existing object attribute to nil to run allow nil # validations. If we find any problem while updating the @existing # record, it's because we can't save nil values in the database. So it - # passes when :allow_nil is false, but should raise an error when - # :allow_nil is true + # passes when :nullable is false, but should raise an error when + # :nullable is true # - def allow_nil? - return true unless @options.key?(:allow_nil) + def nullable? + return true unless @options.key?(:nullable) begin @existing.update_attribute(@attribute, nil) - rescue ::DataMapper::StatementInvalid => e - raise ScriptError, "You declared that #{@attribute} accepts nil values in validate_uniqueness_of, " << - "but I cannot save nil values in the database, got: #{e.message}" if @options[:allow_nil] - return true - end - - super - end - - # Change the existing object attribute to blank to run allow blank - # validation. It uses the same logic as :allow_nil. - # - def allow_blank? - return true unless @options.key?(:allow_blank) - - begin - @existing.update_attribute(@attribute, '') - rescue ::DataMapper::StatementInvalid => e - raise ScriptError, "You declared that #{@attribute} accepts blank values in validate_uniqueness_of, " << - "but I cannot save blank values in the database, got: #{e.message}" if @options[:allow_blank] + rescue StandardError => e #::DataMapper::StatementInvalid => e + raise ScriptError, "You declared that #{@attribute} accepts nil values in validates_is_unique, " << + "but I cannot save nil values in the database, got: #{e.message}" if @options[:unique] return true end From 6f2a2dcc75143528f1bb7816bf1b26098d1c3ed4 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Wed, 2 Sep 2009 16:54:52 -0700 Subject: [PATCH 10/14] Removed another pp debug reference. --- remarkable_datamapper/spec/model_builder.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/remarkable_datamapper/spec/model_builder.rb b/remarkable_datamapper/spec/model_builder.rb index 36e2769..e07c7a4 100644 --- a/remarkable_datamapper/spec/model_builder.rb +++ b/remarkable_datamapper/spec/model_builder.rb @@ -65,7 +65,6 @@ def define_model(name, columns = {}, &block) class_name = name.to_s.pluralize.classify table_name = class_name.tableize 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 @@ -81,8 +80,8 @@ def define_model(name, columns = {}, &block) end module ClassMethods - # This is a macro to run validations of boolean optionals such as :allow_nil - # and :allow_blank. This macro tests all scenarios. The specs must have a + # This is a macro to run validations of boolean optionals such as :nullable + # and :scope. This macro tests all scenarios. The specs must have a # define_and_validate method defined. # def create_optional_boolean_specs(optional, base, options={}) From b3d2315b0a3aecce4e12d0a1c7a30abc1fed546a Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Wed, 2 Sep 2009 16:56:15 -0700 Subject: [PATCH 11/14] Fixed outdated Object.type reference to use Object.class instead. --- .../matchers/validate_is_unique_matcher.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb index de9d7bd..9e88af9 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb @@ -112,7 +112,7 @@ def new_value_for_scope(scope) nil end - case column_type.type + case column_type.class when :int, :integer, :float, :decimal new_value_for_stringfiable_scope(scope) when :datetime, :timestamp, :time From d94d9d1b17188a3b1818d275e330ce4b41f9f2c9 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Wed, 2 Sep 2009 16:58:21 -0700 Subject: [PATCH 12/14] Renamed spec file to be consistent with pattern. --- ..._unique_matcher_spec.rb => validate_is_unique_matcher_spec.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename remarkable_datamapper/spec/{validates_is_unique_matcher_spec.rb => validate_is_unique_matcher_spec.rb} (100%) diff --git a/remarkable_datamapper/spec/validates_is_unique_matcher_spec.rb b/remarkable_datamapper/spec/validate_is_unique_matcher_spec.rb similarity index 100% rename from remarkable_datamapper/spec/validates_is_unique_matcher_spec.rb rename to remarkable_datamapper/spec/validate_is_unique_matcher_spec.rb From 3237af6c3088a1e47c10ab709065e4d6bb0d9f9a Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Wed, 2 Sep 2009 20:46:30 -0700 Subject: [PATCH 13/14] Changed some comment references from :allow_nil to :nullable --- remarkable_datamapper/lib/remarkable_datamapper/base.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/remarkable_datamapper/lib/remarkable_datamapper/base.rb b/remarkable_datamapper/lib/remarkable_datamapper/base.rb index 673ca50..829272d 100644 --- a/remarkable_datamapper/lib/remarkable_datamapper/base.rb +++ b/remarkable_datamapper/lib/remarkable_datamapper/base.rb @@ -5,11 +5,11 @@ class Base < Remarkable::Base # Provides a way to send options to all DataMapper matchers. # - # validates_presence_of(:name).with_options(:allow_nil => false) + # validates_presence_of(:name).with_options(:nullable => false) # # Is equivalent to: # - # validates_presence_of(:name, :allow_nil => false) + # validates_presence_of(:name, :nullable => false) # def with_options(opts={}) @options.merge!(opts) @@ -61,10 +61,10 @@ def assert_good_or_bad_if_key(key, value, message_key=:message) #:nodoc: end end - # Default allow_nil? validation. It accepts the message_key which is + # Default nullable? validation. It accepts the message_key which is # the key which contain the message in @options. # - # It also gets an allow_nil message on remarkable.data_mapper.allow_nil + # It also gets an nullable message on remarkable.data_mapper.nullable # to be used as default. # def nullable?(message_key=:message) #:nodoc: From 484127a7eb029f64707db7cbc8cf14b8deb7a4cf Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Wed, 2 Sep 2009 20:51:04 -0700 Subject: [PATCH 14/14] Added i18n translation for validate_is_unque.optionals.nullable.positive. Also removed those for case_sensitive since that option doesn't exist for DM validates_is_unique. --- remarkable_datamapper/locale/en.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/remarkable_datamapper/locale/en.yml b/remarkable_datamapper/locale/en.yml index cc0d76f..6f30394 100644 --- a/remarkable_datamapper/locale/en.yml +++ b/remarkable_datamapper/locale/en.yml @@ -259,7 +259,5 @@ en: optionals: scope: positive: "scoped to {{sentence}}" - case_sensitive: - positive: "case sensitive" - negative: "case insensitive" - + nullable: + positive: "allowing nil values"