remarkable 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,2 @@
1
+ Remarkable
2
+ ==========
@@ -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