diff --git a/lib/remarkable_mongomapper.rb b/lib/remarkable_mongomapper.rb index 4c3090e..98be619 100644 --- a/lib/remarkable_mongomapper.rb +++ b/lib/remarkable_mongomapper.rb @@ -14,7 +14,9 @@ Remarkable.add_locale File.join(dir, '..', 'locales', 'en.yml') require File.join(dir, 'remarkable_mongomapper', 'base') - +require File.join(dir, 'remarkable_mongomapper', 'describe') +# require File.join(dir, 'remarkable_mongomapper', 'human_names') + # Add matchers Dir[File.join(dir, 'remarkable_mongomapper', 'matchers', '*.rb')].each do |file| require file diff --git a/lib/remarkable_mongomapper/describe.rb b/lib/remarkable_mongomapper/describe.rb new file mode 100644 index 0000000..18d2d21 --- /dev/null +++ b/lib/remarkable_mongomapper/describe.rb @@ -0,0 +1,199 @@ +module Remarkable + module ActiveRecord + + 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: + # mongo_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: + # mongo_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.mongo_mapper.describe.connector", :default => " and " + + description = if self.describe_subject_attributes.blank? + Remarkable.t("remarkable.mongo_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.mongo_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 \ No newline at end of file diff --git a/lib/remarkable_mongomapper/human_names.rb b/lib/remarkable_mongomapper/human_names.rb new file mode 100644 index 0000000..3d1c8a4 --- /dev/null +++ b/lib/remarkable_mongomapper/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 diff --git a/spec/matchers/allow_values_for_matcher_spec.rb b/spec/matchers/allow_values_for_matcher_spec.rb index 25bed83..24b9e3c 100644 --- a/spec/matchers/allow_values_for_matcher_spec.rb +++ b/spec/matchers/allow_values_for_matcher_spec.rb @@ -1,12 +1,24 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe 'allow_values_for' do - subject do - Site.new + include ModelBuilder + + # Defines a model, create a validation and returns a raw matcher + def define_and_validate(options={}) + @model = define_model :product do + include MongoMapper::Document + + key :title, String + key :category, String + + validates_format_of :title, options + end + + allow_values_for(:title) end describe 'messages' do - before(:each){ @model = subject; @matcher = allow_values_for(:title) } + before(:each){ @matcher = define_and_validate(:with => /X|Y|Z/) } it 'should contain a description' do @matcher = allow_values_for(:title, "X", "Y", "Z") @@ -15,40 +27,44 @@ it 'should set is_valid? message' do @matcher.in("A").matches?(subject) - @matcher.failure_message.should == 'Expected Site to be valid when title is set to "A"' + @matcher.failure_message.should == 'Expected Product to be valid when title is set to "A"' end it 'should set allow_nil? message' do @matcher.allow_nil.matches?(subject) - @matcher.failure_message.should == 'Expected Site to allow nil values for title' + @matcher.failure_message.should == 'Expected Product to allow nil values for title' end it 'should set allow_blank? message' do @matcher.allow_blank.matches?(subject) - @matcher.failure_message.should == 'Expected Site to allow blank values for title' + @matcher.failure_message.should == 'Expected Product to allow blank values for title' end end describe 'matchers' do - it { should allow_values_for(:title).in('X', 'Y', 'Z') } - it { should_not allow_values_for(:title).in('A') } + it { should define_and_validate(:with => /X|Y|Z/).in('X', 'Y', 'Z') } + it { should_not define_and_validate(:with => /X|Y|Z/).in('A') } - # it { should define_and_validate(:with => /X|Y|Z/, :message => 'valid').in('X', 'Y', 'Z').message('valid') } + it { should define_and_validate(:with => /X|Y|Z/, :message => 'valid').in('X', 'Y', 'Z').message('valid') } - # create_optional_boolean_specs(:allow_nil, self, :with => /X|Y|Z/) - # create_optional_boolean_specs(:allow_blank, self, :with => /X|Y|Z/) + create_optional_boolean_specs(:allow_nil, self, :with => /X|Y|Z/) + create_optional_boolean_specs(:allow_blank, self, :with => /X|Y|Z/) end describe 'macros' do + before(:each){ define_and_validate(:with => /X|Y|Z/) } + should_allow_values_for :title, 'X' should_not_allow_values_for :title, 'A' end describe 'failures' do it "should fail if any of the values are valid on invalid cases" do + define_and_validate(:with => /X|Y|Z/) + lambda { - should_not allow_values_for :title, 'A', 'X', 'B' - }.should raise_error(Spec::Expectations::ExpectationNotMetError, /Did not expect Site to be valid/) + should_not allow_values_for(:title, 'A', 'X', 'B') + }.should raise_error(Spec::Expectations::ExpectationNotMetError, /Did not expect Product to be valid/) end end end diff --git a/spec/model_builder.rb b/spec/model_builder.rb new file mode 100644 index 0000000..e201fe6 --- /dev/null +++ b/spec/model_builder.rb @@ -0,0 +1,64 @@ +# This is based on Shoulda model builder for Test::Unit. +# +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 + end + end + + base.extend ClassMethods + end + + def define_constant(class_name, &block) + class_name = class_name.to_s.camelize + + klass = Class.new + Object.const_set(class_name, klass) + + klass.class_eval(&block) if block_given? + + @defined_constants ||= [] + @defined_constants << class_name + + klass + end + + def define_model(name, columns = {}, &block) + instance = define_constant(name.to_s.classify, &block).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/spec/models.rb b/spec/models.rb index 9ddfb86..dc9930f 100644 --- a/spec/models.rb +++ b/spec/models.rb @@ -35,8 +35,6 @@ class Rating class Site include MongoMapper::Document - - key :title, String, :format => /X|Y|Z/ end class Webiste diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2aa50a0..afe9c0d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,4 +15,5 @@ def reset_test_db! end require File.join(File.dirname(__FILE__), "..", "lib", "remarkable_mongomapper") -require File.join(File.dirname(__FILE__), "models") \ No newline at end of file +require File.join(File.dirname(__FILE__), "models") +require File.join(File.dirname(__FILE__), "model_builder") \ No newline at end of file