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