remarkable_mongo 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ .DS_Store
data/LICENSE ADDED
@@ -0,0 +1,20 @@
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.
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # Remarkable MongoMapper
2
+
3
+ Remarkable matchers for [MongoMapper](http://github.com/jnunemaker/mongomapper).
4
+
5
+ ## Matchers
6
+
7
+ <pre><code>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) }</code></pre>
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
data/Rakefile ADDED
@@ -0,0 +1,28 @@
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 Mérouze"]
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
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.2
@@ -0,0 +1,30 @@
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
@@ -0,0 +1,223 @@
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
+ # <tt>value</tt> by making sure the <tt>error_message_to_avoid</tt> 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
+ # <tt>value</tt> by making sure the <tt>error_message_to_expect</tt> 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
@@ -0,0 +1,199 @@
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