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