lib/remarkable/dsl/optionals.rb in remarkable-3.0.8 vs lib/remarkable/dsl/optionals.rb in remarkable-3.0.9
- old
+ new
@@ -1,7 +1,104 @@
module Remarkable
- module DSL
+ module DSL
+ # This module is responsable for create optional handlers and providing macro
+ # configration blocks. Consider the matcher below:
+ #
+ # class AllowValuesForMatcher < Remarkable::ActiveRecord::Base
+ # arguments :collection => :attributes, :as => :attribute
+ #
+ # optional :message
+ # optional :in, :splat => true
+ # optional :allow_nil, :allow_blank, :default => true
+ # end
+ #
+ # This allow the matcher to be called as:
+ #
+ # it { should allow_values_for(:email).in("jose.valim@gmail.com", "jose@another.com").message(:invalid).allow_nil }
+ #
+ # It also allow macros to be configured with blocks:
+ #
+ # should_allow_values_for :email do |m|
+ # m.message :invalid
+ # m.allow_nil
+ # m.in "jose.valim@gmail.com"
+ # m.in "jose@another.com"
+ # end
+ #
+ # Which could be also writen as:
+ #
+ # should_allow_values_for :email do |m|
+ # m.message = :invalid
+ # m.allow_nil = true
+ # m.in = [ "jose.valim@gmail.com", "jose@another.com" ]
+ # end
+ #
+ # The difference between the them are: 1) optional= always require an argument
+ # even if :default is given. 2) optional= always overwrite all previous values
+ # even if :splat is given.
+ #
+ # Blocks can be also given when :block => true is set:
+ #
+ # should_set_session :user_id do |m|
+ # m.to { @user.id }
+ # end
+ #
+ # == I18n
+ #
+ # Optionals will be included in description messages if you assign them
+ # properly on your locale file. If you have a validate_uniqueness_of
+ # matcher with the following on your locale file:
+ #
+ # description: validate uniqueness of {{attributes}}
+ # optionals:
+ # scope:
+ # positive: scoped to {{inspect}}
+ # case_sensitive:
+ # positive: case sensitive
+ # negative: case insensitive
+ #
+ # When invoked like below will generate the following messages:
+ #
+ # validate_uniqueness_of :project_id, :scope => :company_id
+ # #=> "validate uniqueness of project_id scoped to :company_id"
+ #
+ # validate_uniqueness_of :project_id, :scope => :company_id, :case_sensitive => true
+ # #=> "validate uniqueness of project_id scoped to :company_id and case sensitive"
+ #
+ # validate_uniqueness_of :project_id, :scope => :company_id, :case_sensitive => false
+ # #=> "validate uniqueness of project_id scoped to :company_id and case insensitive"
+ #
+ # == Interpolation options
+ #
+ # The default interpolation options available are "inspect" and "value". Whenever
+ # you use :splat => true, it also adds a new interpolation option called {{sentence}}.
+ #
+ # Given the following matcher call:
+ #
+ # validate_uniqueness_of :id, :scope => [ :company_id, :project_id ]
+ #
+ # The following yml setting and outputs are:
+ #
+ # scope:
+ # positive: scoped to {{inspect}}
+ # # Outputs: "validate uniqueness of project_id scoped to [ :company_id, :project_id ]"
+ #
+ # positive: scoped to {{value}}
+ # # Outputs: "validate uniqueness of project_id scoped to company_idproject_id"
+ #
+ # positive: scoped to {{value}}
+ # # Outputs: "validate uniqueness of project_id scoped to company_id and project_id"
+ #
+ # == Interpolation keys
+ #
+ # Three keys are available to be used in I18n files and control how optionals
+ # are appended to your description:
+ #
+ # * <tt>positive</tt> - When the optional is given and it evaluates to true (everything but false and nil).
+ # * <tt>negative</tt> - When the optional is given and it evaluates to false (false or nil).
+ # * <tt>not_given</tt> - When the optional is not given.
+ #
module Optionals
OPTIONAL_KEYS = [ :positive, :negative, :not_given ]
def self.included(base) #:nodoc:
@@ -10,94 +107,79 @@
module ClassMethods
protected
- # Creates optional handlers for matchers dynamically. The following
- # statement:
+ # Creates optional handlers for matchers dynamically.
#
- # optional :range, :default => 0..10
+ # == Options
#
- # Will generate:
- #
- # def range(value=0..10)
- # @options ||= {}
- # @options[:range] = value
- # self
- # end
- #
- # Options:
- #
# * <tt>:default</tt> - The default value for this optional
# * <tt>:alias</tt> - An alias for this optional
- # * <tt>:splat</tt> - Should be true if you expects multiple arguments
+ # * <tt>:splat</tt> - Should be true if you expects multiple arguments
+ # * <tt>:block</tt> - Tell this optional can also receive blocks
#
- # Examples:
+ # == Examples
#
- # optional :name, :title
- # optional :range, :default => 0..10, :alias => :within
+ # class AllowValuesForMatcher < Remarkable::ActiveRecord::Base
+ # arguments :collection => :attributes, :as => :attribute
#
- # Optionals will be included in description messages if you assign them
- # properly on your locale file. If you have a validate_uniqueness_of
- # matcher with the following on your locale file:
+ # optional :message
+ # optional :in, :splat => true
+ # optional :allow_nil, :allow_blank, :default => true
+ # end
#
- # description: validate uniqueness of {{attributes}}
- # optionals:
- # scope:
- # positive: scoped to {{value}}
- # case_sensitive:
- # positive: case sensitive
- # negative: case insensitive
- #
- # When invoked like below will generate the following messages:
- #
- # validate_uniqueness_of :project_id, :scope => :company_id
- # #=> "validate uniqueness of project_id scoped to company_id"
- #
- # validate_uniqueness_of :project_id, :scope => :company_id, :case_sensitive => true
- # #=> "validate uniqueness of project_id scoped to company_id and case sensitive"
- #
- # validate_uniqueness_of :project_id, :scope => :company_id, :case_sensitive => false
- # #=> "validate uniqueness of project_id scoped to company_id and case insensitive"
- #
- # The interpolation options available are "value" and "inspect". Where
- # the first is the optional value transformed into a string and the
- # second is the inspected value.
- #
- # Three keys are available to be used in I18n files and control how
- # optionals are appended to your description:
- #
- # * <tt>positive</tt> - When the optional is given and it evaluates to true (everything but false and nil).
- # * <tt>negative</tt> - When the optional is given and it evaluates to false (false or nil).
- # * <tt>not_given</tt> - When the optional is not given.
- #
def optionals(*names)
- options = names.extract_options!
- @matcher_optionals += names
+ options = names.extract_options!
- splat = options[:splat] ? '*' : ''
- default = options[:default] ? "=#{options[:default].inspect}" : ""
+ @matcher_optionals += names
+ default = options[:default] ? "=#{options[:default].inspect}" : nil
+
+ block = if options[:block]
+ @matcher_optionals_block += names
+ default ||= "=nil"
+ ', &block'
+ else
+ nil
+ end
+ splat = if options[:splat]
+ @matcher_optionals_splat += names
+ '*'
+ else
+ nil
+ end
+
names.each do |name|
class_eval <<-END, __FILE__, __LINE__
- def #{name}(#{splat}value#{default})
- @options ||= {}
- @options[:#{name}] = value
+ def #{name}(#{splat}value#{default}#{block})
+ @options ||= {}
+ #{"@options[:#{name}] ||= []" if splat}
+ @options[:#{name}] #{:+ if splat}= #{"block ||" if block} value
self
+ end
+ def #{name}=(value)
+ @options ||= {}
+ @options[:#{name}] = value
+ self
end
END
- end
- class_eval "alias_method(:#{options[:alias]}, :#{names.last})" if options[:alias]
+ end
+ class_eval %{
+ alias :#{options[:alias]} :#{names.last}
+ alias :#{options[:alias]}= :#{names.last}=
+ } if options[:alias]
+
# Call unique to avoid duplicate optionals.
@matcher_optionals.uniq!
end
alias :optional :optionals
# Instead of appending, prepend optionals to the beginning of optionals
- # array. This is important because this decide how the description
- # message is generated.
+ # array. This is important because the optionals declaration order
+ # changes how the description message is generated.
#
def prepend_optionals(*names)
current_optionals = @matcher_optionals.dup
@matcher_optionals = []
optional(*names)
@@ -115,17 +197,23 @@
message = super(options)
message.strip!
optionals = self.class.matcher_optionals.map do |optional|
if @options.key?(optional)
- value = @options[optional]
- defaults = [ (value ? :positive : :negative) ]
+ value = @options[optional]
+ defaults = [ (value ? :positive : :negative) ]
# If optional is a symbol and it's not any to any of the reserved symbols, search for it also
defaults.unshift(value) if value.is_a?(Symbol) && !OPTIONAL_KEYS.include?(value)
defaults << ''
options = { :default => defaults, :inspect => value.inspect, :value => value.to_s }
+
+ if self.class.matcher_optionals_splat.include?(optional)
+ value = [ value ] unless Array === value
+ options[:sentence] = array_to_sentence(value, true)
+ end
+
translate_optionals_with_namespace(optional, defaults.shift, options)
else
translate_optionals_with_namespace(optional, :not_given, :default => '')
end
end.compact