├── VERSION ├── .gitignore ├── spec ├── spec.opts ├── spec_helper.rb ├── models.rb ├── matchers │ ├── have_key_matcher_spec.rb │ ├── validate_presence_of_matcher_spec.rb │ ├── validate_confirmation_of_matcher_spec.rb │ ├── allow_values_for_matcher_spec.rb │ ├── association_matcher_spec.rb │ └── validate_length_of_matcher_spec.rb └── model_builder.rb ├── README.md ├── Rakefile ├── lib └── remarkable │ ├── mongo_mapper.rb │ └── mongo_mapper │ ├── matchers │ ├── have_key_matcher.rb │ ├── validate_presence_of_matcher.rb │ ├── validate_confirmation_of_matcher.rb │ ├── allow_values_for_matcher.rb │ ├── association_matcher.rb │ └── validate_length_of_matcher.rb │ ├── human_names.rb │ ├── describe.rb │ └── base.rb ├── LICENSE ├── remarkable_mongo.gemspec └── locales └── en.yml /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.2 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | pkg/* -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --colour 2 | --format progress 3 | --loadby mtime 4 | --reverse -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "mongo_mapper" 3 | 4 | MongoMapper.database = "remarkable_mongomapper" 5 | 6 | def reset_test_db! 7 | MongoMapper.connection.drop_database("remarkable_mongomapper") 8 | end 9 | 10 | Spec::Runner.configure do |config| 11 | config.before(:all) { reset_test_db! } 12 | config.after(:all) { reset_test_db! } 13 | end 14 | 15 | require File.join(File.dirname(__FILE__), "..", "lib", "remarkable/mongo_mapper") 16 | require File.join(File.dirname(__FILE__), "models") 17 | require File.join(File.dirname(__FILE__), "model_builder") -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remarkable MongoMapper 2 | 3 | Remarkable matchers for [MongoMapper](http://github.com/jnunemaker/mongomapper). 4 | 5 | ## Matchers 6 | 7 |
it { should have_key(:name, String) }
 8 | it { should have_keys(:name, :phone_number, String) }
 9 | it { should validate_presence_of(:name, :phone_number, :message => "not there!") }
10 | it { should belong_to(:user, :class_name => 'Person') }
11 | it { should have_many(:users, :class_name => 'Person', :polymorphic => true) }
12 | 13 | ## TODO 14 | 15 | * Finish validate_length_of 16 | 17 | ## Contributions 18 | 19 | It is far from complete! It'd be very helpful to have some help. 20 | 21 | ## Contributors 22 | 23 | * Nicolas Mérouze -------------------------------------------------------------------------------- /spec/models.rb: -------------------------------------------------------------------------------- 1 | class Article 2 | include MongoMapper::Document 3 | 4 | key :title, String, :required => true 5 | key :body, String 6 | 7 | many :comments 8 | many :unknowns 9 | many :ratings, :class_name => "Rate" 10 | many :assets 11 | belongs_to :user 12 | belongs_to :unknown 13 | belongs_to :site, :class_name => 'Site' 14 | end 15 | 16 | class Comment 17 | include MongoMapper::EmbeddedDocument 18 | 19 | key :body, String 20 | end 21 | 22 | class User 23 | include MongoMapper::Document 24 | 25 | key :login, String 26 | end 27 | 28 | class Rate 29 | include MongoMapper::EmbeddedDocument 30 | end 31 | 32 | class Rating 33 | include MongoMapper::EmbeddedDocument 34 | end 35 | 36 | class Site 37 | include MongoMapper::Document 38 | end 39 | 40 | class Webiste 41 | include MongoMapper::EmbeddedDocument 42 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'spec/rake/spectask' 4 | 5 | begin 6 | require 'jeweler' 7 | Jeweler::Tasks.new do |gem| 8 | gem.name = "remarkable_mongo" 9 | gem.summary = %Q{Remarkable Matchers for MongoDB ORMs} 10 | gem.email = "nicolas.merouze@gmail.com" 11 | gem.homepage = "http://github.com/nmerouze/remarkable_mongo" 12 | gem.authors = ["Nicolas Merouze"] 13 | 14 | gem.add_dependency('remarkable', '~> 3.1.8') 15 | gem.add_dependency('mongo_mapper', '~> 0.6.1') 16 | end 17 | rescue LoadError 18 | puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" 19 | end 20 | 21 | desc 'Default: run specs.' 22 | task :default => :spec 23 | 24 | desc 'Run all the specs for the machinist plugin.' 25 | Spec::Rake::SpecTask.new do |t| 26 | t.spec_files = FileList['spec/**/*_spec.rb'] 27 | t.rcov = false 28 | end -------------------------------------------------------------------------------- /spec/matchers/have_key_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe 'have_key' do 4 | subject do 5 | Article.new 6 | end 7 | 8 | describe 'messages' do 9 | 10 | it 'should contain a description' do 11 | matcher = have_key(:title, String) 12 | matcher.description.should == 'have key(s) title with type String' 13 | end 14 | 15 | it 'should set has_key? message' do 16 | matcher = have_key(:owner, String) 17 | matcher.matches?(subject) 18 | matcher.failure_message.should == 'Expected Article to have key named owner with type String' 19 | end 20 | 21 | end 22 | 23 | describe 'matchers' do 24 | it { should have_key(:title, String) } 25 | it { should have_keys(:title, :body, String) } 26 | end 27 | 28 | describe 'macros' do 29 | should_have_key :title, String 30 | should_have_keys :title, :body, String 31 | end 32 | end -------------------------------------------------------------------------------- /lib/remarkable/mongo_mapper.rb: -------------------------------------------------------------------------------- 1 | # Load Remarkable 2 | unless Object.const_defined?('Remarkable') 3 | begin 4 | require 'remarkable' 5 | rescue LoadError 6 | require 'rubygems' 7 | gem 'remarkable' 8 | require 'remarkable' 9 | end 10 | end 11 | 12 | # Add locale 13 | dir = File.dirname(__FILE__) 14 | Remarkable.add_locale File.join(dir, '..', '..', 'locales', 'en.yml') 15 | 16 | require File.join(dir, 'mongo_mapper', 'base') 17 | require File.join(dir, 'mongo_mapper', 'describe') 18 | # require File.join(dir, 'remarkable_mongomapper', 'human_names') 19 | 20 | # Add matchers 21 | Dir[File.join(dir, 'mongo_mapper', 'matchers', '*.rb')].each do |file| 22 | require file 23 | end 24 | 25 | # Include Remarkable MongoMapper matcher in appropriate ExampleGroup 26 | if defined?(Spec::Rails) 27 | Remarkable.include_matchers!(Remarkable::MongoMapper, Spec::Rails::Example::ModelExampleGroup) 28 | else 29 | Remarkable.include_matchers!(Remarkable::MongoMapper, Spec::Example::ExampleGroup) 30 | end -------------------------------------------------------------------------------- /spec/matchers/validate_presence_of_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe 'validate_presence_of' do 4 | subject do 5 | Article.new 6 | end 7 | 8 | describe 'messages' do 9 | 10 | it 'should contain a description' do 11 | matcher = validate_presence_of(:title, :body) 12 | matcher.description.should == 'require title and body to be set' 13 | end 14 | 15 | it 'should set allow_nil? message' do 16 | matcher = validate_presence_of(:body) 17 | matcher.matches?(subject) 18 | matcher.failure_message.should == 'Expected Article to require body to be set' 19 | matcher.negative_failure_message.should == 'Did not expect Article to require body to be set' 20 | end 21 | 22 | end 23 | 24 | describe 'matchers' do 25 | it { should validate_presence_of(:title) } 26 | it { should_not validate_presence_of(:body) } 27 | end 28 | 29 | describe 'macros' do 30 | should_validate_presence_of(:title) 31 | should_not_validate_presence_of(:body) 32 | end 33 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Nicolas Mérouze 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/remarkable/mongo_mapper/matchers/have_key_matcher.rb: -------------------------------------------------------------------------------- 1 | module Remarkable 2 | module MongoMapper 3 | module Matchers 4 | class HaveKeyMatcher < Remarkable::MongoMapper::Base 5 | 6 | arguments :type, :collection => :attributes, :as => :attribute 7 | 8 | collection_assertions :has_key? 9 | 10 | # before_assert do 11 | # @type = @options[:type] 12 | # end 13 | 14 | protected 15 | 16 | def has_key? 17 | @subject.respond_to?(@attribute) && @subject.class.keys[@attribute] == ::MongoMapper::Plugins::Keys::Key.new(@attribute, @type) 18 | end 19 | 20 | end 21 | 22 | # Ensures that a key of the database actually exists. 23 | # 24 | # == Examples 25 | # 26 | # should_have_key :name, String 27 | # 28 | # it { should have_key(:name, String) } 29 | # it { should have_keys(:name, :phone_number, String) } 30 | # 31 | def have_key(*args, &block) 32 | HaveKeyMatcher.new(args.pop, *args, &block).spec(self) 33 | end 34 | alias :have_keys :have_key 35 | 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /lib/remarkable/mongo_mapper/human_names.rb: -------------------------------------------------------------------------------- 1 | if defined?(Spec) 2 | module Spec #:nodoc: 3 | module Example #:nodoc: 4 | module ExampleGroupMethods #:nodoc: 5 | 6 | # This allows "describe User" to use the I18n human name of User. 7 | # 8 | def self.build_description_with_i18n(*args) 9 | args.inject("") do |description, arg| 10 | arg = if arg.respond_to?(:human_name) 11 | arg.human_name(:locale => Remarkable.locale) 12 | else 13 | arg.to_s 14 | end 15 | 16 | description << " " unless (description == "" || arg =~ /^(\s|\.|#)/) 17 | description << arg 18 | end 19 | end 20 | 21 | # This is for rspec <= 1.1.12. 22 | # 23 | def self.description_text(*args) 24 | self.build_description_with_i18n(*args) 25 | end 26 | 27 | # This is for rspec >= 1.2.0. 28 | # 29 | def self.build_description_from(*args) 30 | text = ExampleGroupMethods.build_description_with_i18n(*args) 31 | text == "" ? nil : text 32 | end 33 | 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/remarkable/mongo_mapper/matchers/validate_presence_of_matcher.rb: -------------------------------------------------------------------------------- 1 | module Remarkable 2 | module MongoMapper 3 | module Matchers 4 | class ValidatePresenceOfMatcher < Remarkable::MongoMapper::Base 5 | arguments :collection => :attributes, :as => :attribute 6 | optional :message 7 | 8 | collection_assertions :allow_nil? 9 | default_options :message => "can't be empty" 10 | 11 | protected 12 | 13 | def allow_nil? 14 | bad?(nil, :message) 15 | end 16 | 17 | end 18 | 19 | # Ensures that the model cannot be saved if one of the attributes listed is not present. 20 | # 21 | # == Options 22 | # 23 | # * :message - value the test expects to find in errors.on(:attribute). 24 | # Regexp, string or symbol. Default = "can't be empty" 25 | # 26 | # == Examples 27 | # 28 | # should_validate_presence_of :name, :phone_number 29 | # it { should validate_presence_of(:name, :phone_number) } 30 | # 31 | def validate_presence_of(*args, &block) 32 | ValidatePresenceOfMatcher.new(*args, &block).spec(self) 33 | end 34 | 35 | end 36 | end 37 | end -------------------------------------------------------------------------------- /lib/remarkable/mongo_mapper/matchers/validate_confirmation_of_matcher.rb: -------------------------------------------------------------------------------- 1 | module Remarkable 2 | module MongoMapper 3 | module Matchers 4 | class ValidateConfirmationOfMatcher < Remarkable::MongoMapper::Base #:nodoc: 5 | arguments :collection => :attributes, :as => :attribute 6 | 7 | optional :message 8 | collection_assertions :responds_to_confirmation?, :confirms? 9 | 10 | default_options :message => "doesn't match confirmation" 11 | 12 | protected 13 | 14 | def responds_to_confirmation? 15 | @subject.respond_to?(:"#{@attribute}_confirmation=") 16 | end 17 | 18 | def confirms? 19 | @subject.send(:"#{@attribute}_confirmation=", 'something') 20 | bad?('different') 21 | end 22 | 23 | end 24 | 25 | # Ensures that the model cannot be saved if one of the attributes is not confirmed. 26 | # 27 | # == Options 28 | # 29 | # * :message - value the test expects to find in errors.on(:attribute). 30 | # Regexp, string or symbol. Default = "doesn't match confirmation" 31 | # 32 | # == Examples 33 | # 34 | # should_validate_confirmation_of :email, :password 35 | # 36 | # it { should validate_confirmation_of(:email, :password) } 37 | # 38 | def validate_confirmation_of(*attributes, &block) 39 | ValidateConfirmationOfMatcher.new(*attributes, &block).spec(self) 40 | end 41 | 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/matchers/validate_confirmation_of_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe 'validate_confirmation_of' do 4 | include ModelBuilder 5 | 6 | # Defines a model, create a validation and returns a raw matcher 7 | def define_and_validate(options={}) 8 | @model = define_model :person do 9 | include MongoMapper::Document 10 | 11 | key :name, String 12 | key :email, String 13 | key :age, String 14 | 15 | validates_confirmation_of :name, :email, options 16 | end 17 | 18 | validate_confirmation_of(:name, :email) 19 | end 20 | 21 | describe 'messages' do 22 | before(:each){ @matcher = define_and_validate } 23 | 24 | it 'should contain a description' do 25 | @matcher.description.should == 'require name and email to be confirmed' 26 | end 27 | 28 | it 'should set responds_to_confirmation? message' do 29 | @matcher = validate_confirmation_of(:age) 30 | @matcher.matches?(@model) 31 | @matcher.failure_message.should == 'Expected Person instance responds to age_confirmation' 32 | end 33 | 34 | it 'should set confirms? message' do 35 | @model.instance_eval{ def age_confirmation=(*args); end } 36 | @matcher = validate_confirmation_of(:age) 37 | @matcher.matches?(@model) 38 | @matcher.failure_message.should == 'Expected Person to be valid only when age is confirmed' 39 | end 40 | 41 | end 42 | 43 | describe 'matchers' do 44 | 45 | describe 'without options' do 46 | before(:each){ define_and_validate } 47 | 48 | it { should validate_confirmation_of(:name) } 49 | it { should validate_confirmation_of(:name, :email) } 50 | it { should_not validate_confirmation_of(:name, :age) } 51 | end 52 | 53 | create_message_specs(self) 54 | end 55 | 56 | describe 'macros' do 57 | before(:each){ define_and_validate } 58 | 59 | should_validate_confirmation_of :name 60 | should_validate_confirmation_of :name, :email 61 | should_not_validate_confirmation_of :name, :age 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /spec/model_builder.rb: -------------------------------------------------------------------------------- 1 | # This is based on Shoulda model builder for Test::Unit. 2 | # 3 | module ModelBuilder 4 | def self.included(base) 5 | return unless base.name =~ /^Spec/ 6 | 7 | base.class_eval do 8 | after(:each) do 9 | if @defined_constants 10 | @defined_constants.each do |class_name| 11 | Object.send(:remove_const, class_name) 12 | end 13 | end 14 | end 15 | end 16 | 17 | base.extend ClassMethods 18 | end 19 | 20 | def define_constant(class_name, &block) 21 | class_name = class_name.to_s.camelize 22 | 23 | klass = Class.new 24 | Object.const_set(class_name, klass) 25 | 26 | klass.class_eval(&block) if block_given? 27 | 28 | @defined_constants ||= [] 29 | @defined_constants << class_name 30 | 31 | klass 32 | end 33 | 34 | def define_model(name, columns = {}, &block) 35 | instance = define_constant(name.to_s.classify, &block).new 36 | 37 | self.class.subject { instance } if self.class.respond_to?(:subject) 38 | instance 39 | end 40 | 41 | module ClassMethods 42 | # This is a macro to run validations of boolean optionals such as :allow_nil 43 | # and :allow_blank. This macro tests all scenarios. The specs must have a 44 | # define_and_validate method defined. 45 | # 46 | def create_optional_boolean_specs(optional, base, options={}) 47 | base.describe "with #{optional} option" do 48 | it { should define_and_validate(options.merge(optional => true)).send(optional) } 49 | it { should define_and_validate(options.merge(optional => false)).send(optional, false) } 50 | it { should_not define_and_validate(options.merge(optional => true)).send(optional, false) } 51 | it { should_not define_and_validate(options.merge(optional => false)).send(optional) } 52 | end 53 | end 54 | 55 | def create_message_specs(base) 56 | base.describe "with message option" do 57 | it { should define_and_validate(:message => 'valid_message').message('valid_message') } 58 | it { should_not define_and_validate(:message => 'not_valid').message('valid_message') } 59 | end 60 | end 61 | end 62 | 63 | end 64 | 65 | -------------------------------------------------------------------------------- /spec/matchers/allow_values_for_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe 'allow_values_for' do 4 | include ModelBuilder 5 | 6 | # Defines a model, create a validation and returns a raw matcher 7 | def define_and_validate(options={}) 8 | @model = define_model :product do 9 | include MongoMapper::Document 10 | 11 | key :title, String 12 | key :category, String 13 | 14 | validates_format_of :title, options 15 | end 16 | 17 | allow_values_for(:title) 18 | end 19 | 20 | describe 'messages' do 21 | before(:each){ @matcher = define_and_validate(:with => /X|Y|Z/) } 22 | 23 | it 'should contain a description' do 24 | @matcher = allow_values_for(:title, "X", "Y", "Z") 25 | @matcher.description.should == 'allow "X", "Y", and "Z" as values for title' 26 | end 27 | 28 | it 'should set is_valid? message' do 29 | @matcher.in("A").matches?(subject) 30 | @matcher.failure_message.should == 'Expected Product to be valid when title is set to "A"' 31 | end 32 | 33 | it 'should set allow_nil? message' do 34 | @matcher.allow_nil.matches?(subject) 35 | @matcher.failure_message.should == 'Expected Product to allow nil values for title' 36 | end 37 | 38 | it 'should set allow_blank? message' do 39 | @matcher.allow_blank.matches?(subject) 40 | @matcher.failure_message.should == 'Expected Product to allow blank values for title' 41 | end 42 | end 43 | 44 | describe 'matchers' do 45 | it { should define_and_validate(:with => /X|Y|Z/).in('X', 'Y', 'Z') } 46 | it { should_not define_and_validate(:with => /X|Y|Z/).in('A') } 47 | 48 | it { should define_and_validate(:with => /X|Y|Z/, :message => 'valid').in('X', 'Y', 'Z').message('valid') } 49 | 50 | create_optional_boolean_specs(:allow_nil, self, :with => /X|Y|Z/) 51 | create_optional_boolean_specs(:allow_blank, self, :with => /X|Y|Z/) 52 | end 53 | 54 | describe 'macros' do 55 | before(:each){ define_and_validate(:with => /X|Y|Z/) } 56 | 57 | should_allow_values_for :title, 'X' 58 | should_not_allow_values_for :title, 'A' 59 | end 60 | 61 | describe 'failures' do 62 | it "should fail if any of the values are valid on invalid cases" do 63 | define_and_validate(:with => /X|Y|Z/) 64 | 65 | lambda { 66 | should_not allow_values_for(:title, 'A', 'X', 'B') 67 | }.should raise_error(Spec::Expectations::ExpectationNotMetError, /Did not expect Product to be valid/) 68 | end 69 | end 70 | end 71 | 72 | -------------------------------------------------------------------------------- /lib/remarkable/mongo_mapper/matchers/allow_values_for_matcher.rb: -------------------------------------------------------------------------------- 1 | module Remarkable 2 | module MongoMapper 3 | module Matchers 4 | class AllowValuesForMatcher < Remarkable::MongoMapper::Base #:nodoc: 5 | include Remarkable::Negative 6 | arguments :collection => :attributes, :as => :attribute 7 | 8 | optional :message 9 | optional :in, :splat => true 10 | optional :allow_nil, :allow_blank, :default => true 11 | 12 | collection_assertions :is_valid?, :is_invalid?, :allow_nil?, :allow_blank? 13 | 14 | default_options :message => "is invalid" 15 | 16 | before_assert do 17 | first_value = @options[:in].is_a?(Array) ? @options[:in].first : @options[:in] 18 | @in_range = first_value.is_a?(Range) 19 | 20 | @options[:in] = if @in_range 21 | first_value.to_a[0,2] + first_value.to_a[-2,2] 22 | else 23 | [*@options[:in]].compact 24 | end 25 | 26 | @options[:in].uniq! 27 | end 28 | 29 | protected 30 | 31 | def is_valid? 32 | assert_collection :value, valid_values do |value| 33 | good?(value) 34 | end 35 | end 36 | 37 | def is_invalid? 38 | assert_collection :value, invalid_values do |value| 39 | bad?(value) 40 | end 41 | end 42 | 43 | def valid_values 44 | @options[:in] 45 | end 46 | 47 | def invalid_values 48 | [] 49 | end 50 | 51 | def interpolation_options 52 | options = if @in_range 53 | { :in => (@options[:in].first..@options[:in].last).inspect } 54 | elsif @options[:in].is_a?(Array) 55 | { :in => array_to_sentence(@options[:in], true, '[]') } 56 | else 57 | { :in => @options[:in].inspect } 58 | end 59 | 60 | options.merge!(:behavior => @behavior.to_s) 61 | end 62 | 63 | end 64 | 65 | # Ensures that the attribute can be set to the given values. 66 | # 67 | # == Options 68 | # 69 | # * :allow_nil - when supplied, validates if it allows nil or not. 70 | # * :allow_blank - when supplied, validates if it allows blank or not. 71 | # * :message - value the test expects to find in errors.on(:attribute). 72 | # Regexp, string or symbol. Default = I18n.translate('activerecord.errors.messages.invalid') 73 | # 74 | # == Examples 75 | # 76 | # should_allow_values_for :isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0" 77 | # it { should allow_values_for(:isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0") } 78 | # 79 | def allow_values_for(attribute, *args, &block) 80 | options = args.extract_options! 81 | AllowValuesForMatcher.new(attribute, options.merge!(:in => args), &block).spec(self) 82 | end 83 | 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /remarkable_mongo.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{remarkable_mongo} 8 | s.version = "0.1.2" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Nicolas M\303\251rouze"] 12 | s.date = %q{2010-01-11} 13 | s.email = %q{nicolas.merouze@gmail.com} 14 | s.extra_rdoc_files = [ 15 | "LICENSE", 16 | "README.md" 17 | ] 18 | s.files = [ 19 | ".gitignore", 20 | "LICENSE", 21 | "README.md", 22 | "Rakefile", 23 | "VERSION", 24 | "lib/remarkable/mongo_mapper.rb", 25 | "lib/remarkable/mongo_mapper/base.rb", 26 | "lib/remarkable/mongo_mapper/describe.rb", 27 | "lib/remarkable/mongo_mapper/human_names.rb", 28 | "lib/remarkable/mongo_mapper/matchers/allow_values_for_matcher.rb", 29 | "lib/remarkable/mongo_mapper/matchers/association_matcher.rb", 30 | "lib/remarkable/mongo_mapper/matchers/have_key_matcher.rb", 31 | "lib/remarkable/mongo_mapper/matchers/validate_confirmation_of_matcher.rb", 32 | "lib/remarkable/mongo_mapper/matchers/validate_length_of_matcher.rb", 33 | "lib/remarkable/mongo_mapper/matchers/validate_presence_of_matcher.rb", 34 | "locales/en.yml", 35 | "spec/matchers/allow_values_for_matcher_spec.rb", 36 | "spec/matchers/association_matcher_spec.rb", 37 | "spec/matchers/have_key_matcher_spec.rb", 38 | "spec/matchers/validate_confirmation_of_matcher_spec.rb", 39 | "spec/matchers/validate_length_of_matcher_spec.rb", 40 | "spec/matchers/validate_presence_of_matcher_spec.rb", 41 | "spec/model_builder.rb", 42 | "spec/models.rb", 43 | "spec/spec.opts", 44 | "spec/spec_helper.rb" 45 | ] 46 | s.homepage = %q{http://github.com/nmerouze/remarkable_mongo} 47 | s.rdoc_options = ["--charset=UTF-8"] 48 | s.require_paths = ["lib"] 49 | s.rubygems_version = %q{1.3.5} 50 | s.summary = %q{Remarkable Matchers for MongoDB ORMs} 51 | s.test_files = [ 52 | "spec/matchers/allow_values_for_matcher_spec.rb", 53 | "spec/matchers/association_matcher_spec.rb", 54 | "spec/matchers/have_key_matcher_spec.rb", 55 | "spec/matchers/validate_confirmation_of_matcher_spec.rb", 56 | "spec/matchers/validate_length_of_matcher_spec.rb", 57 | "spec/matchers/validate_presence_of_matcher_spec.rb", 58 | "spec/model_builder.rb", 59 | "spec/models.rb", 60 | "spec/spec_helper.rb" 61 | ] 62 | 63 | if s.respond_to? :specification_version then 64 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 65 | s.specification_version = 3 66 | 67 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 68 | s.add_runtime_dependency(%q, ["~> 3.1.8"]) 69 | s.add_runtime_dependency(%q, ["~> 0.6.1"]) 70 | else 71 | s.add_dependency(%q, ["~> 3.1.8"]) 72 | s.add_dependency(%q, ["~> 0.6.1"]) 73 | end 74 | else 75 | s.add_dependency(%q, ["~> 3.1.8"]) 76 | s.add_dependency(%q, ["~> 0.6.1"]) 77 | end 78 | end 79 | 80 | -------------------------------------------------------------------------------- /lib/remarkable/mongo_mapper/matchers/association_matcher.rb: -------------------------------------------------------------------------------- 1 | module Remarkable 2 | module MongoMapper 3 | module Matchers 4 | class AssociationMatcher < Remarkable::MongoMapper::Base 5 | arguments :type, :collection => :associations, :as => :association 6 | 7 | optionals :class_name 8 | 9 | collection_assertions :association_exists?, :type_matches?, :klass_exists?, :options_match? 10 | 11 | protected 12 | 13 | def association_exists? 14 | reflection 15 | end 16 | 17 | def type_matches? 18 | reflection.type == @type 19 | end 20 | 21 | def klass_exists? 22 | return true if @options[:polymorphic] 23 | reflection.klass rescue nil 24 | end 25 | 26 | def options_match? 27 | actual_options = {} 28 | 29 | @options.keys.each do |key| 30 | method = :"reflection_#{key}" 31 | 32 | @options[key] = @options[key].to_s 33 | actual_options[key] = (respond_to?(method, true) ? send(method) : reflection.options[key]).to_s 34 | end 35 | 36 | return @options == actual_options, :actual => actual_options.inspect 37 | end 38 | 39 | private 40 | 41 | def reflection 42 | @reflection ||= subject_class.associations[@association] 43 | end 44 | 45 | def interpolation_options 46 | options = {} 47 | options[:type] = Remarkable.t(@type, :scope => matcher_i18n_scope, :default => @type.to_s.gsub("_", "")) 48 | options[:options] = @options.inspect 49 | 50 | if @subject && reflection 51 | options.merge!( 52 | :actual_type => Remarkable.t(reflection.type, :scope => matcher_i18n_scope, :default => reflection.type.to_s) 53 | ) 54 | end 55 | 56 | options 57 | end 58 | 59 | end 60 | 61 | # Ensures that the many relationship exists. Will also test that the 62 | # associated table has the required columns. 63 | # 64 | # == Options 65 | # 66 | # * :class_name - the expected associted class name. 67 | # * :polymorphic - if the association should be polymorphic or not. 68 | # When true it also checks for the association_type column in the subject table. 69 | # 70 | # == Examples 71 | # 72 | # should_have_many :addresses 73 | # should_have_many :users, :class_name => 'Person' 74 | # 75 | # it { should have_many(:addresses) } 76 | # it { should have_many(:users, :class_name => 'Person') } 77 | # 78 | def have_many(*associations, &block) 79 | AssociationMatcher.new(:many, *associations, &block).spec(self) 80 | end 81 | 82 | # Ensures that the many relationship exists. Will also test that the 83 | # associated table has the required columns. 84 | # 85 | # == Options 86 | # 87 | # * :class_name - the expected associted class name. 88 | # * :polymorphic - if the association should be polymorphic or not. 89 | # When true it also checks for the association_type column in the subject table. 90 | # 91 | # == Examples 92 | # 93 | # should_belong_to :user 94 | # should_belong_to :user, :class_name => 'Person' 95 | # 96 | # it { should belong_to(:user) } 97 | # it { should belong_to(:user, :class_name => 'Person') } 98 | # 99 | def belong_to(*associations, &block) 100 | AssociationMatcher.new(:belongs_to, *associations, &block).spec(self) 101 | end 102 | 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/matchers/association_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe 'association_matcher' do 4 | subject do 5 | Article.new 6 | end 7 | 8 | describe 'have_many' do 9 | 10 | describe 'messages' do 11 | it 'should contain a description' do 12 | matcher = have_many(:comments) 13 | matcher.description.should == 'have many comments' 14 | end 15 | 16 | it 'should set association_exists? message' do 17 | matcher = have_many(:whatever) 18 | matcher.matches?(subject) 19 | matcher.failure_message.should == 'Expected Article records have many whatever, but the association does not exist' 20 | end 21 | 22 | it 'should set type_matches? message' do 23 | matcher = belong_to(:comments) 24 | matcher.matches?(subject) 25 | matcher.failure_message.should == 'Expected Article records belong to comments, got Article records have many comments' 26 | end 27 | 28 | it 'should set klass_exists? message' do 29 | matcher = have_many(:unknowns) 30 | matcher.matches?(subject) 31 | matcher.failure_message.should == 'Expected Article records have many unknowns, but the association class does not exist' 32 | end 33 | 34 | it 'should set options_matches? message when :class_name is given' do 35 | matcher = have_many(:ratings, :class_name => 'Rating') 36 | matcher.matches?(subject) 37 | matcher.failure_message.should == 'Expected Article records have many ratings with options {:class_name=>"Rating"}, got {:class_name=>"Rate"}' 38 | end 39 | 40 | it 'should set options_matches? message when :polymorphic is given' do 41 | matcher = have_many(:assets, :polymorphic => true) 42 | matcher.matches?(subject) 43 | matcher.failure_message.should == 'Expected Article records have many assets with options {:polymorphic=>"true"}, got {:polymorphic=>""}' 44 | end 45 | end 46 | 47 | describe 'matchers' do 48 | it { should have_many(:comments) } 49 | it { should_not belong_to(:comment) } 50 | end 51 | 52 | describe 'macros' do 53 | should_have_many :comments 54 | should_not_belong_to :comment 55 | end 56 | 57 | end 58 | 59 | describe 'belong_to' do 60 | 61 | describe 'messages' do 62 | it 'should contain a description' do 63 | matcher = belong_to(:user) 64 | matcher.description.should == 'belong to user' 65 | end 66 | 67 | it 'should set association_exists? message' do 68 | matcher = belong_to(:whatever) 69 | matcher.matches?(subject) 70 | matcher.failure_message.should == 'Expected Article records belong to whatever, but the association does not exist' 71 | end 72 | 73 | it 'should set type_matches? message' do 74 | matcher = have_many(:user) 75 | matcher.matches?(subject) 76 | matcher.failure_message.should == 'Expected Article records have many user, got Article records belong to user' 77 | end 78 | 79 | it 'should set klass_exists? message' do 80 | matcher = belong_to(:unknown) 81 | matcher.matches?(subject) 82 | matcher.failure_message.should == 'Expected Article records belong to unknown, but the association class does not exist' 83 | end 84 | 85 | it 'should set options_matches? message when :class_name is given' do 86 | matcher = belong_to(:site, :class_name => 'Website') 87 | matcher.matches?(subject) 88 | matcher.failure_message.should == 'Expected Article records belong to site with options {:class_name=>"Website"}, got {:class_name=>"Site"}' 89 | end 90 | end 91 | 92 | describe 'matchers' do 93 | it { should belong_to(:user) } 94 | it { should_not have_many(:users) } 95 | end 96 | 97 | describe 'macros' do 98 | should_belong_to :user 99 | should_not_have_many :users 100 | end 101 | 102 | end 103 | 104 | end 105 | -------------------------------------------------------------------------------- /lib/remarkable/mongo_mapper/matchers/validate_length_of_matcher.rb: -------------------------------------------------------------------------------- 1 | module Remarkable 2 | module MongoMapper 3 | module Matchers 4 | class ValidateLengthOfMatcher < Remarkable::MongoMapper::Base #:nodoc: 5 | arguments :collection => :attributes, :as => :attribute 6 | 7 | optional :within, :minimum, :maximum, :is 8 | optional :allow_nil, :allow_blank, :default => true 9 | optional :message 10 | 11 | default_options :message => "is invalid" 12 | 13 | collection_assertions :less_than_min_length?, :exactly_min_length?, 14 | :more_than_max_length?, :exactly_max_length?, 15 | :allow_nil?, :allow_blank? 16 | 17 | before_assert do 18 | if @options[:is] 19 | @min_value, @max_value = @options[:is], @options[:is] 20 | elsif @options[:within] 21 | @min_value, @max_value = @options[:within].first, @options[:within].last 22 | elsif @options[:maximum] 23 | @min_value, @max_value = nil, @options[:maximum] 24 | elsif @options[:minimum] 25 | @min_value, @max_value = @options[:minimum], nil 26 | end 27 | end 28 | 29 | protected 30 | def allow_nil? 31 | super(default_message_for(:too_short)) 32 | end 33 | 34 | def allow_blank? 35 | super(default_message_for(:too_short)) 36 | end 37 | 38 | def less_than_min_length? 39 | @min_value.nil? || @min_value <= 1 || bad?(@min_value - 1, default_message_for(:too_short)) 40 | end 41 | 42 | def exactly_min_length? 43 | @min_value.nil? || @min_value <= 0 || good?(@min_value, default_message_for(:too_short)) 44 | end 45 | 46 | def more_than_max_length? 47 | @max_value.nil? || bad?(@max_value + 1, default_message_for(:too_long)) 48 | end 49 | 50 | def exactly_max_length? 51 | @max_value.nil? || @min_value == @max_value || good?(@max_value, default_message_for(:too_long)) 52 | end 53 | 54 | def interpolation_options 55 | { :minimum => @min_value, :maximum => @max_value } 56 | end 57 | 58 | # Returns the default message for the validation type. 59 | # If user supplied :message, it will return it. Otherwise it will return 60 | # wrong_length on :is validation and :too_short or :too_long in the other 61 | # types. 62 | # 63 | def default_message_for(validation_type) 64 | return :message if @options[:message] 65 | end 66 | end 67 | 68 | # Validates the length of the given attributes. You have also to supply 69 | # one of the following options: minimum, maximum, is or within. 70 | # 71 | # Note: this method is also aliased as validate_size_of. 72 | # 73 | # == Options 74 | # 75 | # * :minimum - The minimum size of the attribute. 76 | # * :maximum - The maximum size of the attribute. 77 | # * :is - The exact size of the attribute. 78 | # * :within - A range specifying the minimum and maximum size of the attribute. 79 | # * :allow_nil - when supplied, validates if it allows nil or not. 80 | # * :allow_blank - when supplied, validates if it allows blank or not. 81 | # * :message - value the test expects to find in errors.on(:attribute). 82 | # Regexp, string or symbol. Default = "is invalid" 83 | # 84 | # == Examples 85 | # 86 | # it { should validate_length_of(:password).within(6..20) } 87 | # it { should validate_length_of(:password).maximum(20) } 88 | # it { should validate_length_of(:password).minimum(6) } 89 | # it { should validate_length_of(:age).is(18) } 90 | # 91 | # should_validate_length_of :password, :within => 6..20 92 | # should_validate_length_of :password, :maximum => 20 93 | # should_validate_length_of :password, :minimum => 6 94 | # should_validate_length_of :age, :is => 18 95 | # 96 | # should_validate_length_of :password do |m| 97 | # m.minimum 6 98 | # m.maximum 20 99 | # end 100 | # 101 | def validate_length_of(*attributes, &block) 102 | ValidateLengthOfMatcher.new(*attributes, &block).spec(self) 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/matchers/validate_length_of_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe 'validate_length_of' do 4 | include ModelBuilder 5 | 6 | # Defines a model, create a validation and returns a raw matcher 7 | def define_and_validate(options={}) 8 | options = options.merge(:within => 3..5) if options.slice(:within, :maximum, :minimum, :is).empty? 9 | 10 | @model = define_model :product do 11 | include MongoMapper::Document 12 | 13 | key :size, String 14 | key :category, String 15 | 16 | validates_length_of :size, options 17 | end 18 | 19 | validate_length_of(:size) 20 | end 21 | 22 | describe 'messages' do 23 | before(:each){ @matcher = define_and_validate } 24 | 25 | it 'should contain a description' do 26 | @matcher.within(3..5) 27 | @matcher.description.should == 'ensure length of size is within 3..5 characters' 28 | 29 | @matcher.within(nil).is(3) 30 | @matcher.description.should == 'ensure length of size is equal to 3 characters' 31 | 32 | @matcher.is(nil).maximum(5) 33 | @matcher.description.should == 'ensure length of size is maximum 5 characters' 34 | 35 | @matcher.maximum(nil).minimum(3) 36 | @matcher.description.should == 'ensure length of size is minimum 3 characters' 37 | 38 | @matcher.allow_nil(false) 39 | @matcher.description.should == 'ensure length of size is minimum 3 characters and not allowing nil values' 40 | 41 | @matcher.allow_blank 42 | @matcher.description.should == 'ensure length of size is minimum 3 characters, not allowing nil values, and allowing blank values' 43 | end 44 | 45 | it 'should set less_than_min_length? message' do 46 | @matcher.within(4..5).matches?(@model) 47 | @matcher.failure_message.should == 'Expected Product to be invalid when size length is less than 4 characters' 48 | end 49 | 50 | it 'should set exactly_min_length? message' do 51 | @matcher.should_receive(:less_than_min_length?).and_return(true) 52 | @matcher.within(2..5).matches?(@model) 53 | @matcher.failure_message.should == 'Expected Product to be valid when size length is 2 characters' 54 | end 55 | 56 | it 'should set more_than_max_length? message' do 57 | @matcher.within(3..4).matches?(@model) 58 | @matcher.failure_message.should == 'Expected Product to be invalid when size length is more than 4 characters' 59 | end 60 | 61 | it 'should set exactly_max_length? message' do 62 | @matcher.should_receive(:more_than_max_length?).and_return(true) 63 | @matcher.within(3..6).matches?(@model) 64 | @matcher.failure_message.should == 'Expected Product to be valid when size length is 6 characters' 65 | end 66 | 67 | it 'should set allow_blank? message' do 68 | @matcher.within(3..5).allow_blank.matches?(@model) 69 | @matcher.failure_message.should == 'Expected Product to allow blank values for size' 70 | end 71 | 72 | it 'should set allow_nil? message' do 73 | @matcher.within(3..5).allow_nil.matches?(@model) 74 | @matcher.failure_message.should == 'Expected Product to allow nil values for size' 75 | end 76 | end 77 | 78 | describe 'matcher' do 79 | # Wrap specs without options. Usually a couple specs. 80 | describe 'without options' do 81 | before(:each){ define_and_validate } 82 | 83 | it { should validate_length_of(:size, :within => 3..5) } 84 | it { should_not validate_length_of(:category, :within => 3..5) } 85 | end 86 | 87 | describe "with message option" do 88 | 89 | # if RAILS_VERSION =~ /^2.3/ 90 | # it { should define_and_validate(:message => 'not valid').within(3..5).message('not valid') } 91 | # it { should_not define_and_validate(:message => 'not valid').within(3..5).message('valid') } 92 | # else 93 | # it { should define_and_validate(:too_short => 'not valid', :too_long => 'not valid').within(3..5).message('not valid') } 94 | # it { should_not define_and_validate(:too_short => 'not valid', :too_long => 'not valid').within(3..5).message('valid') } 95 | # end 96 | 97 | it { should define_and_validate(:is => 4, :message => 'not valid').is(4).message('not valid') } 98 | it { should_not define_and_validate(:is => 4, :message => 'not valid').is(4).message('valid') } 99 | end 100 | 101 | describe "with within option" do 102 | it { should define_and_validate(:within => 3..5).within(3..5) } 103 | it { should_not define_and_validate(:within => 3..5).within(2..5) } 104 | it { should_not define_and_validate(:within => 3..5).within(4..5) } 105 | it { should_not define_and_validate(:within => 3..5).within(3..4) } 106 | it { should_not define_and_validate(:within => 3..5).within(3..6) } 107 | end 108 | 109 | describe "with minimum option" do 110 | it { should define_and_validate(:minimum => 3).minimum(3) } 111 | it { should_not define_and_validate(:minimum => 3).minimum(2) } 112 | it { should_not define_and_validate(:minimum => 3).minimum(4) } 113 | end 114 | 115 | describe "with maximum option" do 116 | it { should define_and_validate(:maximum => 3).maximum(3) } 117 | it { should_not define_and_validate(:maximum => 3).maximum(2) } 118 | it { should_not define_and_validate(:maximum => 3).maximum(4) } 119 | end 120 | 121 | describe "with is option" do 122 | it { should define_and_validate(:is => 3).is(3) } 123 | it { should_not define_and_validate(:is => 3).is(2) } 124 | it { should_not define_and_validate(:is => 3).is(4) } 125 | end 126 | 127 | # Those are macros to test optionals which accept only boolean values 128 | create_optional_boolean_specs(:allow_nil, self) 129 | create_optional_boolean_specs(:allow_blank, self) 130 | end 131 | 132 | # In macros we include just a few tests to assure that everything works properly 133 | describe 'macros' do 134 | before(:each) { define_and_validate } 135 | 136 | should_validate_length_of :size, :within => 3..5 137 | 138 | should_not_validate_length_of :size, :within => 2..5 139 | should_not_validate_length_of :size, :within => 4..5 140 | should_not_validate_length_of :size, :within => 3..4 141 | should_not_validate_length_of :size, :within => 3..6 142 | 143 | should_validate_length_of :size do |m| 144 | m.within = 3..5 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | remarkable: 3 | mongo_mapper: 4 | describe: 5 | each: "{{key}} is {{value}}" 6 | prepend: "when " 7 | connector: " and " 8 | expectations: 9 | allow_nil: "{{subject_name}} to {{not}}allow nil values for {{attribute}}" 10 | allow_blank: "{{subject_name}} to {{not}}allow blank values for {{attribute}}" 11 | optionals: 12 | allow_nil: 13 | positive: "allowing nil values" 14 | negative: "not allowing nil values" 15 | allow_blank: 16 | positive: "allowing blank values" 17 | negative: "not allowing blank values" 18 | 19 | allow_values_for: 20 | description: "allow {{in}} as values for {{attributes}}" 21 | expectations: 22 | is_valid: "{{subject_name}} to be valid when {{attribute}} is set to {{value}}" 23 | 24 | association: 25 | many: have many 26 | belongs_to: belong to 27 | description: "{{type}} {{associations}}" 28 | expectations: 29 | association_exists: "{{subject_name}} records {{type}} {{association}}, but the association does not exist" 30 | type_matches: "{{subject_name}} records {{type}} {{association}}, got {{subject_name}} records {{actual_type}} {{association}}" 31 | klass_exists: "{{subject_name}} records {{type}} {{association}}, but the association class does not exist" 32 | options_match: "{{subject_name}} records {{type}} {{association}} with options {{options}}, got {{actual}}" 33 | 34 | have_key: 35 | description: "have key(s) {{attributes}} with type {{type}}" 36 | expectations: 37 | has_key: "{{subject_name}} to have key named {{attribute}} with type {{type}}" 38 | 39 | validate_acceptance_of: 40 | description: "require {{attributes}} to be accepted" 41 | expectations: 42 | requires_acceptance: "{{subject_name}} to be invalid if {{attribute}} is not accepted" 43 | accept_is_valid: "{{subject_name}} to be valid when {{attribute}} is accepted with value {{accept}}" 44 | optionals: 45 | accept: 46 | positive: "with value {{inspect}}" 47 | 48 | validate_associated: 49 | description: "require associated {{associations}} to be valid" 50 | expectations: 51 | is_valid: "{{subject_name}} to be invalid when {{association}} is invalid" 52 | 53 | validate_confirmation_of: 54 | description: "require {{attributes}} to be confirmed" 55 | expectations: 56 | responds_to_confirmation: "{{subject_name}} instance responds to {{attribute}}_confirmation" 57 | confirms: "{{subject_name}} to be valid only when {{attribute}} is confirmed" 58 | 59 | validate_exclusion_of: 60 | description: "ensure exclusion of {{attributes}} in {{in}}" 61 | expectations: 62 | is_valid: "{{subject_name}} to be valid when {{attribute}} is set to {{value}}" 63 | is_invalid: "{{subject_name}} to be invalid when {{attribute}} is set to {{value}}" 64 | 65 | validate_inclusion_of: 66 | description: "ensure inclusion of {{attributes}} in {{in}}" 67 | expectations: 68 | is_valid: "{{subject_name}} to be valid when {{attribute}} is set to {{value}}" 69 | is_invalid: "{{subject_name}} to be invalid when {{attribute}} is set to {{value}}" 70 | 71 | validate_length_of: 72 | description: "ensure length of {{attributes}}" 73 | expectations: 74 | less_than_min_length: "{{subject_name}} to be invalid when {{attribute}} length is less than {{minimum}} characters" 75 | exactly_min_length: "{{subject_name}} to be valid when {{attribute}} length is {{minimum}} characters" 76 | more_than_max_length: "{{subject_name}} to be invalid when {{attribute}} length is more than {{maximum}} characters" 77 | exactly_max_length: "{{subject_name}} to be valid when {{attribute}} length is {{maximum}} characters" 78 | optionals: 79 | within: 80 | positive: "is within {{inspect}} characters" 81 | maximum: 82 | positive: "is maximum {{inspect}} characters" 83 | minimum: 84 | positive: "is minimum {{inspect}} characters" 85 | is: 86 | positive: "is equal to {{inspect}} characters" 87 | with_kind_of: 88 | positive: "with kind of {{value}}" 89 | 90 | validate_numericality_of: 91 | description: "ensure numericality of {{attributes}}" 92 | expectations: 93 | only_numeric_values: "{{subject_name}} to allow only numeric values for {{attribute}}" 94 | only_integer: "{{subject_name}} to {{not}}allow only integer values for {{attribute}}" 95 | only_even: "{{subject_name}} to allow only even values for {{attribute}}" 96 | only_odd: "{{subject_name}} to allow only odd values for {{attribute}}" 97 | equals_to: "{{subject_name}} to be valid only when {{attribute}} is equal to {{count}}" 98 | more_than_maximum: "{{subject_name}} to be invalid when {{attribute}} is greater than {{count}}" 99 | less_than_minimum: "{{subject_name}} to be invalid when {{attribute}} is less than {{count}}" 100 | optionals: 101 | only_integer: 102 | positive: "allowing only integer values" 103 | odd: 104 | positive: "allowing only odd values" 105 | even: 106 | positive: "allowing only even values" 107 | equal_to: 108 | positive: "is equal to {{inspect}}" 109 | less_than: 110 | positive: "is less than {{inspect}}" 111 | greater_than: 112 | positive: "is greater than {{inspect}}" 113 | less_than_or_equal_to: 114 | positive: "is less than or equal to {{inspect}}" 115 | greater_than_or_equal_to: 116 | positive: "is greater than or equal to {{inspect}}" 117 | 118 | validate_presence_of: 119 | description: "require {{attributes}} to be set" 120 | expectations: 121 | allow_nil: "{{subject_name}} to require {{attribute}} to be set" 122 | 123 | validate_uniqueness_of: 124 | description: "require unique values for {{attributes}}" 125 | expectations: 126 | responds_to_scope: "{{subject_name}} instance responds to {{method}}" 127 | is_unique: "{{subject_name}} to require unique values for {{attribute}}" 128 | case_sensitive: "{{subject_name}} to {{not}}be case sensitive on {{attribute}} validation" 129 | valid_with_new_scope: "{{subject_name}} to be valid when {{attribute}} scope ({{method}}) change" 130 | optionals: 131 | scope: 132 | positive: "scoped to {{sentence}}" 133 | case_sensitive: 134 | positive: "case sensitive" 135 | negative: "case insensitive" -------------------------------------------------------------------------------- /lib/remarkable/mongo_mapper/describe.rb: -------------------------------------------------------------------------------- 1 | module Remarkable 2 | module ActiveRecord 3 | 4 | def self.after_include(target) #:nodoc: 5 | target.class_inheritable_reader :describe_subject_attributes, :default_subject_attributes 6 | target.send :include, Describe 7 | end 8 | 9 | # Overwrites describe to provide quick way to configure your subject: 10 | # 11 | # describe Post 12 | # should_validate_presente_of :title 13 | # 14 | # describe :published => true do 15 | # should_validate_presence_of :published_at 16 | # end 17 | # end 18 | # 19 | # This is the same as: 20 | # 21 | # describe Post 22 | # should_validate_presente_of :title 23 | # 24 | # describe "when published is true" do 25 | # subject { Post.new(:published => true) } 26 | # should_validate_presence_of :published_at 27 | # end 28 | # end 29 | # 30 | # The string can be localized using I18n. An example yml file is: 31 | # 32 | # locale: 33 | # remarkable: 34 | # mongo_mapper: 35 | # describe: 36 | # each: "{{key}} is {{value}}" 37 | # prepend: "when " 38 | # connector: " and " 39 | # 40 | # You can also call subject attributes to set the default attributes for a 41 | # subject. You can even mix with a fixture replacement tool: 42 | # 43 | # describe Post 44 | # # Fixjour example 45 | # subject_attributes { valid_post_attributes } 46 | # 47 | # describe :published => true do 48 | # should_validate_presence_of :published_at 49 | # end 50 | # end 51 | # 52 | # You can retrieve the merged result of all attributes given using the 53 | # subject_attributes instance method: 54 | # 55 | # describe Post 56 | # # Fixjour example 57 | # subject_attributes { valid_post_attributes } 58 | # 59 | # describe :published => true do 60 | # it "should have default subject attributes" do 61 | # subject_attributes.should == { :title => 'My title', :published => true } 62 | # end 63 | # end 64 | # end 65 | # 66 | module Describe 67 | 68 | def self.included(base) #:nodoc: 69 | base.extend ClassMethods 70 | end 71 | 72 | module ClassMethods 73 | 74 | # Overwrites describe to provide quick way to configure your subject: 75 | # 76 | # describe Post 77 | # should_validate_presente_of :title 78 | # 79 | # describe :published => true do 80 | # should_validate_presence_of :published_at 81 | # end 82 | # end 83 | # 84 | # This is the same as: 85 | # 86 | # describe Post 87 | # should_validate_presente_of :title 88 | # 89 | # describe "when published is true" do 90 | # subject { Post.new(:published => true) } 91 | # should_validate_presence_of :published_at 92 | # end 93 | # end 94 | # 95 | # The string can be localized using I18n. An example yml file is: 96 | # 97 | # locale: 98 | # remarkable: 99 | # mongo_mapper: 100 | # describe: 101 | # each: "{{key}} is {{value}}" 102 | # prepend: "when " 103 | # connector: " and " 104 | # 105 | # See also subject_attributes instance and class methods for more 106 | # information. 107 | # 108 | def describe(*args, &block) 109 | if described_class && args.first.is_a?(Hash) 110 | attributes = args.shift 111 | 112 | connector = Remarkable.t "remarkable.mongo_mapper.describe.connector", :default => " and " 113 | 114 | description = if self.describe_subject_attributes.blank? 115 | Remarkable.t("remarkable.mongo_mapper.describe.prepend", :default => "when ") 116 | else 117 | connector.lstrip 118 | end 119 | 120 | pieces = [] 121 | attributes.each do |key, value| 122 | translated_key = if described_class.respond_to?(:human_attribute_name) 123 | described_class.human_attribute_name(key.to_s, :locale => Remarkable.locale) 124 | else 125 | key.to_s.humanize 126 | end 127 | 128 | pieces << Remarkable.t("remarkable.mongo_mapper.describe.each", 129 | :default => "{{key}} is {{value}}", 130 | :key => translated_key.downcase, :value => value.inspect) 131 | end 132 | 133 | description << pieces.join(connector) 134 | args.unshift(description) 135 | 136 | # Creates an example group, set the subject and eval the given block. 137 | # 138 | example_group = super(*args) do 139 | write_inheritable_hash(:describe_subject_attributes, attributes) 140 | set_described_subject! 141 | instance_eval(&block) 142 | end 143 | else 144 | super(*args, &block) 145 | end 146 | end 147 | 148 | # Sets default attributes for the subject. You can use this to set up 149 | # your subject with valid attributes. You can even mix with a fixture 150 | # replacement tool and still use quick subjects: 151 | # 152 | # describe Post 153 | # # Fixjour example 154 | # subject_attributes { valid_post_attributes } 155 | # 156 | # describe :published => true do 157 | # should_validate_presence_of :published_at 158 | # end 159 | # end 160 | # 161 | def subject_attributes(options=nil, &block) 162 | write_inheritable_attribute(:default_subject_attributes, options || block) 163 | set_described_subject! 164 | end 165 | 166 | def set_described_subject! 167 | subject { 168 | record = self.class.described_class.new 169 | record.send(:attributes=, subject_attributes, false) 170 | record 171 | } 172 | end 173 | end 174 | 175 | # Returns a hash with the subject attributes declared using the 176 | # subject_attributes class method and the attributes given using the 177 | # describe method. 178 | # 179 | # describe Post 180 | # subject_attributes { valid_post_attributes } 181 | # 182 | # describe :published => true do 183 | # it "should have default subject attributes" do 184 | # subject_attributes.should == { :title => 'My title', :published => true } 185 | # end 186 | # end 187 | # end 188 | # 189 | def subject_attributes 190 | default = self.class.default_subject_attributes 191 | default = self.instance_eval(&default) if default.is_a?(Proc) 192 | default ||= {} 193 | 194 | default.merge(self.class.describe_subject_attributes || {}) 195 | end 196 | 197 | end 198 | end 199 | end -------------------------------------------------------------------------------- /lib/remarkable/mongo_mapper/base.rb: -------------------------------------------------------------------------------- 1 | module Remarkable 2 | module MongoMapper 3 | class Base < Remarkable::Base 4 | I18N_COLLECTION = [ :attributes, :associations ] 5 | 6 | # Provides a way to send options to all MongoMapper matchers. 7 | # 8 | # validates_presence_of(:name).with_options(:allow_nil => false) 9 | # 10 | # Is equivalent to: 11 | # 12 | # validates_presence_of(:name, :allow_nil => false) 13 | # 14 | def with_options(opts={}) 15 | @options.merge!(opts) 16 | self 17 | end 18 | 19 | protected 20 | 21 | # Checks for the given key in @options, if it exists and it's true, 22 | # tests that the value is bad, otherwise tests that the value is good. 23 | # 24 | # It accepts the key to check for, the value that is used for testing 25 | # and an @options key where the message to search for is. 26 | # 27 | def assert_bad_or_good_if_key(key, value, message_key=:message) #:nodoc: 28 | return positive? unless @options.key?(key) 29 | 30 | if @options[key] 31 | return bad?(value, message_key), :not => not_word 32 | else 33 | return good?(value, message_key), :not => '' 34 | end 35 | end 36 | 37 | # Checks for the given key in @options, if it exists and it's true, 38 | # tests that the value is good, otherwise tests that the value is bad. 39 | # 40 | # It accepts the key to check for, the value that is used for testing 41 | # and an @options key where the message to search for is. 42 | # 43 | def assert_good_or_bad_if_key(key, value, message_key=:message) #:nodoc: 44 | return positive? unless @options.key?(key) 45 | 46 | if @options[key] 47 | return good?(value, message_key), :not => '' 48 | else 49 | return bad?(value, message_key), :not => not_word 50 | end 51 | end 52 | 53 | # Default allow_nil? validation. It accepts the message_key which is 54 | # the key which contain the message in @options. 55 | # 56 | # It also gets an allow_nil message on remarkable.mongo_mapper.allow_nil 57 | # to be used as default. 58 | # 59 | def allow_nil?(message_key=:message) #:nodoc: 60 | assert_good_or_bad_if_key(:allow_nil, nil, message_key) 61 | end 62 | 63 | # Default allow_blank? validation. It accepts the message_key which is 64 | # the key which contain the message in @options. 65 | # 66 | # It also gets an allow_blank message on remarkable.mongo_mapper.allow_blank 67 | # to be used as default. 68 | # 69 | def allow_blank?(message_key=:message) #:nodoc: 70 | assert_good_or_bad_if_key(:allow_blank, '', message_key) 71 | end 72 | 73 | # Shortcut for assert_good_value. 74 | # 75 | def good?(value, message_sym=:message) #:nodoc: 76 | assert_good_value(@subject, @attribute, value, @options[message_sym]) 77 | end 78 | 79 | # Shortcut for assert_bad_value. 80 | # 81 | def bad?(value, message_sym=:message) #:nodoc: 82 | assert_bad_value(@subject, @attribute, value, @options[message_sym]) 83 | end 84 | 85 | # Asserts that an MongoMapper model validates with the passed 86 | # value by making sure the error_message_to_avoid is not 87 | # contained within the list of errors for that attribute. 88 | # 89 | # assert_good_value(User.new, :email, "user@example.com") 90 | # assert_good_value(User.new, :ssn, "123456789", /length/) 91 | # 92 | # If a class is passed as the first argument, a new object will be 93 | # instantiated before the assertion. If an instance variable exists with 94 | # the same name as the class (underscored), that object will be used 95 | # instead. 96 | # 97 | # assert_good_value(User, :email, "user@example.com") 98 | # 99 | # @product = Product.new(:tangible => false) 100 | # assert_good_value(Product, :price, "0") 101 | # 102 | def assert_good_value(model, attribute, value, error_message_to_avoid=//) # :nodoc: 103 | model.send("#{attribute}=", value) 104 | 105 | return true if model.valid? 106 | 107 | error_message_to_avoid = error_message_from_model(model, attribute, error_message_to_avoid) 108 | assert_does_not_contain(model.errors.on(attribute), error_message_to_avoid) 109 | end 110 | 111 | # Asserts that an MongoMapper model invalidates the passed 112 | # value by making sure the error_message_to_expect is 113 | # contained within the list of errors for that attribute. 114 | # 115 | # assert_bad_value(User.new, :email, "invalid") 116 | # assert_bad_value(User.new, :ssn, "123", /length/) 117 | # 118 | # If a class is passed as the first argument, a new object will be 119 | # instantiated before the assertion. If an instance variable exists with 120 | # the same name as the class (underscored), that object will be used 121 | # instead. 122 | # 123 | # assert_bad_value(User, :email, "invalid") 124 | # 125 | # @product = Product.new(:tangible => true) 126 | # assert_bad_value(Product, :price, "0") 127 | # 128 | def assert_bad_value(model, attribute, value, error_message_to_expect=:invalid) #:nodoc: 129 | model.send("#{attribute}=", value) 130 | 131 | return false if model.valid? || model.errors.on(attribute).blank? 132 | 133 | error_message_to_expect = error_message_from_model(model, attribute, error_message_to_expect) 134 | assert_contains(model.errors.on(attribute), error_message_to_expect) 135 | end 136 | 137 | # Return the error message to be checked. If the message is not a Symbol 138 | # neither a Hash, it returns the own message. 139 | # 140 | # But the nice thing is that when the message is a Symbol we get the error 141 | # messsage from within the model, using already existent structure inside 142 | # MongoMapper. 143 | # 144 | # This allows a couple things from the user side: 145 | # 146 | # 1. Specify symbols in their tests: 147 | # 148 | # should_allow_values_for(:shirt_size, 'S', 'M', 'L', :message => :inclusion) 149 | # 150 | # As we know, allow_values_for searches for a :invalid message. So if we 151 | # were testing a validates_inclusion_of with allow_values_for, previously 152 | # we had to do something like this: 153 | # 154 | # should_allow_values_for(:shirt_size, 'S', 'M', 'L', :message => 'not included in list') 155 | # 156 | # Now everything gets resumed to a Symbol. 157 | # 158 | # 2. Do not worry with specs if their are using I18n API properly. 159 | # 160 | # As we know, I18n API provides several interpolation options besides 161 | # fallback when creating error messages. If the user changed the message, 162 | # macros would start to pass when they shouldn't. 163 | # 164 | # Using the underlying mechanism inside ActiveRecord makes us free from 165 | # all thos errors. 166 | # 167 | # We replace {{count}} interpolation for 12345 which later is replaced 168 | # by a regexp which contains \d+. 169 | # 170 | def error_message_from_model(model, attribute, message) #:nodoc: 171 | # FIXME 172 | message 173 | end 174 | 175 | # Asserts that the given collection does not contain item x. If x is a 176 | # regular expression, ensure that none of the elements from the collection 177 | # match x. 178 | # 179 | def assert_does_not_contain(collection, x) #:nodoc: 180 | !assert_contains(collection, x) 181 | end 182 | 183 | # Changes how collection are interpolated to provide localized names 184 | # whenever is possible. 185 | # 186 | def collection_interpolation #:nodoc: 187 | described_class = if @subject 188 | subject_class 189 | elsif @spec 190 | @spec.send(:described_class) 191 | end 192 | 193 | if i18n_collection? && described_class.respond_to?(:human_attribute_name) 194 | options = {} 195 | 196 | collection_name = self.class.matcher_arguments[:collection].to_sym 197 | if collection = instance_variable_get("@#{collection_name}") 198 | collection = collection.map do |attr| 199 | described_class.human_attribute_name(attr.to_s, :locale => Remarkable.locale).downcase 200 | end 201 | options[collection_name] = array_to_sentence(collection) 202 | end 203 | 204 | object_name = self.class.matcher_arguments[:as] 205 | if object = instance_variable_get("@#{object_name}") 206 | object = described_class.human_attribute_name(object.to_s, :locale => Remarkable.locale).downcase 207 | options[object_name] = object 208 | end 209 | 210 | options 211 | else 212 | super 213 | end 214 | end 215 | 216 | # Returns true if the given collection should be translated. 217 | # 218 | def i18n_collection? #:nodoc: 219 | RAILS_I18N && I18N_COLLECTION.include?(self.class.matcher_arguments[:collection]) 220 | end 221 | end 222 | end 223 | end --------------------------------------------------------------------------------