Skip to content
Browse files

Merge commit 'bgentry/master' into dm

  • Loading branch information...
2 parents 03990a8 + 484127a commit 90d0f3eb374ed6fc6cdf8b521e2f2a9006c7b4c7 @josevalim josevalim committed
View
1 Rakefile
@@ -9,6 +9,7 @@ include FileUtils
REMARKABLE_GEMS = [
:remarkable,
:remarkable_activerecord,
+ :remarkable_datamapper,
:remarkable_rails
]
View
2 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
View
30 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
View
30 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 DataMapper 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)
View
245 remarkable_datamapper/lib/remarkable_datamapper/base.rb
@@ -0,0 +1,245 @@
+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(:nullable => false)
+ #
+ # Is equivalent to:
+ #
+ # validates_presence_of(:name, :nullable => 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 nullable? validation. It accepts the message_key which is
+ # the key which contain the message in @options.
+ #
+ # It also gets an nullable message on remarkable.data_mapper.nullable
+ # to be used as default.
+ #
+ 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
+ # the key which contain the message in @options.
+ #
+ # 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:
+ 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 a DataMapper model validates with the passed
+ # <tt>value</tt> by making sure the <tt>error_message_to_avoid</tt> 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 a DataMapper model invalidates the passed
+ # <tt>value</tt> by making sure the <tt>error_message_to_expect</tt> 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 those 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
+ # TODO: No Internationalization yet.
+ message = ::DataMapper::Validate::ValidationErrors.default_error_message(message, attribute, '12345')
+
+ 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
View
199 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
View
37 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
View
197 remarkable_datamapper/lib/remarkable_datamapper/matchers/validate_is_unique_matcher.rb
@@ -0,0 +1,197 @@
+module Remarkable
+ module DataMapper
+ module Matchers
+ class ValidateIsUniqueMatcher < Remarkable::DataMapper::Base #:nodoc:
+ arguments :collection => :attributes, :as => :attribute
+
+ optional :message
+ optional :scope, :splat => true
+ optional :nullable, :default => true
+
+ collection_assertions :find_first_object?, :responds_to_scope?, :is_unique?,
+ :valid_with_new_scope?, :nullable?
+
+ 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 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
+ # is not the @subject.
+ #
+ # If any of these attempts fail, an error is raised.
+ #
+ def find_first_object?
+ conditions, message = [[], ""]
+ if @options[:nullable]
+ conditions << {::DataMapper::Query::Operator.new(@attribute, :not) => nil}
+ message << " with #{@attribute} not nil"
+ end
+
+ 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)}
+ end
+
+ return true if @existing = subject_class.first(conditions)
+ 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
+
+ # 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 :nullable is false, but should raise an error when
+ # :nullable is true
+ #
+ def nullable?
+ return true unless @options.key?(:nullable)
+
+ begin
+ @existing.update_attribute(@attribute, nil)
+ 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
+
+ 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.class
+ 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.all(:conditions => conditions, :fields => [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
+ #
+ # * <tt>:scope</tt> - field(s) to scope the uniqueness to.
+ # * <tt>:allow_nil</tt> - when supplied, validates if it allows nil or not.
+ # * <tt>:allow_blank</tt> - when supplied, validates if it allows blank or not.
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
+ # Regexp, string or symbol. Default = <tt>I18n.translate('datamapper.errors.messages.taken')</tt>
+ #
+ # == Examples
+ #
+ # it { should validate_uniqueness_of(:keyword, :username) }
+ # 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
+ # should_validate_uniqueness_of :address, :scope => [:first_name, :last_name]
+ #
+ # should_validate_uniqueness_of :email do |m|
+ # m.scope = name
+ # end
+ #
+ def validate_is_unique(*attributes, &block)
+ ValidateIsUniqueMatcher.new(*attributes, &block).spec(self)
+ end
+ end
+ end
+end
View
263 remarkable_datamapper/locale/en.yml
@@ -0,0 +1,263 @@
+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_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:
+ scope:
+ positive: "scoped to {{sentence}}"
+ nullable:
+ positive: "allowing nil values"
View
105 remarkable_datamapper/spec/model_builder.rb
@@ -0,0 +1,105 @@
+# This is based on Shoulda model builder for Test::Unit.
+#
+
+# TODO: !!! These functions are not all updated yet
+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
+
+ 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(model)
+ adapter = DataMapper::Repository.adapters[:default]
+ table_name = model.to_s.tableize
+ command = "DROP TABLE IF EXISTS #{table_name}"
+
+ begin
+ adapter.execute(command)
+ adapter.create_model_storage(model)
+ @created_tables ||= []
+ @created_tables << table_name
+ adapter
+ rescue Exception => e
+ adapter.execute(command)
+ raise e
+ end
+ end
+
+ def define_constant(class_name, base, &block)
+ class_name = class_name.to_s.camelize
+
+ klass = Class.new
+ klass.send :include, base
+ Object.const_set(class_name, klass) #unless 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, DataMapper::Resource, &block)
+ end
+
+ 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)
+ 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
+
+ module ClassMethods
+ # 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={})
+ 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
+
View
2 remarkable_datamapper/spec/rcov.opts
@@ -0,0 +1,2 @@
+--exclude "spec/*,gems/*"
+--rails
View
4 remarkable_datamapper/spec/spec.opts
@@ -0,0 +1,4 @@
+--colour
+--format progress
+--loadby mtime
+--reverse
View
59 remarkable_datamapper/spec/spec_helper.rb
@@ -0,0 +1,59 @@
+# 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 'dm-core', DM_VERSION
+require 'dm-core'
+
+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'
+
+# 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)
View
155 remarkable_datamapper/spec/validate_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
View
262 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"
+

0 comments on commit 90d0f3e

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