Iâve found a use case that badly wants this revised algorithm. It seems like a generally applicable pattern, so I thought Iâd post it.
Problem statement:
Suppose youâve got a middleware system, similar to iron. Some middleware just pass through the Request they receive to the next step in the chain, whereas others may construct additional metadata and attach it to the Request. For example, a timeout middleware wonât modify the request, whereas a cookie parsing middleware will.
Furthermore, some middleware will require that one of these mutative middleware has already been passed through. For example, an auth middleware might require that the cookies have been parsed.
Given these 3 middleware, (timeout, cookies, auth), there are multiple orderings that should be well typed:
- Timeout -> Cookies -> Auth
- Cookies -> Timeout -> Auth
- Cookies -> Auth -> Timeout
The important thing is that auth come after cookies. Any other ordering should not be well typed.
Iron solves this problem using a dynamically dispatched TypeMap, but that answer is so unsatisfying! First of all, it should be a compile time error to run the auth middleware without running the cookies middleware first. But moreover, the whole problem of e.g. grabbing the cookies from the metadata should be resolvable statically if the order of middleware is statically known.
Solution:
The basic idea is to have a matryushka nesting doll pattern of metadata types, which are built up as middleware are run. Each is wrapping expected to Deref
to its inner middleware.
For example:
struct Request<T> {
...,
metadata: T,
}
// The initial form of a request is Request<NoMetadata>;
struct NoMetadata;
// The cookies metadata will instead pass the next layer
// a Request<CookiesMetadata<NoMetadata>>
struct CookiesMetadata<T> {
cookies: Cookies,
inner: T,
}
impl<T> Deref for CookiesMetadata<T> {
type Target = T;
...
}
The problem this presents is how the Auth metadata will access the cookies. Trivially, this could be implementing AsRef<Cookies>
for CookiesMetadata<T>
, but then if another layer wraps CookiesMetadata<T>
in some other type, access to the cookies is lost.
The solution to this is to use specialization to ârecursivelyâ define this trait:
trait HasCookies {
fn cookies(&self) -> &Cookies;
}
// "Base case"
impl<T> HasCookies for CookiesMetadata<T> {
...
}
// "Recursive case"
impl<T> HasCookies for T
where
T: Deref,
T::Target: HasCookies
{
...
}
The problem is that these impls do not form an order according to the current specialization rules unless the T
in the CookiesMetadata<T>
impl is bound HasCookies
. But of course the whole point of CookiesMetadata is that T
doesnât matter, because this is the layer that contains the cookies.
However, this change to the rules would solve this problem neatly. CookiesMetadata<T>
would be more speicalized than T: Deref where T::Target: HasCookies
.
Having said that I am now urgently enthusiastic for this change! Do we think it needs a whole RFC or could someone implement it through the tracking issue?