module Remarkable module DSL module Assertions protected # It sets the arguments your matcher receives on initialization. # # arguments :name, :range # # Which is roughly the same as: # # def initialize(name, range, options = {}) # @name = name # @range = range # @options = options # end # # But most of the time your matchers iterates through a collection, # such as a collection of attributes in the case below: # # @product.should validate_presence_of(:title, :name) # # validate_presence_of is a matcher declared as: # # class ValidatePresenceOfMatcher < Remarkable::Base # arguments :collection => :attributes, :as => :attribute # end # # In this case, Remarkable provides an API that enables you to easily # assert each item of the collection. Let's check more examples: # # should allow_values_for(:email, "jose@valim.com", "carlos@brando.com") # # Is declared as: # # arguments :attribute, :collection => :good_values, :as => :good_value # # And this is the same as: # # class AllowValuesForMatcher < Remarkable::Base # def initialize(attribute, *good_values) # @attribute = attribute # @options = default_options.merge(good_values.extract_options!) # @good_values = good_values # end # end # # Now, the collection is @good_values. In each assertion method we will # have a @good_value variable (in singular) instantiated with the value # to assert. # # Finally, if your matcher deals with blocks, you can also set them as # option: # # arguments :name, :block => :builder # # It will be available under the instance variable @builder. # def arguments(*names) options = names.extract_options! args = names.dup @matcher_arguments[:names] = names if collection = options.delete(:collection) @matcher_arguments[:collection] = collection if options[:as] @matcher_arguments[:as] = options.delete(:as) else raise ArgumentError, 'You gave me :collection as option but have not give me :as as well' end args << "*#{collection}" get_options = "#{collection}.extract_options!" set_collection = "@#{collection} = #{collection}" else args << 'options={}' get_options = 'options' set_collection = '' end if block = options.delete(:block) @matcher_arguments[:block] = block args << "&#{block}" names << block end assignments = names.map do |name| "@#{name} = #{name}" end.join("\n ") class_eval <<-END, __FILE__, __LINE__ def initialize(#{args.join(',')}) #{assignments} @options = default_options.merge(#{get_options}) #{set_collection} run_after_initialize_callbacks end END end # Call it to declare your collection assertions. Every method given will # iterate through the whole collection given in :arguments. # # For example, validate_presence_of can be written as: # # class ValidatePresenceOfMatcher < Remarkable::Base # arguments :collection => :attributes, :as => :attribute # collection_assertions :allow_nil? # # protected # def allow_nil? # # matcher logic # end # end # # Then we call it as: # # should validate_presence_of(:email, :password) # # For each attribute given, it will call the method :allow_nil which # contains the matcher logic. As stated in arguments, those # attributes will be available under the instance variable @argument # and the matcher subject is available under the instance variable # @subject. # # If a block is given, it will create a method with the name given. # So we could write the same class as above just as: # # class ValidatePresenceOfMatcher < Remarkable::Base # arguments :collection => :attributes # # collection_assertion :allow_nil? do # # matcher logic # end # end # # Those methods should return true if it pass or false if it fails. When # it fails, it will use I18n API to find the proper failure message: # # expectations: # allow_nil: allowed the value to be nil # allow_blank: allowed the value to be blank # # Or you can set the message in the instance variable @expectation in the # assertion method if you don't want to rely on I18n API. # # As you might have noticed from the examples above, this method is also # aliased as collection_assertion. # def collection_assertions(*methods, &block) define_method methods.last, &block if block_given? @matcher_collection_assertions += methods end alias :collection_assertion :collection_assertions # In contrast to collection_assertions, the methods given here # are called just once. In other words, it does not iterate through the # collection given in arguments. # # It also accepts blocks and is aliased as assertion. # def assertions(*methods, &block) define_method methods.last, &block if block_given? @matcher_single_assertions += methods end alias :assertion :assertions # Class method that accepts a block or a hash to set matcher's default options. # def default_options(hash = {}, &block) if block_given? define_method :default_options, &block else class_eval "def default_options; #{hash.inspect}; end" end end end end end