├── 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
--------------------------------------------------------------------------------