remarkable 3.1.8 → 3.1.9
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 +58 -58
- data/LICENSE +20 -20
- data/README +197 -197
- data/lib/remarkable.rb +18 -18
- data/lib/remarkable/base.rb +55 -56
- data/lib/remarkable/core_ext/array.rb +19 -19
- data/lib/remarkable/dsl.rb +35 -35
- data/lib/remarkable/dsl/assertions.rb +384 -384
- data/lib/remarkable/dsl/callbacks.rb +52 -52
- data/lib/remarkable/dsl/optionals.rb +122 -122
- data/lib/remarkable/i18n.rb +53 -53
- data/lib/remarkable/macros.rb +48 -48
- data/lib/remarkable/matchers.rb +17 -17
- data/lib/remarkable/messages.rb +103 -103
- data/lib/remarkable/pending.rb +66 -66
- data/lib/remarkable/rspec.rb +24 -24
- data/lib/remarkable/version.rb +3 -3
- data/locale/en.yml +14 -14
- data/spec/base_spec.rb +41 -41
- data/spec/dsl/assertions_spec.rb +54 -54
- data/spec/dsl/optionals_spec.rb +237 -237
- data/spec/i18n_spec.rb +41 -41
- data/spec/locale/en.yml +20 -20
- data/spec/locale/pt-BR.yml +21 -21
- data/spec/macros_spec.rb +26 -26
- data/spec/matchers/be_a_person_matcher.rb +25 -25
- data/spec/matchers/collection_contain_matcher.rb +32 -32
- data/spec/matchers/contain_matcher.rb +31 -31
- data/spec/matchers/single_contain_matcher.rb +49 -49
- data/spec/matchers_spec.rb +5 -5
- data/spec/messages_spec.rb +66 -66
- data/spec/pending_spec.rb +7 -7
- data/spec/spec.opts +4 -4
- data/spec/spec_helper.rb +15 -15
- metadata +3 -3
data/lib/remarkable.rb
CHANGED
@@ -1,19 +1,19 @@
|
|
1
|
-
# Load core files
|
2
|
-
dir = File.dirname(__FILE__)
|
3
|
-
require File.join(dir, 'remarkable', 'version')
|
4
|
-
require File.join(dir, 'remarkable', 'matchers')
|
5
|
-
require File.join(dir, 'remarkable', 'i18n')
|
6
|
-
require File.join(dir, 'remarkable', 'dsl')
|
7
|
-
require File.join(dir, 'remarkable', 'messages')
|
8
|
-
|
9
|
-
require File.join(dir, 'remarkable', 'base')
|
10
|
-
require File.join(dir, 'remarkable', 'macros')
|
1
|
+
# Load core files
|
2
|
+
dir = File.dirname(__FILE__)
|
3
|
+
require File.join(dir, 'remarkable', 'version')
|
4
|
+
require File.join(dir, 'remarkable', 'matchers')
|
5
|
+
require File.join(dir, 'remarkable', 'i18n')
|
6
|
+
require File.join(dir, 'remarkable', 'dsl')
|
7
|
+
require File.join(dir, 'remarkable', 'messages')
|
8
|
+
|
9
|
+
require File.join(dir, 'remarkable', 'base')
|
10
|
+
require File.join(dir, 'remarkable', 'macros')
|
11
11
|
require File.join(dir, 'remarkable', 'pending')
|
12
|
-
require File.join(dir, 'remarkable', 'negative')
|
13
|
-
require File.join(dir, 'remarkable', 'core_ext', 'array')
|
14
|
-
|
15
|
-
if defined?(Spec)
|
16
|
-
require File.join(dir, 'remarkable', 'rspec')
|
17
|
-
end
|
18
|
-
|
19
|
-
Remarkable.add_locale File.join(dir, '..', 'locale', 'en.yml')
|
12
|
+
require File.join(dir, 'remarkable', 'negative')
|
13
|
+
require File.join(dir, 'remarkable', 'core_ext', 'array')
|
14
|
+
|
15
|
+
if defined?(Spec)
|
16
|
+
require File.join(dir, 'remarkable', 'rspec')
|
17
|
+
end
|
18
|
+
|
19
|
+
Remarkable.add_locale File.join(dir, '..', 'locale', 'en.yml')
|
data/lib/remarkable/base.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
|
-
module Remarkable
|
2
|
-
# This class holds the basic structure for Remarkable matchers. All matchers
|
3
|
-
# must inherit from it.
|
4
|
-
class Base
|
5
|
-
include Remarkable::Messages
|
6
|
-
extend Remarkable::DSL
|
7
|
-
|
8
|
-
# Optional to provide spec binding to matchers.
|
9
|
-
def spec(binding)
|
10
|
-
@spec = binding
|
11
|
-
self
|
12
|
-
end
|
1
|
+
module Remarkable
|
2
|
+
# This class holds the basic structure for Remarkable matchers. All matchers
|
3
|
+
# must inherit from it.
|
4
|
+
class Base
|
5
|
+
include Remarkable::Messages
|
6
|
+
extend Remarkable::DSL
|
7
|
+
|
8
|
+
# Optional to provide spec binding to matchers.
|
9
|
+
def spec(binding)
|
10
|
+
@spec = binding
|
11
|
+
self
|
12
|
+
end
|
13
13
|
|
14
14
|
def positive?
|
15
15
|
!negative?
|
@@ -18,25 +18,24 @@ module Remarkable
|
|
18
18
|
def negative?
|
19
19
|
false
|
20
20
|
end
|
21
|
-
|
22
|
-
protected
|
23
|
-
|
24
|
-
# Returns the subject class unless it's a class object.
|
25
|
-
def subject_class
|
26
|
-
nil unless @subject
|
27
|
-
@subject.is_a?(Class) ? @subject : @subject.class
|
28
|
-
end
|
29
|
-
|
30
|
-
# Returns the subject name based on its class.
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
#
|
38
|
-
|
39
|
-
def assert_collection(key, collection, inspect=true) #:nodoc:
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
# Returns the subject class unless it's a class object.
|
25
|
+
def subject_class
|
26
|
+
nil unless @subject
|
27
|
+
@subject.is_a?(Class) ? @subject : @subject.class
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the subject name based on its class.
|
31
|
+
def subject_name
|
32
|
+
nil unless @subject
|
33
|
+
subject_class.name
|
34
|
+
end
|
35
|
+
|
36
|
+
# Iterates over the collection given yielding the block and return false
|
37
|
+
# if any of them also returns false.
|
38
|
+
def assert_collection(key, collection, inspect=true) #:nodoc:
|
40
39
|
collection.each do |item|
|
41
40
|
value = yield(item)
|
42
41
|
|
@@ -47,28 +46,28 @@ module Remarkable
|
|
47
46
|
else
|
48
47
|
return negative?
|
49
48
|
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
positive?
|
53
|
-
end
|
54
|
-
|
55
|
-
# Asserts that the given collection contains item x. If x is a regular
|
56
|
-
# expression, ensure that at least one element from the collection matches x.
|
57
|
-
#
|
58
|
-
# assert_contains(['a', '1'], /\d/) => passes
|
59
|
-
# assert_contains(['a', '1'], 'a') => passes
|
60
|
-
# assert_contains(['a', '1'], /not there/) => fails
|
61
|
-
#
|
62
|
-
def assert_contains(collection, x)
|
63
|
-
collection = [collection] unless collection.is_a?(Array)
|
64
|
-
|
65
|
-
case x
|
66
|
-
when Regexp
|
67
|
-
collection.detect { |e| e =~ x }
|
68
|
-
else
|
69
|
-
collection.include?(x)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
end
|
74
|
-
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
positive?
|
52
|
+
end
|
53
|
+
|
54
|
+
# Asserts that the given collection contains item x. If x is a regular
|
55
|
+
# expression, ensure that at least one element from the collection matches x.
|
56
|
+
#
|
57
|
+
# assert_contains(['a', '1'], /\d/) => passes
|
58
|
+
# assert_contains(['a', '1'], 'a') => passes
|
59
|
+
# assert_contains(['a', '1'], /not there/) => fails
|
60
|
+
#
|
61
|
+
def assert_contains(collection, x)
|
62
|
+
collection = [collection] unless collection.is_a?(Array)
|
63
|
+
|
64
|
+
case x
|
65
|
+
when Regexp
|
66
|
+
collection.detect { |e| e =~ x }
|
67
|
+
else
|
68
|
+
collection.include?(x)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -1,19 +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
|
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
|
data/lib/remarkable/dsl.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
dir = File.dirname(__FILE__)
|
2
|
-
require File.join(dir, 'dsl', 'assertions')
|
3
|
-
require File.join(dir, 'dsl', 'optionals')
|
4
|
-
require File.join(dir, 'dsl', 'callbacks')
|
5
|
-
|
1
|
+
dir = File.dirname(__FILE__)
|
2
|
+
require File.join(dir, 'dsl', 'assertions')
|
3
|
+
require File.join(dir, 'dsl', 'optionals')
|
4
|
+
require File.join(dir, 'dsl', 'callbacks')
|
5
|
+
|
6
6
|
module Remarkable
|
7
7
|
# The DSL module is responsable for all Remarkable convenience methods.
|
8
8
|
# It has three main submodules:
|
@@ -15,41 +15,41 @@ module Remarkable
|
|
15
15
|
#
|
16
16
|
# * <tt>Optionals</tt> - add an optionals DSL, which is also used for the auto configuring blocks
|
17
17
|
# and dynamic descriptions.
|
18
|
-
#
|
19
|
-
module DSL
|
18
|
+
#
|
19
|
+
module DSL
|
20
20
|
ATTR_READERS = [
|
21
21
|
:matcher_arguments,
|
22
22
|
:matcher_optionals,
|
23
23
|
:matcher_optionals_splat,
|
24
24
|
:matcher_optionals_block,
|
25
|
-
:matcher_single_assertions,
|
25
|
+
:matcher_single_assertions,
|
26
26
|
:matcher_collection_assertions,
|
27
27
|
:before_assert_callbacks,
|
28
28
|
:after_initialize_callbacks
|
29
|
-
] unless self.const_defined?(:ATTR_READERS)
|
30
|
-
|
31
|
-
def self.extended(base) #:nodoc:
|
32
|
-
base.send :include, Assertions
|
33
|
-
base.send :include, Callbacks
|
34
|
-
base.send :include, Optionals
|
35
|
-
|
36
|
-
# Initialize matcher_arguments hash with names as an empty array
|
37
|
-
base.instance_variable_set('@matcher_arguments', { :names => [] })
|
38
|
-
end
|
39
|
-
|
40
|
-
# Make Remarkable::Base DSL inheritable.
|
41
|
-
#
|
42
|
-
def inherited(base) #:nodoc:
|
43
|
-
base.class_eval do
|
44
|
-
class << self
|
45
|
-
attr_reader *ATTR_READERS
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
ATTR_READERS.each do |attr|
|
50
|
-
current_value = self.instance_variable_get("@#{attr}")
|
51
|
-
base.instance_variable_set("@#{attr}", current_value ? current_value.dup : [])
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
29
|
+
] unless self.const_defined?(:ATTR_READERS)
|
30
|
+
|
31
|
+
def self.extended(base) #:nodoc:
|
32
|
+
base.send :include, Assertions
|
33
|
+
base.send :include, Callbacks
|
34
|
+
base.send :include, Optionals
|
35
|
+
|
36
|
+
# Initialize matcher_arguments hash with names as an empty array
|
37
|
+
base.instance_variable_set('@matcher_arguments', { :names => [] })
|
38
|
+
end
|
39
|
+
|
40
|
+
# Make Remarkable::Base DSL inheritable.
|
41
|
+
#
|
42
|
+
def inherited(base) #:nodoc:
|
43
|
+
base.class_eval do
|
44
|
+
class << self
|
45
|
+
attr_reader *ATTR_READERS
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
ATTR_READERS.each do |attr|
|
50
|
+
current_value = self.instance_variable_get("@#{attr}")
|
51
|
+
base.instance_variable_set("@#{attr}", current_value ? current_value.dup : [])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -1,285 +1,285 @@
|
|
1
|
-
module Remarkable
|
2
|
-
module DSL
|
3
|
-
# This module is responsable to create a basic matcher structure using a DSL.
|
4
|
-
#
|
5
|
-
# A matcher that checks if an element is included in an array can be done
|
6
|
-
# just with:
|
7
|
-
#
|
8
|
-
# class IncludedMatcher < Remarkable::Base
|
9
|
-
# arguments :value
|
10
|
-
# assertion :is_included?
|
11
|
-
#
|
12
|
-
# protected
|
13
|
-
# def is_included?
|
14
|
-
# @subject.include?(@value)
|
15
|
-
# end
|
16
|
-
# end
|
17
|
-
#
|
18
|
-
# As you have noticed, the DSL also allows you to remove the messages from
|
19
|
-
# matcher. Since it will look for it on I18n yml file.
|
20
|
-
#
|
21
|
-
# If you want to create a matcher that accepts multile values to be tested,
|
22
|
-
# you just need to do:
|
23
|
-
#
|
24
|
-
# class IncludedMatcher < Remarkable::Base
|
25
|
-
# arguments :collection => :values, :as => :value
|
26
|
-
# collection_assertion :is_included?
|
27
|
-
#
|
28
|
-
# protected
|
29
|
-
# def is_included?
|
30
|
-
# @subject.include?(@value)
|
31
|
-
# end
|
32
|
-
# end
|
33
|
-
#
|
34
|
-
# Notice that the :is_included? logic didn't have to change, because Remarkable
|
35
|
-
# handle this automatically for you.
|
36
|
-
#
|
37
|
-
module Assertions
|
38
|
-
|
39
|
-
def self.included(base) # :nodoc:
|
40
|
-
base.extend ClassMethods
|
41
|
-
end
|
42
|
-
|
43
|
-
module ClassMethods
|
44
|
-
|
45
|
-
protected
|
46
|
-
|
47
|
-
# It sets the arguments your matcher receives on initialization.
|
48
|
-
#
|
49
|
-
# == Options
|
50
|
-
#
|
51
|
-
# * <tt>:collection</tt> - if a collection is expected.
|
52
|
-
# * <tt>:as</tt> - how each item of the collection will be available.
|
53
|
-
# * <tt>:block</tt> - tell the matcher can receive blocks as argument and store
|
54
|
-
# them under the variable given.
|
55
|
-
#
|
56
|
-
# Note: the expected block cannot have arity 1. This is already reserved
|
57
|
-
# for macro configuration.
|
58
|
-
#
|
59
|
-
# == Examples
|
60
|
-
#
|
61
|
-
# Let's see for each example how the arguments declarion reflects on
|
62
|
-
# the matcher API:
|
63
|
-
#
|
64
|
-
# arguments :assign
|
65
|
-
# # Can be called as:
|
66
|
-
# #=> should_assign :task
|
67
|
-
# #=> should_assign :task, :with => Task.new
|
68
|
-
#
|
69
|
-
# This is roughly the same as:
|
70
|
-
#
|
71
|
-
# def initialize(assign, options = {})
|
72
|
-
# @assign = name
|
73
|
-
# @options = options
|
74
|
-
# end
|
75
|
-
#
|
76
|
-
# As you noticed, a matcher can always receive options on initialization.
|
77
|
-
# If you have a matcher that accepts only options, for example,
|
78
|
-
# have_default_scope you just need to call <tt>arguments</tt>:
|
79
|
-
#
|
80
|
-
# arguments
|
81
|
-
# # Can be called as:
|
82
|
-
# #=> should_have_default_scope :limit => 10
|
83
|
-
#
|
84
|
-
# arguments :collection => :assigns, :as => :assign
|
85
|
-
# # Can be called as:
|
86
|
-
# #=> should_assign :task1, :task2
|
87
|
-
# #=> should_assign :task1, :task2, :with => Task.new
|
88
|
-
#
|
89
|
-
# arguments :collection => :assigns, :as => :assign, :block => :buildeer
|
90
|
-
# # Can be called as:
|
91
|
-
# #=> should_assign :task1, :task2
|
92
|
-
# #=> should_assign(:task1, :task2){ Task.new }
|
93
|
-
#
|
94
|
-
# The block will be available under the instance variable @builder.
|
95
|
-
#
|
96
|
-
# == I18n
|
97
|
-
#
|
98
|
-
# All the parameters given to arguments are available for interpolation
|
99
|
-
# in I18n. So if you have the following declarion:
|
100
|
-
#
|
101
|
-
# class InRange < Remarkable::Base
|
102
|
-
# arguments :range, :collection => :names, :as => :name
|
103
|
-
#
|
104
|
-
# You will have {{range}}, {{names}} and {{name}} available for I18n
|
105
|
-
# messages:
|
106
|
-
#
|
107
|
-
# in_range:
|
108
|
-
# description: "have {{names}} to be on range {{range}}"
|
109
|
-
#
|
110
|
-
# Before a collection is sent to I18n, it's transformed to a sentence.
|
111
|
-
# So if the following matcher:
|
112
|
-
#
|
113
|
-
# in_range(2..20, :username, :password)
|
114
|
-
#
|
115
|
-
# Has the following description:
|
116
|
-
#
|
117
|
-
# "should have username and password in range 2..20"
|
118
|
-
#
|
119
|
-
def arguments(*names)
|
120
|
-
options = names.extract_options!
|
121
|
-
args = names.dup
|
122
|
-
|
123
|
-
@matcher_arguments[:names] = names
|
124
|
-
|
125
|
-
if collection = options.delete(:collection)
|
126
|
-
@matcher_arguments[:collection] = collection
|
127
|
-
|
128
|
-
if options[:as]
|
129
|
-
@matcher_arguments[:as] = options.delete(:as)
|
130
|
-
else
|
131
|
-
raise ArgumentError, 'You gave me :collection as option but have not give me :as as well'
|
132
|
-
end
|
133
|
-
|
134
|
-
args << "*#{collection}"
|
135
|
-
get_options = "#{collection}.extract_options!"
|
136
|
-
set_collection = "@#{collection} = #{collection}"
|
137
|
-
else
|
138
|
-
args << 'options={}'
|
139
|
-
get_options = 'options'
|
140
|
-
set_collection = ''
|
141
|
-
end
|
142
|
-
|
143
|
-
if block = options.delete(:block)
|
144
|
-
block = :block unless block.is_a?(Symbol)
|
145
|
-
@matcher_arguments[:block] = block
|
146
|
-
end
|
147
|
-
|
148
|
-
# Blocks are always appended. If they have arity 1, they are used for
|
149
|
-
# macro configuration, otherwise, they are stored in the :block variable.
|
150
|
-
#
|
151
|
-
args << "&block"
|
152
|
-
|
153
|
-
assignments = names.map do |name|
|
154
|
-
"@#{name} = #{name}"
|
155
|
-
end.join("\n ")
|
156
|
-
|
157
|
-
class_eval <<-END, __FILE__, __LINE__
|
158
|
-
def initialize(#{args.join(',')})
|
159
|
-
_builder, block = block, nil if block && block.arity == 1
|
160
|
-
#{assignments}
|
161
|
-
#{"@#{block} = block" if block}
|
162
|
-
@options = default_options.merge(#{get_options})
|
163
|
-
#{set_collection}
|
164
|
-
run_after_initialize_callbacks
|
165
|
-
_builder.call(self) if _builder
|
166
|
-
end
|
167
|
-
END
|
168
|
-
end
|
169
|
-
|
170
|
-
# Declare the assertions that are runned for each element in the collection.
|
171
|
-
# It must be used with <tt>arguments</tt> methods in order to work properly.
|
172
|
-
#
|
173
|
-
# == Examples
|
174
|
-
#
|
175
|
-
# The example given in <tt>assertions</tt> can be transformed to
|
176
|
-
# accept a collection just doing:
|
177
|
-
#
|
178
|
-
# class IncludedMatcher < Remarkable::Base
|
179
|
-
# arguments :collection => :values, :as => :value
|
180
|
-
# collection_assertion :is_included?
|
181
|
-
#
|
182
|
-
# protected
|
183
|
-
# def is_included?
|
184
|
-
# @subject.include?(@value)
|
185
|
-
# end
|
186
|
-
# end
|
187
|
-
#
|
188
|
-
# All further consideration done in <tt>assertions</tt> are also valid here.
|
189
|
-
#
|
190
|
-
def collection_assertions(*methods, &block)
|
191
|
-
define_method methods.last, &block if block_given?
|
192
|
-
@matcher_collection_assertions += methods
|
193
|
-
end
|
194
|
-
alias :collection_assertion :collection_assertions
|
195
|
-
|
196
|
-
# Declares the assertions that are run once per matcher.
|
197
|
-
#
|
198
|
-
# == Examples
|
199
|
-
#
|
200
|
-
# A matcher that checks if an element is included in an array can be done
|
201
|
-
# just with:
|
202
|
-
#
|
203
|
-
# class IncludedMatcher < Remarkable::Base
|
204
|
-
# arguments :value
|
205
|
-
# assertion :is_included?
|
206
|
-
#
|
207
|
-
# protected
|
208
|
-
# def is_included?
|
209
|
-
# @subject.include?(@value)
|
210
|
-
# end
|
211
|
-
# end
|
212
|
-
#
|
213
|
-
# Whenever the matcher is called, the :is_included? action is automatically
|
214
|
-
# triggered. Each assertion must return true or false. In case it's false
|
215
|
-
# it will seach for an expectation message on the I18n file. In this
|
216
|
-
# case, the error message would be on:
|
217
|
-
#
|
218
|
-
# included:
|
219
|
-
# description: "check {{value}} is included in the array"
|
220
|
-
# expectations:
|
221
|
-
# is_included: "{{value}} is included in the array"
|
222
|
-
#
|
223
|
-
# In case of failure, it will output:
|
224
|
-
#
|
225
|
-
# "Expected {{value}} is included in the array"
|
226
|
-
#
|
227
|
-
# Notice that on the yml file the question mark is removed for readability.
|
228
|
-
#
|
229
|
-
# == Shortcut declaration
|
230
|
-
#
|
231
|
-
# You can shortcut declaration by giving a name and block to assertion
|
232
|
-
# method:
|
233
|
-
#
|
234
|
-
# class IncludedMatcher < Remarkable::Base
|
235
|
-
# arguments :value
|
236
|
-
#
|
237
|
-
# assertion :is_included? do
|
238
|
-
# @subject.include?(@value)
|
239
|
-
# end
|
240
|
-
# end
|
241
|
-
#
|
242
|
-
def assertions(*methods, &block)
|
243
|
-
if block_given?
|
244
|
-
define_method methods.last, &block
|
245
|
-
protected methods.last
|
246
|
-
end
|
247
|
-
|
248
|
-
@matcher_single_assertions += methods
|
249
|
-
end
|
250
|
-
alias :assertion :assertions
|
251
|
-
|
252
|
-
# Class method that accepts a block or a hash to set matcher's default
|
253
|
-
# options. It's called on matcher initialization and stores the default
|
254
|
-
# value in the @options instance variable.
|
255
|
-
#
|
256
|
-
# == Examples
|
257
|
-
#
|
258
|
-
# default_options do
|
259
|
-
# { :name => @subject.name }
|
260
|
-
# end
|
261
|
-
#
|
262
|
-
# default_options :message => :invalid
|
263
|
-
#
|
264
|
-
def default_options(hash = {}, &block)
|
265
|
-
if block_given?
|
266
|
-
define_method :default_options, &block
|
267
|
-
else
|
268
|
-
class_eval "def default_options; #{hash.inspect}; end"
|
269
|
-
end
|
270
|
-
end
|
271
|
-
end
|
272
|
-
|
273
|
-
# This method is responsable for connecting <tt>arguments</tt>, <tt>assertions</tt>
|
274
|
-
# and <tt>collection_assertions</tt>.
|
275
|
-
#
|
276
|
-
# It's the one that executes the assertions once, executes the collection
|
277
|
-
# assertions for each element in the collection and also responsable to set
|
278
|
-
# the I18n messages.
|
279
|
-
#
|
280
|
-
def matches?(subject)
|
281
|
-
@subject = subject
|
282
|
-
|
1
|
+
module Remarkable
|
2
|
+
module DSL
|
3
|
+
# This module is responsable to create a basic matcher structure using a DSL.
|
4
|
+
#
|
5
|
+
# A matcher that checks if an element is included in an array can be done
|
6
|
+
# just with:
|
7
|
+
#
|
8
|
+
# class IncludedMatcher < Remarkable::Base
|
9
|
+
# arguments :value
|
10
|
+
# assertion :is_included?
|
11
|
+
#
|
12
|
+
# protected
|
13
|
+
# def is_included?
|
14
|
+
# @subject.include?(@value)
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# As you have noticed, the DSL also allows you to remove the messages from
|
19
|
+
# matcher. Since it will look for it on I18n yml file.
|
20
|
+
#
|
21
|
+
# If you want to create a matcher that accepts multile values to be tested,
|
22
|
+
# you just need to do:
|
23
|
+
#
|
24
|
+
# class IncludedMatcher < Remarkable::Base
|
25
|
+
# arguments :collection => :values, :as => :value
|
26
|
+
# collection_assertion :is_included?
|
27
|
+
#
|
28
|
+
# protected
|
29
|
+
# def is_included?
|
30
|
+
# @subject.include?(@value)
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# Notice that the :is_included? logic didn't have to change, because Remarkable
|
35
|
+
# handle this automatically for you.
|
36
|
+
#
|
37
|
+
module Assertions
|
38
|
+
|
39
|
+
def self.included(base) # :nodoc:
|
40
|
+
base.extend ClassMethods
|
41
|
+
end
|
42
|
+
|
43
|
+
module ClassMethods
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
# It sets the arguments your matcher receives on initialization.
|
48
|
+
#
|
49
|
+
# == Options
|
50
|
+
#
|
51
|
+
# * <tt>:collection</tt> - if a collection is expected.
|
52
|
+
# * <tt>:as</tt> - how each item of the collection will be available.
|
53
|
+
# * <tt>:block</tt> - tell the matcher can receive blocks as argument and store
|
54
|
+
# them under the variable given.
|
55
|
+
#
|
56
|
+
# Note: the expected block cannot have arity 1. This is already reserved
|
57
|
+
# for macro configuration.
|
58
|
+
#
|
59
|
+
# == Examples
|
60
|
+
#
|
61
|
+
# Let's see for each example how the arguments declarion reflects on
|
62
|
+
# the matcher API:
|
63
|
+
#
|
64
|
+
# arguments :assign
|
65
|
+
# # Can be called as:
|
66
|
+
# #=> should_assign :task
|
67
|
+
# #=> should_assign :task, :with => Task.new
|
68
|
+
#
|
69
|
+
# This is roughly the same as:
|
70
|
+
#
|
71
|
+
# def initialize(assign, options = {})
|
72
|
+
# @assign = name
|
73
|
+
# @options = options
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# As you noticed, a matcher can always receive options on initialization.
|
77
|
+
# If you have a matcher that accepts only options, for example,
|
78
|
+
# have_default_scope you just need to call <tt>arguments</tt>:
|
79
|
+
#
|
80
|
+
# arguments
|
81
|
+
# # Can be called as:
|
82
|
+
# #=> should_have_default_scope :limit => 10
|
83
|
+
#
|
84
|
+
# arguments :collection => :assigns, :as => :assign
|
85
|
+
# # Can be called as:
|
86
|
+
# #=> should_assign :task1, :task2
|
87
|
+
# #=> should_assign :task1, :task2, :with => Task.new
|
88
|
+
#
|
89
|
+
# arguments :collection => :assigns, :as => :assign, :block => :buildeer
|
90
|
+
# # Can be called as:
|
91
|
+
# #=> should_assign :task1, :task2
|
92
|
+
# #=> should_assign(:task1, :task2){ Task.new }
|
93
|
+
#
|
94
|
+
# The block will be available under the instance variable @builder.
|
95
|
+
#
|
96
|
+
# == I18n
|
97
|
+
#
|
98
|
+
# All the parameters given to arguments are available for interpolation
|
99
|
+
# in I18n. So if you have the following declarion:
|
100
|
+
#
|
101
|
+
# class InRange < Remarkable::Base
|
102
|
+
# arguments :range, :collection => :names, :as => :name
|
103
|
+
#
|
104
|
+
# You will have {{range}}, {{names}} and {{name}} available for I18n
|
105
|
+
# messages:
|
106
|
+
#
|
107
|
+
# in_range:
|
108
|
+
# description: "have {{names}} to be on range {{range}}"
|
109
|
+
#
|
110
|
+
# Before a collection is sent to I18n, it's transformed to a sentence.
|
111
|
+
# So if the following matcher:
|
112
|
+
#
|
113
|
+
# in_range(2..20, :username, :password)
|
114
|
+
#
|
115
|
+
# Has the following description:
|
116
|
+
#
|
117
|
+
# "should have username and password in range 2..20"
|
118
|
+
#
|
119
|
+
def arguments(*names)
|
120
|
+
options = names.extract_options!
|
121
|
+
args = names.dup
|
122
|
+
|
123
|
+
@matcher_arguments[:names] = names
|
124
|
+
|
125
|
+
if collection = options.delete(:collection)
|
126
|
+
@matcher_arguments[:collection] = collection
|
127
|
+
|
128
|
+
if options[:as]
|
129
|
+
@matcher_arguments[:as] = options.delete(:as)
|
130
|
+
else
|
131
|
+
raise ArgumentError, 'You gave me :collection as option but have not give me :as as well'
|
132
|
+
end
|
133
|
+
|
134
|
+
args << "*#{collection}"
|
135
|
+
get_options = "#{collection}.extract_options!"
|
136
|
+
set_collection = "@#{collection} = #{collection}"
|
137
|
+
else
|
138
|
+
args << 'options={}'
|
139
|
+
get_options = 'options'
|
140
|
+
set_collection = ''
|
141
|
+
end
|
142
|
+
|
143
|
+
if block = options.delete(:block)
|
144
|
+
block = :block unless block.is_a?(Symbol)
|
145
|
+
@matcher_arguments[:block] = block
|
146
|
+
end
|
147
|
+
|
148
|
+
# Blocks are always appended. If they have arity 1, they are used for
|
149
|
+
# macro configuration, otherwise, they are stored in the :block variable.
|
150
|
+
#
|
151
|
+
args << "&block"
|
152
|
+
|
153
|
+
assignments = names.map do |name|
|
154
|
+
"@#{name} = #{name}"
|
155
|
+
end.join("\n ")
|
156
|
+
|
157
|
+
class_eval <<-END, __FILE__, __LINE__
|
158
|
+
def initialize(#{args.join(',')})
|
159
|
+
_builder, block = block, nil if block && block.arity == 1
|
160
|
+
#{assignments}
|
161
|
+
#{"@#{block} = block" if block}
|
162
|
+
@options = default_options.merge(#{get_options})
|
163
|
+
#{set_collection}
|
164
|
+
run_after_initialize_callbacks
|
165
|
+
_builder.call(self) if _builder
|
166
|
+
end
|
167
|
+
END
|
168
|
+
end
|
169
|
+
|
170
|
+
# Declare the assertions that are runned for each element in the collection.
|
171
|
+
# It must be used with <tt>arguments</tt> methods in order to work properly.
|
172
|
+
#
|
173
|
+
# == Examples
|
174
|
+
#
|
175
|
+
# The example given in <tt>assertions</tt> can be transformed to
|
176
|
+
# accept a collection just doing:
|
177
|
+
#
|
178
|
+
# class IncludedMatcher < Remarkable::Base
|
179
|
+
# arguments :collection => :values, :as => :value
|
180
|
+
# collection_assertion :is_included?
|
181
|
+
#
|
182
|
+
# protected
|
183
|
+
# def is_included?
|
184
|
+
# @subject.include?(@value)
|
185
|
+
# end
|
186
|
+
# end
|
187
|
+
#
|
188
|
+
# All further consideration done in <tt>assertions</tt> are also valid here.
|
189
|
+
#
|
190
|
+
def collection_assertions(*methods, &block)
|
191
|
+
define_method methods.last, &block if block_given?
|
192
|
+
@matcher_collection_assertions += methods
|
193
|
+
end
|
194
|
+
alias :collection_assertion :collection_assertions
|
195
|
+
|
196
|
+
# Declares the assertions that are run once per matcher.
|
197
|
+
#
|
198
|
+
# == Examples
|
199
|
+
#
|
200
|
+
# A matcher that checks if an element is included in an array can be done
|
201
|
+
# just with:
|
202
|
+
#
|
203
|
+
# class IncludedMatcher < Remarkable::Base
|
204
|
+
# arguments :value
|
205
|
+
# assertion :is_included?
|
206
|
+
#
|
207
|
+
# protected
|
208
|
+
# def is_included?
|
209
|
+
# @subject.include?(@value)
|
210
|
+
# end
|
211
|
+
# end
|
212
|
+
#
|
213
|
+
# Whenever the matcher is called, the :is_included? action is automatically
|
214
|
+
# triggered. Each assertion must return true or false. In case it's false
|
215
|
+
# it will seach for an expectation message on the I18n file. In this
|
216
|
+
# case, the error message would be on:
|
217
|
+
#
|
218
|
+
# included:
|
219
|
+
# description: "check {{value}} is included in the array"
|
220
|
+
# expectations:
|
221
|
+
# is_included: "{{value}} is included in the array"
|
222
|
+
#
|
223
|
+
# In case of failure, it will output:
|
224
|
+
#
|
225
|
+
# "Expected {{value}} is included in the array"
|
226
|
+
#
|
227
|
+
# Notice that on the yml file the question mark is removed for readability.
|
228
|
+
#
|
229
|
+
# == Shortcut declaration
|
230
|
+
#
|
231
|
+
# You can shortcut declaration by giving a name and block to assertion
|
232
|
+
# method:
|
233
|
+
#
|
234
|
+
# class IncludedMatcher < Remarkable::Base
|
235
|
+
# arguments :value
|
236
|
+
#
|
237
|
+
# assertion :is_included? do
|
238
|
+
# @subject.include?(@value)
|
239
|
+
# end
|
240
|
+
# end
|
241
|
+
#
|
242
|
+
def assertions(*methods, &block)
|
243
|
+
if block_given?
|
244
|
+
define_method methods.last, &block
|
245
|
+
protected methods.last
|
246
|
+
end
|
247
|
+
|
248
|
+
@matcher_single_assertions += methods
|
249
|
+
end
|
250
|
+
alias :assertion :assertions
|
251
|
+
|
252
|
+
# Class method that accepts a block or a hash to set matcher's default
|
253
|
+
# options. It's called on matcher initialization and stores the default
|
254
|
+
# value in the @options instance variable.
|
255
|
+
#
|
256
|
+
# == Examples
|
257
|
+
#
|
258
|
+
# default_options do
|
259
|
+
# { :name => @subject.name }
|
260
|
+
# end
|
261
|
+
#
|
262
|
+
# default_options :message => :invalid
|
263
|
+
#
|
264
|
+
def default_options(hash = {}, &block)
|
265
|
+
if block_given?
|
266
|
+
define_method :default_options, &block
|
267
|
+
else
|
268
|
+
class_eval "def default_options; #{hash.inspect}; end"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# This method is responsable for connecting <tt>arguments</tt>, <tt>assertions</tt>
|
274
|
+
# and <tt>collection_assertions</tt>.
|
275
|
+
#
|
276
|
+
# It's the one that executes the assertions once, executes the collection
|
277
|
+
# assertions for each element in the collection and also responsable to set
|
278
|
+
# the I18n messages.
|
279
|
+
#
|
280
|
+
def matches?(subject)
|
281
|
+
@subject = subject
|
282
|
+
|
283
283
|
run_before_assert_callbacks
|
284
284
|
|
285
285
|
assertions = self.class.matcher_single_assertions
|
@@ -288,90 +288,90 @@ module Remarkable
|
|
288
288
|
return negative? if positive? == !value
|
289
289
|
end
|
290
290
|
|
291
|
-
matches_collection_assertions?
|
292
|
-
end
|
293
|
-
|
294
|
-
protected
|
295
|
-
|
296
|
-
# You can overwrite this instance method to provide default options on
|
297
|
-
# initialization.
|
298
|
-
#
|
299
|
-
def default_options
|
300
|
-
{}
|
301
|
-
end
|
302
|
-
|
303
|
-
# Overwrites default_i18n_options to provide arguments and optionals
|
304
|
-
# to interpolation options.
|
305
|
-
#
|
306
|
-
# If you still need to provide more other interpolation options, you can
|
307
|
-
# do that in two ways:
|
308
|
-
#
|
309
|
-
# 1. Overwrite interpolation_options:
|
310
|
-
#
|
311
|
-
# def interpolation_options
|
312
|
-
# { :real_value => real_value }
|
313
|
-
# end
|
314
|
-
#
|
315
|
-
# 2. Return a hash from your assertion method:
|
316
|
-
#
|
317
|
-
# def my_assertion
|
318
|
-
# return true if real_value == expected_value
|
319
|
-
# return false, :real_value => real_value
|
320
|
-
# end
|
321
|
-
#
|
322
|
-
# In both cases, :real_value will be available as interpolation option.
|
323
|
-
#
|
324
|
-
def default_i18n_options #:nodoc:
|
325
|
-
i18n_options = {}
|
326
|
-
|
327
|
-
@options.each do |key, value|
|
328
|
-
i18n_options[key] = value.inspect
|
329
|
-
end if @options
|
330
|
-
|
331
|
-
# Also add arguments as interpolation options.
|
332
|
-
self.class.matcher_arguments[:names].each do |name|
|
333
|
-
i18n_options[name] = instance_variable_get("@#{name}").inspect
|
334
|
-
end
|
335
|
-
|
336
|
-
# Add collection interpolation options.
|
337
|
-
i18n_options.update(collection_interpolation)
|
338
|
-
|
339
|
-
# Add default options (highest priority). They should not be overwritten.
|
340
|
-
i18n_options.update(super)
|
341
|
-
end
|
342
|
-
|
343
|
-
# Method responsible to add collection as interpolation.
|
344
|
-
#
|
345
|
-
def collection_interpolation #:nodoc:
|
346
|
-
options = {}
|
347
|
-
|
348
|
-
if collection_name = self.class.matcher_arguments[:collection]
|
349
|
-
collection_name = collection_name.to_sym
|
350
|
-
collection = instance_variable_get("@#{collection_name}")
|
351
|
-
options[collection_name] = array_to_sentence(collection) if collection
|
352
|
-
|
353
|
-
object_name = self.class.matcher_arguments[:as].to_sym
|
354
|
-
object = instance_variable_get("@#{object_name}")
|
355
|
-
options[object_name] = object if object
|
356
|
-
end
|
357
|
-
|
358
|
-
options
|
359
|
-
end
|
360
|
-
|
361
|
-
# Send the assertion methods given and create a expectation message
|
362
|
-
# if any of those methods returns false.
|
363
|
-
#
|
364
|
-
# Since most assertion methods ends with an question mark and it's not
|
365
|
-
# readable in yml files, we remove question and exclation marks at the
|
366
|
-
# end of the method name before translating it. So if you have a method
|
367
|
-
# called is_valid? on I18n yml file we will check for a key :is_valid.
|
368
|
-
#
|
369
|
-
def send_methods_and_generate_message(methods) #:nodoc:
|
370
|
-
methods.each do |method|
|
371
|
-
bool, hash = send(method)
|
372
|
-
|
373
|
-
if positive? == !bool
|
374
|
-
parent_scope = matcher_i18n_scope.split('.')
|
291
|
+
matches_collection_assertions?
|
292
|
+
end
|
293
|
+
|
294
|
+
protected
|
295
|
+
|
296
|
+
# You can overwrite this instance method to provide default options on
|
297
|
+
# initialization.
|
298
|
+
#
|
299
|
+
def default_options
|
300
|
+
{}
|
301
|
+
end
|
302
|
+
|
303
|
+
# Overwrites default_i18n_options to provide arguments and optionals
|
304
|
+
# to interpolation options.
|
305
|
+
#
|
306
|
+
# If you still need to provide more other interpolation options, you can
|
307
|
+
# do that in two ways:
|
308
|
+
#
|
309
|
+
# 1. Overwrite interpolation_options:
|
310
|
+
#
|
311
|
+
# def interpolation_options
|
312
|
+
# { :real_value => real_value }
|
313
|
+
# end
|
314
|
+
#
|
315
|
+
# 2. Return a hash from your assertion method:
|
316
|
+
#
|
317
|
+
# def my_assertion
|
318
|
+
# return true if real_value == expected_value
|
319
|
+
# return false, :real_value => real_value
|
320
|
+
# end
|
321
|
+
#
|
322
|
+
# In both cases, :real_value will be available as interpolation option.
|
323
|
+
#
|
324
|
+
def default_i18n_options #:nodoc:
|
325
|
+
i18n_options = {}
|
326
|
+
|
327
|
+
@options.each do |key, value|
|
328
|
+
i18n_options[key] = value.inspect
|
329
|
+
end if @options
|
330
|
+
|
331
|
+
# Also add arguments as interpolation options.
|
332
|
+
self.class.matcher_arguments[:names].each do |name|
|
333
|
+
i18n_options[name] = instance_variable_get("@#{name}").inspect
|
334
|
+
end
|
335
|
+
|
336
|
+
# Add collection interpolation options.
|
337
|
+
i18n_options.update(collection_interpolation)
|
338
|
+
|
339
|
+
# Add default options (highest priority). They should not be overwritten.
|
340
|
+
i18n_options.update(super)
|
341
|
+
end
|
342
|
+
|
343
|
+
# Method responsible to add collection as interpolation.
|
344
|
+
#
|
345
|
+
def collection_interpolation #:nodoc:
|
346
|
+
options = {}
|
347
|
+
|
348
|
+
if collection_name = self.class.matcher_arguments[:collection]
|
349
|
+
collection_name = collection_name.to_sym
|
350
|
+
collection = instance_variable_get("@#{collection_name}")
|
351
|
+
options[collection_name] = array_to_sentence(collection) if collection
|
352
|
+
|
353
|
+
object_name = self.class.matcher_arguments[:as].to_sym
|
354
|
+
object = instance_variable_get("@#{object_name}")
|
355
|
+
options[object_name] = object if object
|
356
|
+
end
|
357
|
+
|
358
|
+
options
|
359
|
+
end
|
360
|
+
|
361
|
+
# Send the assertion methods given and create a expectation message
|
362
|
+
# if any of those methods returns false.
|
363
|
+
#
|
364
|
+
# Since most assertion methods ends with an question mark and it's not
|
365
|
+
# readable in yml files, we remove question and exclation marks at the
|
366
|
+
# end of the method name before translating it. So if you have a method
|
367
|
+
# called is_valid? on I18n yml file we will check for a key :is_valid.
|
368
|
+
#
|
369
|
+
def send_methods_and_generate_message(methods) #:nodoc:
|
370
|
+
methods.each do |method|
|
371
|
+
bool, hash = send(method)
|
372
|
+
|
373
|
+
if positive? == !bool
|
374
|
+
parent_scope = matcher_i18n_scope.split('.')
|
375
375
|
matcher_name = parent_scope.pop
|
376
376
|
method_name = method.to_s.gsub(/(\?|\!)$/, '')
|
377
377
|
|
@@ -380,16 +380,16 @@ module Remarkable
|
|
380
380
|
lookup << :"#{matcher_name}.expectations.#{method_name}"
|
381
381
|
lookup << :"negative_expectations.#{method_name}" if negative?
|
382
382
|
lookup << :"expectations.#{method_name}"
|
383
|
-
|
384
|
-
hash = { :scope => parent_scope, :default => lookup }.merge(hash || {})
|
385
|
-
@expectation ||= Remarkable.t lookup.shift, default_i18n_options.merge(hash)
|
386
|
-
|
387
|
-
return negative?
|
388
|
-
end
|
389
|
-
end
|
390
|
-
|
391
|
-
return positive?
|
392
|
-
end
|
383
|
+
|
384
|
+
hash = { :scope => parent_scope, :default => lookup }.merge(hash || {})
|
385
|
+
@expectation ||= Remarkable.t lookup.shift, default_i18n_options.merge(hash)
|
386
|
+
|
387
|
+
return negative?
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
return positive?
|
392
|
+
end
|
393
393
|
|
394
394
|
def matches_single_assertions? #:nodoc:
|
395
395
|
assertions = self.class.matcher_single_assertions
|
@@ -401,13 +401,13 @@ module Remarkable
|
|
401
401
|
assertions = self.class.matcher_collection_assertions
|
402
402
|
collection = instance_variable_get("@#{self.class.matcher_arguments[:collection]}") || []
|
403
403
|
|
404
|
-
assert_collection(nil, collection) do |value|
|
405
|
-
instance_variable_set("@#{arguments[:as]}", value)
|
406
|
-
send_methods_and_generate_message(assertions)
|
404
|
+
assert_collection(nil, collection) do |value|
|
405
|
+
instance_variable_set("@#{arguments[:as]}", value)
|
406
|
+
send_methods_and_generate_message(assertions)
|
407
407
|
end
|
408
408
|
end
|
409
|
-
|
410
|
-
|
411
|
-
end
|
412
|
-
end
|
413
|
-
end
|
409
|
+
|
410
|
+
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|