Initial work on response constraints

This commit is contained in:
Michael Miller 2022-03-05 10:41:39 -07:00
parent de7cd90d11
commit 2adc867843
No known key found for this signature in database
GPG key ID: AC78B32D30CE34A2
7 changed files with 85 additions and 6 deletions

View file

@ -1,7 +1,7 @@
require "../../spec_helper"
Spectator.describe Spectator::Double do
subject(dbl) { Spectator::Double.new("foobar", foo: 42, bar: "baz") }
subject(dbl) { Spectator::Double({foo: Int32, bar: String}).new("foobar", foo: 42, bar: "baz") }
it "responds to defined messages" do
aggregate_failures do

View file

@ -0,0 +1,5 @@
module Spectator
# Untyped arguments to a method call (message).
abstract class AbstractArguments
end
end

View file

@ -0,0 +1,11 @@
module Spectator
# Untyped response to a method call (message).
abstract class AbstractResponse
# Name of the method this response is for.
getter method : Symbol
# Creates the base of the response.
def initialize(@method : Symbol)
end
end
end

View file

@ -1,10 +1,12 @@
require "./abstract_arguments"
module Spectator
# Arguments used in a method call.
#
# Can also be used to match arguments.
# *T* must be a `Tuple` type representing the positional arguments.
# *NT* must be a `NamedTuple` type representing the keyword arguments.
class Arguments(T, NT)
class Arguments(T, NT) < AbstractArguments
# Positional arguments.
getter args : T
@ -20,6 +22,11 @@ module Spectator
new(args, kwargs)
end
# Constructs an instance of empty arguments.
macro empty
{{@type.name(generic_args: false)}}.capture
end
# Constructs a string representation of the arguments.
def to_s(io : IO) : Nil
io << '('

View file

@ -1,3 +1,4 @@
require "./abstract_response"
require "./unexpected_message"
module Spectator
@ -5,10 +6,20 @@ module Spectator
#
# Handles all messages (method calls), but only responds to those configured.
# Methods called that were not configured will raise `UnexpectedMessage`.
class Double(Messages)
# `NT` must be a type of `NamedTuple` that maps method names to their return types.
class Double(NT)
# Stores responses to messages (method calls).
@responses : Array(AbstractResponse)
# Creates a double with pre-configures responses.
# A *name* can be provided, otherwise it is considered an anonymous double.
def initialize(@name : String? = nil, **@messages : **Messages)
def initialize(@responses : Array(AbstractResponse), @name : String? = nil)
end
def initialize(@name : String? = nil, **methods : **NT)
@responses = methods.map do |method, value|
Response.new(method, value).as(AbstractResponse)
end
end
# Utility returning the double's name as a string.
@ -37,7 +48,7 @@ module Spectator
{% if meth.double_splat %}**{{meth.double_splat}}, {% end %}
{% if meth.block_arg %}&{{meth.block_arg}}{% elsif meth.accepts_block? %}&{% end %}
){% if meth.return_type %} : {{meth.return_type}}{% end %}{% if !meth.free_vars.empty? %} forall {{meth.free_vars.splat}}{% end %}
\{% if type = Messages[{{meth.name.symbolize}}] %}
\{% if type = NT[{{meth.name.symbolize}}] %}
{% if meth.return_type %}
\{% if type <= {{meth.return_type}} %}
# Return type appears to match configured type.
@ -70,7 +81,10 @@ module Spectator
# Handle all methods but only respond to configured messages.
# Raises an `UnexpectedMessage` error for non-configures messages.
macro method_missing(call)
\{% if Messages.keys.includes?({{call.name.symbolize}}.id) %}
\{% if NT.keys.includes?({{call.name.symbolize}}.id) %}
# Find a suitable response.
call = MethodCall.capture({{call.name.symbolize}}, {{call.args.splat}})
response = @responses.find &.===(call)
# Return configured response.
@messages[{{call.name.symbolize}}]
\{% else %}

View file

@ -0,0 +1,18 @@
require "./abstract_arguments"
require "./arguments"
module Spectator
class MethodCall
getter method : Symbol
getter arguments : AbstractArguments
def initialize(@method : Symbol, @arguments : Arguments = Arguments.empty)
end
def self.capture(method : Symbol, *args, **kwargs)
arguments = Arguments.new(args, kwargs)
new(method, arguments)
end
end
end

View file

@ -0,0 +1,24 @@
require "./abstract_arguments"
require "./abstract_response"
module Spectator
class Response(T) < AbstractResponse
# Return value.
getter value : T
# Arguments the method must have been called with to provide this response.
# Is nil when there's no constraint - only the method name must match.
getter constraint : AbstractArguments?
# Creates the response.
def initialize(@method : Symbol, @value : T, @constraint : Arguments? = nil)
end
def ===(call : MethodCall)
return false if method != call.method
return true unless constraint = @constraint
constraint === call.arguments
end
end
end