Created
July 18, 2012 08:30
-
-
Save certainty/3135048 to your computer and use it in GitHub Desktop.
interface matcher
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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