Skip to content

Instantly share code, notes, and snippets.

@certainty
Created July 18, 2012 08:30
Show Gist options
  • Save certainty/3135048 to your computer and use it in GitHub Desktop.
Save certainty/3135048 to your computer and use it in GitHub Desktop.
interface matcher
class MissmatchRecorder
attr_reader :record
def initialize
@record = ""
end
def <<(msg)
@record << msg
end
def reset!
@record = ""
end
def empty?
@record == ""
end
end
class HashMatcher
attr_reader :recorder
def initialize(spec,recorder,any = false)
@any = any
@spec = spec.symbolize_keys
@recorder = recorder
end
def match?(other)
unless other.is_a?(Hash)
@recorder << "HashMatcher: expected a hash but got #{other.inspect}"
return false
end
return true if @any
unless other.keys.length == @spec.keys.length
@recorder << "HashMatcher: expected hash of length #{@spec.keys.length} but got #{other.inspect}"
return false
end
other_symbolized = other.symbolize_keys
@spec.keys.all? do |key|
unless other_symbolized.key?(key)
@recorder << "HashMatcher: expected hash to contain key '#{key}' but didn't find in #{other_symbolized.inspect}"
false
else
@spec[key].match?(other_symbolized[key.to_sym])
end
end
end
def inspect
@spec.inspect
end
def values
@spec.values
end
end
class ArrayMatcher
attr_reader :constraint
attr_reader :matchers
attr_reader :recorder
def initialize(matchers,constraint,recorder)
@constraint = constraint
@matchers = matchers
@recorder = recorder
end
def match?(other)
unless other.is_a?(Array)
@recorder << "ArrayMatcher: expected an array but got #{other.inspect}"
return false
end
if @constraint == :zero_or_many
return true if other.empty?
other.all?{ |cur| @matchers.match?(cur) }
elsif @constraint == :one_or_many
if other.empty?
@recorder << "ArrayMatcher: expected non-empty array but got #{other.inspect}"
return false
end
other.all?{ |cur| @matchers.match?(cur) }
else
unless @matchers.length == other.length
@recorder << "ArrayMatcher: expected array of length #{@matchers.length} but got #{other.length}"
return false
end
@matchers.each_with_index do |matcher,i|
return false unless matcher.match?(other[i])
end
true
end
end
def inspect
if @constraint == :zero_or_many
[:*,@matchers].inspect
elsif @constraint == :one_or_many
[:+,@matchers].inspect
else
@matchers.inspect
end
end
end
class AtomMatcher
attr_reader :recorder
def initialize(spec,recorder)
@spec = spec
@recorder = recorder
end
def match?(other)
case @spec
when :nil
unless other.nil?
@recorder << "AtomMatcher: expected nil got #{other.inspect}"
false
else
true
end
when :bool
if other.is_a?(TrueClass) || other.is_a?(FalseClass)
true
else
@recorder << "AtomMatcher: expected bool got #{other.inspect}"
false
end
#match allways
when :_any_
true
else
#possibly composed
alternatives = @spec.to_s.split('_or_')
if alternatives.any?{ |a| (a == 'nil' && other.nil?) || (other.class.name == a.downcase.capitalize) }
true
else
@recorder << "AtomMatcher: expected instance of any of #{alternatives.join(',')} got #{other.inspect}"
false
end
end
end
def inspect
@spec.inspect
end
end
class InvalidMatchSpec < RuntimeError
end
class MatcherFactory
def self.matcher(recorder,elt)
if elt.is_a?(Array)
case elt[0]
when :*
if elt[1].nil?
#we don't care about the format
#[:*] is a shorthand for [:*,:_any_]
ArrayMatcher.new(matcher(recorder,:_any_),:zero_or_many,recorder)
else
ArrayMatcher.new(matcher(recorder,elt[1]),:zero_or_many,recorder)
end
when :+
ArrayMatcher.new(matcher(recorder,elt[1]),:one_or_many,recorder)
else #nil or sth else
ArrayMatcher.new(elt.map{ |cur| matcher(recorder,cur) },nil,recorder)
end
elsif elt.is_a?(Hash)
if elt.size == 1 && elt[:_any_] == :_any_
HashMatcher.new({ },recorder,true)
else
HashMatcher.new(elt.inject({ }) { |hsh,var| hsh[var[0]] = matcher(recorder,var[1]); hsh },recorder)
end
elsif elt.is_a?(Symbol)
AtomMatcher.new(elt,recorder)
else
raise InvalidMatchSpec,"Invalid Matchspecifier #{elt.inspect}"
end
end
end
class MatchInterface
def initialize(iface_spec)
@recorder = MissmatchRecorder.new
@matcher = MatcherFactory.matcher(@recorder,iface_spec)
end
def matches?(data)
@receive = data
@matcher.match?(@received)
end
def description
" check that the given value matches the template given"
end
def failure_message
"#{@matcher.recorder.record}"
end
def negative_failure_message
"not(#{@matcher.recorder.record})"
end
end
class StickToInterface
def initialize(iface_spec)
@recorder = MissmatchRecorder.new
@matcher = MatcherFactory.matcher(@recorder,iface_spec)
end
def matches?(resp)
@received = ActiveSupport::JSON.decode(resp.body)
@matcher.match?(@received)
end
def description
" check that the given value matches the template given"
end
def failure_message
"#{@matcher.recorder.record}"
end
def negative_failure_message
"not(#{@matcher.recorder.record})"
end
end
#must respond to body
def stick_to_interface(struct_spec)
StickToInterface.new(struct_spec)
end
#any structure
def match_interface(iface)
MatchInterface.new(iface)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment