module ValidationSpecHelper def accept(*example) ValidationMatcher.new(true, example) end def reject(*example) ValidationMatcher.new(false, example) end class ValidationMatcher def initialize(accept, initial_input) @accept = accept @initial_input = initial_input @fields = [] end # the basic matcher interface def matches?(model) raise ArgumentError.new("Must specify a field to validate") if @fields.empty? raise ArgumentError.new("Number of fields and arguments do not match") if @fields.length != @example.length @model = model matches_validation && matches_errors end def description "#{accept_or_reject} #{describe_fields_and_example}" + with_reason_description end def failure_message(should = true) "expected validation #{should ? "to" : "to not"} #{description}\nbut #{describe_actual_result}" end def negative_failure_message failure_message(false) end # extensions to the matcher def for(*fields) @fields, @example, @syntax = fields, @initial_input, :for self end def of(*example) @fields, @example, @syntax = @initial_input, example, :of self end def with(reason) if @accept raise ArgumentError.new("Must not specify an error condition (via 'with') when the match is expected to accept") end @reason = reason self end private # match helpers def matches_validation @valid ||= run_validation not (@accept ^ @valid) end def run_validation @fields.each_index { |i| @model.send("#{@fields[i]}=".to_sym, @example[i]) } if check_for_any_errors @model.valid? else @model.valid? @model.errors.on(@fields[0].to_sym).nil? end end def check_for_any_errors @fields.length > 1 end def matches_errors return true unless @reason if check_for_any_errors @model.errors.any? {|attr, err| @reason === err } else @model.errors.on(@fields[0]).any? {|err| @reason === err } end end # description helpers def describe_fields_and_example if @syntax == :for "#{describe_example} for #{describe_fields}" else "#{describe_fields} of #{describe_example}" end end def describe_example @example.map { |e| e.inspect }.join(", ") end def describe_fields @fields.map { |f| f.to_s }.join(", ") end def with_reason_description return "" unless @reason " with #{@reason.inspect}" end def describe_actual_result checking_on = check_for_any_errors ? "model" : @fields[0] if @valid "#{checking_on} was valid" else "#{checking_on} had the following errors:\n#{errors_list}" end end def errors_list errors = @model.errors errors = @model.errors.select {|field, msg| field == @fields[0].to_s } unless check_for_any_errors errors.map {|field, msg| "- #{field}: #{msg}" }.sort.join("\n") end def accept_or_reject(accept=@accept) accept ? "accept" : "reject" end end end