remarkable 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,142 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module DSL
|
3
|
+
module Optionals
|
4
|
+
|
5
|
+
OPTIONAL_KEYS = [ :positive, :negative, :not_given ]
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
# Creates optional handlers for matchers dynamically. The following
|
16
|
+
# statement:
|
17
|
+
#
|
18
|
+
# optional :range, :default => 0..10
|
19
|
+
#
|
20
|
+
# Will generate:
|
21
|
+
#
|
22
|
+
# def range(value=0..10)
|
23
|
+
# @options ||= {}
|
24
|
+
# @options[:range] = value
|
25
|
+
# self
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# Options:
|
29
|
+
#
|
30
|
+
# * <tt>:default</tt> - The default value for this optional
|
31
|
+
# * <tt>:alias</tt> - An alias for this optional
|
32
|
+
# * <tt>:splat</tt> - Should be true if you expects multiple arguments
|
33
|
+
#
|
34
|
+
# Examples:
|
35
|
+
#
|
36
|
+
# optional :name, :title
|
37
|
+
# optional :range, :default => 0..10, :alias => :within
|
38
|
+
#
|
39
|
+
# Optionals will be included in description messages if you assign them
|
40
|
+
# properly on your locale file. If you have a validate_uniqueness_of
|
41
|
+
# matcher with the following on your locale file:
|
42
|
+
#
|
43
|
+
# description: validate uniqueness of {{attributes}}
|
44
|
+
# optionals:
|
45
|
+
# scope:
|
46
|
+
# positive: scoped to {{value}}
|
47
|
+
# case_sensitive:
|
48
|
+
# positive: case sensitive
|
49
|
+
# negative: case insensitive
|
50
|
+
#
|
51
|
+
# When invoked like below will generate the following messages:
|
52
|
+
#
|
53
|
+
# validate_uniqueness_of :project_id, :scope => :company_id
|
54
|
+
# #=> "validate uniqueness of project_id scoped to company_id"
|
55
|
+
#
|
56
|
+
# validate_uniqueness_of :project_id, :scope => :company_id, :case_sensitive => true
|
57
|
+
# #=> "validate uniqueness of project_id scoped to company_id and case sensitive"
|
58
|
+
#
|
59
|
+
# validate_uniqueness_of :project_id, :scope => :company_id, :case_sensitive => false
|
60
|
+
# #=> "validate uniqueness of project_id scoped to company_id and case insensitive"
|
61
|
+
#
|
62
|
+
# The interpolation options available are "value" and "inspect". Where
|
63
|
+
# the first is the optional value transformed into a string and the
|
64
|
+
# second is the inspected value.
|
65
|
+
#
|
66
|
+
# Three keys are available to be used in I18n files and control how
|
67
|
+
# optionals are appended to your description:
|
68
|
+
#
|
69
|
+
# * <tt>positive</tt> - When the optional is given and it evaluates to true (everything but false and nil).
|
70
|
+
# * <tt>negative</tt> - When the optional is given and it evaluates to false (false or nil).
|
71
|
+
# * <tt>not_given</tt> - When the optional is not given.
|
72
|
+
#
|
73
|
+
def optionals(*names)
|
74
|
+
options = names.extract_options!
|
75
|
+
@matcher_optionals += names
|
76
|
+
|
77
|
+
splat = options[:splat] ? '*' : ''
|
78
|
+
default = options[:default] ? "=#{options[:default].inspect}" : ""
|
79
|
+
|
80
|
+
names.each do |name|
|
81
|
+
class_eval <<-END, __FILE__, __LINE__
|
82
|
+
def #{name}(#{splat}value#{default})
|
83
|
+
@options ||= {}
|
84
|
+
@options[:#{name}] = value
|
85
|
+
self
|
86
|
+
end
|
87
|
+
END
|
88
|
+
end
|
89
|
+
class_eval "alias_method(:#{options[:alias]}, :#{names.last})" if options[:alias]
|
90
|
+
|
91
|
+
# Call unique to avoid duplicate optionals.
|
92
|
+
@matcher_optionals.uniq!
|
93
|
+
end
|
94
|
+
alias :optional :optionals
|
95
|
+
|
96
|
+
# Instead of appending, prepend optionals to the beginning of optionals
|
97
|
+
# array. This is important because this decide how the description
|
98
|
+
# message is generated.
|
99
|
+
#
|
100
|
+
def prepend_optionals(*names)
|
101
|
+
current_optionals = @matcher_optionals.dup
|
102
|
+
@matcher_optionals = []
|
103
|
+
optional(*names)
|
104
|
+
@matcher_optionals += current_optionals
|
105
|
+
@matcher_optionals.uniq!
|
106
|
+
end
|
107
|
+
alias :prepend_optional :prepend_optionals
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
# Overwrites description to support optionals. Check <tt>optional</tt> for
|
112
|
+
# more information.
|
113
|
+
#
|
114
|
+
def description(options={})
|
115
|
+
message = super(options)
|
116
|
+
message.strip!
|
117
|
+
|
118
|
+
optionals = self.class.matcher_optionals.map do |optional|
|
119
|
+
scope = matcher_i18n_scope + ".optionals.#{optional}"
|
120
|
+
|
121
|
+
if @options.key?(optional)
|
122
|
+
value = @options[optional]
|
123
|
+
defaults = [ (value ? :positive : :negative) ]
|
124
|
+
|
125
|
+
# If optional is a symbol and it's not any to any of the reserved symbols, search for it also
|
126
|
+
defaults.unshift(value) if value.is_a?(Symbol) && !OPTIONAL_KEYS.include?(value)
|
127
|
+
|
128
|
+
Remarkable.t defaults.shift, :default => defaults, :raise => true, :scope => scope,
|
129
|
+
:inspect => value.inspect, :value => value.to_s
|
130
|
+
else
|
131
|
+
Remarkable.t :not_given, :raise => true, :scope => scope
|
132
|
+
end rescue nil
|
133
|
+
end.compact
|
134
|
+
|
135
|
+
message << ' ' << array_to_sentence(optionals)
|
136
|
+
message.strip!
|
137
|
+
message
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,39 @@
|
|
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', 'matches')
|
5
|
+
require File.join(dir, 'dsl', 'callbacks')
|
6
|
+
|
7
|
+
module Remarkable
|
8
|
+
module DSL
|
9
|
+
ATTR_READERS = [ :matcher_arguments, :matcher_optionals, :matcher_single_assertions,
|
10
|
+
:matcher_collection_assertions, :before_assert_callbacks, :after_initialize_callbacks
|
11
|
+
] unless self.const_defined?(:ATTR_READERS)
|
12
|
+
|
13
|
+
def self.extended(base)
|
14
|
+
# Load modules
|
15
|
+
base.extend Assertions
|
16
|
+
base.send :include, Callbacks
|
17
|
+
base.send :include, Matches
|
18
|
+
base.send :include, Optionals
|
19
|
+
|
20
|
+
# Set the default value for matcher_arguments
|
21
|
+
base.instance_variable_set('@matcher_arguments', { :names => [] })
|
22
|
+
end
|
23
|
+
|
24
|
+
# Make Remarkable::Base DSL inheritable.
|
25
|
+
#
|
26
|
+
def inherited(base)
|
27
|
+
base.class_eval do
|
28
|
+
class << self
|
29
|
+
attr_reader *ATTR_READERS
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
ATTR_READERS.each do |attr|
|
34
|
+
current_value = self.instance_variable_get("@#{attr}")
|
35
|
+
base.instance_variable_set("@#{attr}", current_value ? current_value.dup : [])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# This is a wrapper for I18n default functionality
|
2
|
+
module Remarkable
|
3
|
+
module I18n
|
4
|
+
|
5
|
+
# Add locale files to I18n and to load path, if it exists.
|
6
|
+
def add_locale(*locales)
|
7
|
+
::I18n.backend.load_translations *locales
|
8
|
+
::I18n.load_path += locales if ::I18n.respond_to?(:load_path)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Set Remarkable locale (which is not necessarily the same as the application)
|
12
|
+
def locale=(locale)
|
13
|
+
@@locale = locale
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get Remarkable locale (which is not necessarily the same as the application)
|
17
|
+
def locale
|
18
|
+
@@locale
|
19
|
+
end
|
20
|
+
|
21
|
+
# Wrapper for translation
|
22
|
+
def translate(string, options = {})
|
23
|
+
::I18n.translate string, { :locale => @@locale }.merge(options)
|
24
|
+
end
|
25
|
+
alias :t :translate
|
26
|
+
|
27
|
+
# Wrapper for localization
|
28
|
+
def localize(object, options = {})
|
29
|
+
::I18n.localize object, { :locale => @@locale }.merge(options)
|
30
|
+
end
|
31
|
+
alias :l :localize
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Load I18n
|
37
|
+
RAILS_I18N = Object.const_defined?(:I18n) unless Object.const_defined?(:RAILS_I18N) # Rails >= 2.2
|
38
|
+
|
39
|
+
unless RAILS_I18N
|
40
|
+
begin
|
41
|
+
require 'i18n'
|
42
|
+
rescue LoadError
|
43
|
+
require 'rubygems'
|
44
|
+
# TODO Move to i18n gem as soon as it gets updated
|
45
|
+
gem 'svenfuchs-i18n'
|
46
|
+
require 'i18n'
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set default locale
|
50
|
+
::I18n.default_locale = :en
|
51
|
+
end
|
52
|
+
|
53
|
+
Remarkable.extend Remarkable::I18n
|
54
|
+
Remarkable.locale = :en
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module Macros
|
3
|
+
|
4
|
+
protected
|
5
|
+
|
6
|
+
def method_missing(method_id, *args, &block)
|
7
|
+
if method_id.to_s =~ /^(should_not|should)_(.+)/
|
8
|
+
should_or_should_not_method_missing($1, $2, caller, *args, &block)
|
9
|
+
elsif method_id.to_s =~ /^x(should_not|should)_(.+)/
|
10
|
+
disabled_method_missing($1, $2, *args, &block)
|
11
|
+
else
|
12
|
+
super(method_id, *args, &block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def should_or_should_not_method_missing(should_or_should_not, method, calltrace, *args, &block)
|
17
|
+
it {
|
18
|
+
begin
|
19
|
+
send(should_or_should_not, send(method, *args, &block))
|
20
|
+
rescue Exception => e
|
21
|
+
e.set_backtrace(calltrace.to_a)
|
22
|
+
raise e
|
23
|
+
end
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def disabled_method_missing(should_or_should_not, method, *args, &block)
|
28
|
+
description = get_description_from_matcher(should_or_should_not, method, *args, &block)
|
29
|
+
xexample(description)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Try to get the description from the matcher. If an error is raised, we
|
33
|
+
# deduct the description from the matcher name, but it will be shown in
|
34
|
+
# english.
|
35
|
+
#
|
36
|
+
def get_description_from_matcher(should_or_should_not, method, *args, &block)
|
37
|
+
verb = should_or_should_not.to_s.gsub('_', ' ')
|
38
|
+
|
39
|
+
desc = Remarkable::Matchers.send(method, *args, &block).spec(self).description
|
40
|
+
verb = Remarkable.t("remarkable.core.#{should_or_should_not}", :default => verb)
|
41
|
+
rescue
|
42
|
+
desc = method.to_s.gsub('_', ' ')
|
43
|
+
ensure
|
44
|
+
verb << ' ' << desc
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Remarkable core module
|
2
|
+
module Remarkable
|
3
|
+
# A module that keeps all matchers added. This is useful because it allows
|
4
|
+
# to include matchers in Test::Unit as well.
|
5
|
+
module Matchers; end
|
6
|
+
|
7
|
+
# Helper that includes required Remarkable modules into the given klass.
|
8
|
+
def self.include_matchers!(base, klass)
|
9
|
+
# Add Remarkable macros core module
|
10
|
+
klass.send :extend, Remarkable::Macros
|
11
|
+
|
12
|
+
if defined?(base::Matchers)
|
13
|
+
klass.send :include, base::Matchers
|
14
|
+
|
15
|
+
Remarkable::Matchers.send :extend, base::Matchers
|
16
|
+
Remarkable::Matchers.send :include, base::Matchers
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module Messages
|
3
|
+
|
4
|
+
# Provides a default description message. Overwrite it if needed.
|
5
|
+
# By default it uses default i18n options, but without the subjects, which
|
6
|
+
# usually are not available when description is called.
|
7
|
+
#
|
8
|
+
def description(options={})
|
9
|
+
options = default_i18n_options.merge(options)
|
10
|
+
|
11
|
+
# Remove subject keys
|
12
|
+
options.delete(:subject_name)
|
13
|
+
options.delete(:subject_inspect)
|
14
|
+
|
15
|
+
Remarkable.t 'description', options
|
16
|
+
end
|
17
|
+
|
18
|
+
# Provides a default failure message. Overwrite it if needed.
|
19
|
+
#
|
20
|
+
def failure_message_for_should
|
21
|
+
Remarkable.t 'remarkable.core.failure_message_for_should', :expectation => @expectation
|
22
|
+
end
|
23
|
+
alias :failure_message :failure_message_for_should
|
24
|
+
|
25
|
+
# Provides a default negative failure message. Overwrite it if needed.
|
26
|
+
#
|
27
|
+
def failure_message_for_should_not
|
28
|
+
Remarkable.t 'remarkable.core.failure_message_for_should_not', :expectation => @expectation
|
29
|
+
end
|
30
|
+
alias :negative_failure_message :failure_message_for_should_not
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Returns the matcher scope in I18n.
|
35
|
+
#
|
36
|
+
# If the matcher is Remarkable::ActiveRecord::Matchers::ValidatePresenceOfMatcher
|
37
|
+
# the default scope will be:
|
38
|
+
#
|
39
|
+
# 'remarkable.active_record.validate_presence_of'
|
40
|
+
#
|
41
|
+
def matcher_i18n_scope
|
42
|
+
@matcher_i18n_scope ||= self.class.name.to_s.
|
43
|
+
gsub(/::Matchers::/, '::').
|
44
|
+
gsub(/::/, '.').
|
45
|
+
gsub(/Matcher$/, '').
|
46
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
47
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
48
|
+
tr("-", "_").
|
49
|
+
downcase
|
50
|
+
end
|
51
|
+
|
52
|
+
# Matcher i18n options used in description, failure_message and
|
53
|
+
# negative_failure_message. It provides by default the subject_name and
|
54
|
+
# the subject_inspect value. But when used with DSL, it provides a whole
|
55
|
+
# bunch of options (check dsl/matches.rb for more information).
|
56
|
+
#
|
57
|
+
def default_i18n_options
|
58
|
+
interpolation_options.update(
|
59
|
+
:scope => matcher_i18n_scope,
|
60
|
+
:subject_name => subject_name,
|
61
|
+
:subject_inspect => @subject.inspect
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Method to be overwritten if user wants to provide more interpolation
|
66
|
+
# options to I18n.
|
67
|
+
#
|
68
|
+
def interpolation_options
|
69
|
+
{}
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the not word from I18n API.
|
73
|
+
#
|
74
|
+
def not_word
|
75
|
+
Remarkable.t('remarkable.core.not', :default => 'not') + " "
|
76
|
+
end
|
77
|
+
|
78
|
+
# Converts an array to a sentence
|
79
|
+
#
|
80
|
+
def array_to_sentence(array)
|
81
|
+
words_connector = Remarkable.t 'remarkable.core.helpers.words_connector'
|
82
|
+
two_words_connector = Remarkable.t 'remarkable.core.helpers.two_words_connector'
|
83
|
+
last_word_connector = Remarkable.t 'remarkable.core.helpers.last_word_connector'
|
84
|
+
|
85
|
+
case array.length
|
86
|
+
when 0
|
87
|
+
''
|
88
|
+
when 1
|
89
|
+
array[0].to_s
|
90
|
+
when 2
|
91
|
+
"#{array[0]}#{two_words_connector}#{array[1]}"
|
92
|
+
else
|
93
|
+
"#{array[0...-1].join(words_connector)}#{last_word_connector}#{array[-1]}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Remarkable
|
2
|
+
module Macros
|
3
|
+
|
4
|
+
protected
|
5
|
+
|
6
|
+
def pending(description=nil, &block)
|
7
|
+
PendingSandbox.new(description, self).instance_eval(&block)
|
8
|
+
end
|
9
|
+
|
10
|
+
class PendingSandbox < Struct.new(:description, :spec)
|
11
|
+
include Macros
|
12
|
+
|
13
|
+
def example(mather_description=nil)
|
14
|
+
method_caller = caller.detect{ |c| c !~ /method_missing'/ }
|
15
|
+
|
16
|
+
error = begin
|
17
|
+
::Spec::Example::ExamplePendingError.new(description || 'TODO', method_caller)
|
18
|
+
rescue # For rspec <= 1.1.12
|
19
|
+
::Spec::Example::ExamplePendingError.new(description || 'TODO')
|
20
|
+
end
|
21
|
+
|
22
|
+
spec.send(:example, mather_description){ raise error }
|
23
|
+
end
|
24
|
+
alias :it :example
|
25
|
+
alias :specify :example
|
26
|
+
|
27
|
+
def should_or_should_not_method_missing(should_or_should_not, method, calltrace, *args, &block)
|
28
|
+
example(get_description_from_matcher(should_or_should_not, method, *args, &block))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Hacks into rspec to provide I18n.
|
2
|
+
#
|
3
|
+
module Spec #:nodoc:
|
4
|
+
module Matchers #:nodoc:
|
5
|
+
# Provides I18n on should and should_not.
|
6
|
+
#
|
7
|
+
def self.generated_description
|
8
|
+
return nil if last_should.nil?
|
9
|
+
verb = Remarkable.t "remarkable.core.#{last_should}", :default => last_should.to_s.gsub('_',' ')
|
10
|
+
"#{verb} #{last_description}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Example #:nodoc:
|
15
|
+
module ExampleGroupMethods #:nodoc:
|
16
|
+
# Provides I18n on example disabled message.
|
17
|
+
#
|
18
|
+
def xexample(description=nil, opts={}, &block)
|
19
|
+
disabled = Remarkable.t 'remarkable.core.example_disabled', :default => 'Example disabled'
|
20
|
+
Kernel.warn("#{disabled}: #{description}")
|
21
|
+
end
|
22
|
+
alias_method :xit, :xexample
|
23
|
+
alias_method :xspecify, :xexample
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|