remarkable 3.0.0
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/CHANGELOG +16 -0
- data/LICENSE +20 -0
- data/README +2 -0
- data/lib/remarkable/base.rb +54 -0
- data/lib/remarkable/core_ext/array.rb +19 -0
- data/lib/remarkable/dsl/assertions.rb +184 -0
- data/lib/remarkable/dsl/callbacks.rb +54 -0
- data/lib/remarkable/dsl/matches.rb +142 -0
- data/lib/remarkable/dsl/optionals.rb +142 -0
- data/lib/remarkable/dsl.rb +39 -0
- data/lib/remarkable/i18n.rb +54 -0
- data/lib/remarkable/macros.rb +48 -0
- data/lib/remarkable/matchers.rb +19 -0
- data/lib/remarkable/messages.rb +98 -0
- data/lib/remarkable/pending.rb +33 -0
- data/lib/remarkable/rspec.rb +26 -0
- data/lib/remarkable/version.rb +3 -0
- data/lib/remarkable.rb +20 -0
- data/locale/en.yml +14 -0
- data/spec/base_spec.rb +42 -0
- data/spec/dsl/assertions_spec.rb +54 -0
- data/spec/dsl/optionals_spec.rb +42 -0
- data/spec/i18n_spec.rb +41 -0
- data/spec/locale/en.yml +19 -0
- data/spec/locale/pt-BR.yml +21 -0
- data/spec/macros_spec.rb +26 -0
- data/spec/matchers/be_a_person_matcher.rb +25 -0
- data/spec/matchers/collection_contain_matcher.rb +32 -0
- data/spec/matchers/contain_matcher.rb +31 -0
- data/spec/matchers/single_contain_matcher.rb +50 -0
- data/spec/messages_spec.rb +65 -0
- data/spec/pending_spec.rb +12 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +16 -0
- metadata +105 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# v3.0.0
|
2
|
+
|
3
|
+
* Added Remarkable::Matchers. Now you can include your Remarkable matchers and
|
4
|
+
macros in test unit as well.
|
5
|
+
|
6
|
+
class Test::Unit::TestCase
|
7
|
+
include Spec::Matchers
|
8
|
+
include Remarkable::Matchers
|
9
|
+
extend Remarkable::Macros
|
10
|
+
end
|
11
|
+
|
12
|
+
* Added pending and disabled macros
|
13
|
+
* Added I18n
|
14
|
+
* Added DSL core structure
|
15
|
+
* Added macros core structure
|
16
|
+
* Added matchers core structure
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Carlos Brando
|
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
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module Remarkable
|
2
|
+
class Base
|
3
|
+
include Remarkable::Messages
|
4
|
+
extend Remarkable::DSL
|
5
|
+
|
6
|
+
def spec(binding)
|
7
|
+
@spec = binding
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
# Returns the subject class if it's not one.
|
14
|
+
def subject_class
|
15
|
+
nil unless @subject
|
16
|
+
@subject.is_a?(Class) ? @subject : @subject.class
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns the subject name based on its class. If the class respond to
|
20
|
+
# human_name (which is usually localized) returns it.
|
21
|
+
def subject_name
|
22
|
+
nil unless @subject
|
23
|
+
subject_class.respond_to?(:human_name) ? subject_class.human_name : subject_class.name
|
24
|
+
end
|
25
|
+
|
26
|
+
# Iterates over the collection given yielding the block and return false
|
27
|
+
# if any of them also returns false.
|
28
|
+
def assert_matcher_for(collection)
|
29
|
+
collection.each do |item|
|
30
|
+
return false unless yield(item)
|
31
|
+
end
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
# Asserts that the given collection contains item x. If x is a regular
|
36
|
+
# expression, ensure that at least one element from the collection matches x.
|
37
|
+
#
|
38
|
+
# assert_contains(['a', '1'], /\d/) => passes
|
39
|
+
# assert_contains(['a', '1'], 'a') => passes
|
40
|
+
# assert_contains(['a', '1'], /not there/) => fails
|
41
|
+
#
|
42
|
+
def assert_contains(collection, x) # :nodoc:
|
43
|
+
collection = [collection] unless collection.is_a?(Array)
|
44
|
+
|
45
|
+
case x
|
46
|
+
when Regexp
|
47
|
+
collection.detect { |e| e =~ x }
|
48
|
+
else
|
49
|
+
collection.include?(x)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Remarkable # :nodoc:
|
2
|
+
module CoreExt
|
3
|
+
module Array
|
4
|
+
# Extracts options from a set of arguments. Removes and returns the last
|
5
|
+
# element in the array if it's a hash, otherwise returns a blank hash.
|
6
|
+
#
|
7
|
+
# def options(*args)
|
8
|
+
# args.extract_options!
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# options(1, 2) # => {}
|
12
|
+
# options(1, 2, :a => :b) # => {:a=>:b}
|
13
|
+
def extract_options!
|
14
|
+
last.is_a?(::Hash) ? pop : {}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
Array.send :include, Remarkable::CoreExt::Array
|
@@ -0,0 +1,184 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module DSL
|
3
|
+
module Assertions
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
# It sets the arguments your matcher receives on initialization.
|
8
|
+
#
|
9
|
+
# arguments :name, :range
|
10
|
+
#
|
11
|
+
# Which is roughly the same as:
|
12
|
+
#
|
13
|
+
# def initialize(name, range, options = {})
|
14
|
+
# @name = name
|
15
|
+
# @range = range
|
16
|
+
# @options = options
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# But most of the time your matchers iterates through a collection,
|
20
|
+
# such as a collection of attributes in the case below:
|
21
|
+
#
|
22
|
+
# @product.should validate_presence_of(:title, :name)
|
23
|
+
#
|
24
|
+
# validate_presence_of is a matcher declared as:
|
25
|
+
#
|
26
|
+
# class ValidatePresenceOfMatcher < Remarkable::Base
|
27
|
+
# arguments :collection => :attributes
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# In this case, Remarkable provides an API that enables you to easily
|
31
|
+
# assert each item of the collection. Let's check more examples:
|
32
|
+
#
|
33
|
+
# should allow_values_for(:email, "jose@valim.com", "carlos@brando.com")
|
34
|
+
#
|
35
|
+
# Is declared as:
|
36
|
+
#
|
37
|
+
# arguments :attribute, :collection => :good_values, :as => :good_value
|
38
|
+
#
|
39
|
+
# And this is the same as:
|
40
|
+
#
|
41
|
+
# class AllowValuesForMatcher < Remarkable::Base
|
42
|
+
# def initialize(attribute, *good_values)
|
43
|
+
# @attribute = attribute
|
44
|
+
# @options = default_options.merge(good_values.extract_options!)
|
45
|
+
# @good_values = good_values
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# Now, the collection is @good_values. In each assertion method we will
|
50
|
+
# have a @good_value variable (in singular) instantiated with the value
|
51
|
+
# to assert.
|
52
|
+
#
|
53
|
+
# Finally, if your matcher deals with blocks, you can also set them as
|
54
|
+
# option:
|
55
|
+
#
|
56
|
+
# arguments :name, :block => :builder
|
57
|
+
#
|
58
|
+
# It will be available under the instance variable @builder.
|
59
|
+
#
|
60
|
+
def arguments(*names)
|
61
|
+
options = names.extract_options!
|
62
|
+
args = names.dup
|
63
|
+
|
64
|
+
@matcher_arguments[:names] = names
|
65
|
+
|
66
|
+
if collection = options.delete(:collection)
|
67
|
+
@matcher_arguments[:collection] = collection
|
68
|
+
|
69
|
+
if options[:as]
|
70
|
+
@matcher_arguments[:as] = options.delete(:as)
|
71
|
+
else
|
72
|
+
raise ArgumentError, 'You gave me :collection as option but have not give me :as as well'
|
73
|
+
end
|
74
|
+
|
75
|
+
args << "*#{collection}"
|
76
|
+
get_options = "#{collection}.extract_options!"
|
77
|
+
set_collection = "@#{collection} = #{collection}"
|
78
|
+
else
|
79
|
+
args << 'options={}'
|
80
|
+
get_options = 'options'
|
81
|
+
set_collection = ''
|
82
|
+
end
|
83
|
+
|
84
|
+
if block = options.delete(:block)
|
85
|
+
@matcher_arguments[:block] = block
|
86
|
+
args << "&#{block}"
|
87
|
+
names << block
|
88
|
+
end
|
89
|
+
|
90
|
+
assignments = names.map do |name|
|
91
|
+
"@#{name} = #{name}"
|
92
|
+
end.join("\n ")
|
93
|
+
|
94
|
+
class_eval <<-END, __FILE__, __LINE__
|
95
|
+
def initialize(#{args.join(',')})
|
96
|
+
#{assignments}
|
97
|
+
@options = default_options.merge(#{get_options})
|
98
|
+
#{set_collection}
|
99
|
+
run_after_initialize_callbacks
|
100
|
+
end
|
101
|
+
END
|
102
|
+
end
|
103
|
+
|
104
|
+
# Call it to declare your collection assertions. Every method given will
|
105
|
+
# iterate through the whole collection given in <tt>:arguments</tt>.
|
106
|
+
#
|
107
|
+
# For example, validate_presence_of can be written as:
|
108
|
+
#
|
109
|
+
# class ValidatePresenceOfMatcher < Remarkable::Base
|
110
|
+
# arguments :collection => :attributes
|
111
|
+
# collection_assertions :allow_nil?
|
112
|
+
#
|
113
|
+
# protected
|
114
|
+
# def allow_nil?
|
115
|
+
# # matcher logic
|
116
|
+
# end
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# Then we call it as:
|
120
|
+
#
|
121
|
+
# should validate_presence_of(:email, :password)
|
122
|
+
#
|
123
|
+
# For each attribute given, it will call the method :allow_nil which
|
124
|
+
# contains the matcher logic. As stated in <tt>arguments</tt>, those
|
125
|
+
# attributes will be available under the instance variable @argument
|
126
|
+
# and the matcher subject is available under the instance variable
|
127
|
+
# @subject.
|
128
|
+
#
|
129
|
+
# If a block is given, it will create a method with the name given.
|
130
|
+
# So we could write the same class as above just as:
|
131
|
+
#
|
132
|
+
# class ValidatePresenceOfMatcher < Remarkable::Base
|
133
|
+
# arguments :collection => :attributes
|
134
|
+
#
|
135
|
+
# collection_assertion :allow_nil? do
|
136
|
+
# # matcher logic
|
137
|
+
# end
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# Those methods should return true if it pass or false if it fails. When
|
141
|
+
# it fails, it will use I18n API to find the proper failure message:
|
142
|
+
#
|
143
|
+
# expectations:
|
144
|
+
# allow_nil: allowed the value to be nil
|
145
|
+
# allow_blank: allowed the value to be blank
|
146
|
+
#
|
147
|
+
# Or you can set the message in the instance variable @expectation in the
|
148
|
+
# assertion method if you don't want to rely on I18n API.
|
149
|
+
#
|
150
|
+
# As you might have noticed from the examples above, this method is also
|
151
|
+
# aliased as <tt>collection_assertion</tt>.
|
152
|
+
#
|
153
|
+
def collection_assertions(*methods, &block)
|
154
|
+
define_method methods.last, &block if block_given?
|
155
|
+
@matcher_collection_assertions += methods
|
156
|
+
end
|
157
|
+
alias :collection_assertion :collection_assertions
|
158
|
+
|
159
|
+
# In contrast to <tt>collection_assertions</tt>, the methods given here
|
160
|
+
# are called just once. In other words, it does not iterate through the
|
161
|
+
# collection given in arguments.
|
162
|
+
#
|
163
|
+
# It also accepts blocks and is aliased as assertion.
|
164
|
+
#
|
165
|
+
def assertions(*methods, &block)
|
166
|
+
define_method methods.last, &block if block_given?
|
167
|
+
@matcher_single_assertions += methods
|
168
|
+
end
|
169
|
+
alias :assertion :assertions
|
170
|
+
|
171
|
+
# Class method that accepts a block or a Hash that will overwrite
|
172
|
+
# instance method default_options.
|
173
|
+
#
|
174
|
+
def default_options(hash = {}, &block)
|
175
|
+
if block_given?
|
176
|
+
define_method :default_options, &block
|
177
|
+
else
|
178
|
+
class_eval "def default_options; #{hash.inspect}; end"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module DSL
|
3
|
+
module Callbacks
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
protected
|
11
|
+
# Class method that accepts a block which is called after initialization.
|
12
|
+
#
|
13
|
+
def after_initialize(symbol=nil, &block)
|
14
|
+
if block_given?
|
15
|
+
@after_initialize_callbacks << block
|
16
|
+
elsif symbol
|
17
|
+
@after_initialize_callbacks << symbol
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Class method that accepts a block which is called before assertion.
|
22
|
+
#
|
23
|
+
def before_assert(symbol=nil, &block)
|
24
|
+
if block_given?
|
25
|
+
@before_assert_callbacks << block
|
26
|
+
elsif symbol
|
27
|
+
@before_assert_callbacks << symbol
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def run_after_initialize_callbacks
|
33
|
+
self.class.after_initialize_callbacks.each do |method|
|
34
|
+
if method.is_a?(Proc)
|
35
|
+
instance_eval &method
|
36
|
+
elsif method.is_a?(Symbol)
|
37
|
+
send(method)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def run_before_assert_callbacks
|
43
|
+
self.class.before_assert_callbacks.each do |method|
|
44
|
+
if method.is_a?(Proc)
|
45
|
+
instance_eval &method
|
46
|
+
elsif method.is_a?(Symbol)
|
47
|
+
send(method)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module DSL
|
3
|
+
module Matches
|
4
|
+
|
5
|
+
# For each instance under the collection declared in <tt>arguments</tt>,
|
6
|
+
# this method will call each method declared in <tt>assertions</tt>.
|
7
|
+
#
|
8
|
+
# As an example, let's assume you have the following matcher:
|
9
|
+
#
|
10
|
+
# arguments :collection => :attributes
|
11
|
+
# assertions :allow_nil?, :allow_blank?
|
12
|
+
#
|
13
|
+
# For each attribute in @attributes, we will set the instance variable
|
14
|
+
# @attribute and then call allow_nil? and allow_blank?. Assertions should
|
15
|
+
# return true if it pass or false if it fails. When it fails, it will use
|
16
|
+
# I18n API to find the proper failure message:
|
17
|
+
#
|
18
|
+
# expectations:
|
19
|
+
# allow_nil?: allowed the value to be nil
|
20
|
+
# allow_blank?: allowed the value to be blank
|
21
|
+
#
|
22
|
+
# Or you can set the message in the instance variable @expectation in the
|
23
|
+
# assertion method if you don't want to rely on I18n API.
|
24
|
+
#
|
25
|
+
# This method also call the methods declared in single_assertions. Which
|
26
|
+
# work the same way as assertions, except it doesn't loop for each value in
|
27
|
+
# the collection.
|
28
|
+
#
|
29
|
+
# It also provides a before_assert callback that you might want to use it
|
30
|
+
# to manipulate the subject before the assertions start.
|
31
|
+
#
|
32
|
+
def matches?(subject)
|
33
|
+
@subject = subject
|
34
|
+
|
35
|
+
run_before_assert_callbacks
|
36
|
+
|
37
|
+
send_methods_and_generate_message(self.class.matcher_single_assertions) &&
|
38
|
+
assert_matcher_for(instance_variable_get("@#{self.class.matcher_arguments[:collection]}") || []) do |value|
|
39
|
+
instance_variable_set("@#{self.class.matcher_arguments[:as]}", value)
|
40
|
+
send_methods_and_generate_message(self.class.matcher_collection_assertions)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
# Overwrite to provide default options.
|
47
|
+
#
|
48
|
+
def default_options
|
49
|
+
{}
|
50
|
+
end
|
51
|
+
|
52
|
+
# Overwrites default_i18n_options to provide collection interpolation,
|
53
|
+
# arguments and optionals to interpolation options.
|
54
|
+
#
|
55
|
+
# Their are appended in the reverse order above. So if you have an optional
|
56
|
+
# with the same name as an argument, the argument overwrites the optional.
|
57
|
+
#
|
58
|
+
# All values are provided calling inspect, so what you will have in your
|
59
|
+
# I18n available for interpolation is @options[:allow_nil].inspect.
|
60
|
+
#
|
61
|
+
# If you still need to provide more other interpolation options, you can
|
62
|
+
# do that in two ways:
|
63
|
+
#
|
64
|
+
# 1. Overwrite interpolation_options:
|
65
|
+
#
|
66
|
+
# def interpolation_options
|
67
|
+
# { :real_value => real_value }
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# 2. Return a hash from your assertion method:
|
71
|
+
#
|
72
|
+
# def my_assertion
|
73
|
+
# return true if real_value == expected_value
|
74
|
+
# return false, :real_value => real_value
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# In both cases, :real_value will be available as interpolation option.
|
78
|
+
#
|
79
|
+
def default_i18n_options
|
80
|
+
i18n_options = {}
|
81
|
+
|
82
|
+
@options.each do |key, value|
|
83
|
+
i18n_options[key] = value.inspect
|
84
|
+
end if @options
|
85
|
+
|
86
|
+
# Also add arguments as interpolation options.
|
87
|
+
self.class.matcher_arguments[:names].each do |name|
|
88
|
+
i18n_options[name] = instance_variable_get("@#{name}").inspect
|
89
|
+
end
|
90
|
+
|
91
|
+
# Add collection interpolation options.
|
92
|
+
i18n_options.update(collection_interpolation)
|
93
|
+
|
94
|
+
# Add default options (highest priority). They should not be overwritten.
|
95
|
+
i18n_options.update(super)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Methods that return collection_name and object_name as a Hash for
|
99
|
+
# interpolation.
|
100
|
+
#
|
101
|
+
def collection_interpolation
|
102
|
+
options = {}
|
103
|
+
|
104
|
+
# Add collection to options
|
105
|
+
if collection_name = self.class.matcher_arguments[:collection]
|
106
|
+
collection_name = collection_name.to_sym
|
107
|
+
collection = instance_variable_get("@#{collection_name}")
|
108
|
+
options[collection_name] = array_to_sentence(collection) if collection
|
109
|
+
|
110
|
+
object_name = self.class.matcher_arguments[:as].to_sym
|
111
|
+
object = instance_variable_get("@#{object_name}")
|
112
|
+
options[object_name] = object if object
|
113
|
+
end
|
114
|
+
|
115
|
+
options
|
116
|
+
end
|
117
|
+
|
118
|
+
# Helper that send the methods given and create a expectation message if
|
119
|
+
# any returns false.
|
120
|
+
#
|
121
|
+
# Since most assertion methods ends with an question mark and it's not
|
122
|
+
# readable in yml files, we remove question and exclation marks at the
|
123
|
+
# end of the method name before translating it. So if you have a method
|
124
|
+
# called is_valid? on I18n yml file we will check for a key :is_valid.
|
125
|
+
#
|
126
|
+
def send_methods_and_generate_message(methods)
|
127
|
+
methods.each do |method|
|
128
|
+
bool, hash = send(method)
|
129
|
+
|
130
|
+
unless bool
|
131
|
+
@expectation ||= Remarkable.t "expectations.#{method.to_s.gsub(/(\?|\!)$/, '')}",
|
132
|
+
default_i18n_options.merge(hash || {})
|
133
|
+
return false
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
return true
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|