Depending on how many assumptions you can or want to make about your services, here is a completely unsophisticated approach, which, however, may be an option. It mainly relies on 1) all known known message types and b) partial functions are partial in only one dimension (more on this later).
We need a finite set of possible message types:
sealed trait Message case class Hello(who: String) extends Message case class Lunch(withWhom: String) extends Message case class Dinner(withWhom: String) extends Message case class Goodbye(who: String) extends Message
And a few examples of services:
val service0: PartialFunction[Any, Unit] = { case Hello(who) => () case Goodbye(who) => () } val service1: PartialFunction[Any, Unit] = { case Hello(who) => () case Lunch(withWhom) => () case Goodbye(who) => () } var services = List(service0, service1)
Further, we also define a list of message instances intended to be executed as drawings for received messages:
val simpleInstances = List(Hello("*"), Lunch("*"), Dinner("*"), Goodbye("*"))
Finally, we define a method that returns accepted arguments from a partial function and a list of possible arguments:
def supportedArguments[F, T, A <: F] (pf: PartialFunction[F, T], args: Iterable[A]) = args filter pf.isDefinedAt
Cute printer:
def printSupportedArguments(services: Iterable[PartialFunction[Any, Unit]], messages: Iterable[Message]) { services.zipWithIndex.foreach {case (s, i) => val supported = supportedArguments(s, messages) println(s"Service $i supports $supported") } }
Release:
printSupportedArguments(services, simpleInstances) // Service 0 supports List(Hello(*), Goodbye(*)) // Service 1 supports List(Hello(*), Lunch(*), Goodbye(*)) // Service 2 supports List(Goodbye(*))
Everything becomes more complicated if the services are not only partial with respect to the type of message they receive, but also partially relate to the content of the message they receive, that is, if they are partial in more than one direction:
val service2: PartialFunction[Any, Unit] = { case Hello("Thomas") => () case Hello("Laura") => () case Goodbye(who) => () }
Such a service also requires adaptation of the list of schema instances:
val moreInstances = simpleInstances ++ List(Hello("Thomas"), Hello("Laura"))
Result:
printSupportedArguments(services :+ service2, moreInstances) // Service 0 supports List(Hello(*), Goodbye(*), Hello(Thomas), Hello(Laura)) // Service 1 supports List(Hello(*), Lunch(*), Goodbye(*), Hello(Thomas), // Hello(Laura)) // Service 2 supports List(Goodbye(*), Hello(Thomas), Hello(Laura))
Many of the drawbacks of this approach obviously include the following:
Message types must be known.
If services are partial in several dimensions, all possible contents of the message should be known.
Using an instance of Hello("*") to indicate arbitrary instances of Hello makes it impossible to distinguish between services that accept Hello(_) and services that literally only accept Hello("*")
If you find a real solution using macros or reflection (or magic), post it here!