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