remarkable 3.0.0

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