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