Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit for remarkable_datamapper.

  • Loading branch information...
commit c59c15123d30d4775ddcacfa0e67aa4991089ac5 1 parent 3398a9b
Blake Gentry authored
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 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)
View
248 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
+ # <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 an Active Record 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 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
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
Please sign in to comment.
Something went wrong with that request. Please try again.