Skip to content
This repository has been archived by the owner on Sep 13, 2018. It is now read-only.

Just Middleware. #21

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 12 additions & 60 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ use std::io;
use std::rc::Rc;
use std::sync::Arc;

mod middleware;

pub use self::middleware::{Middleware, MiddlewareChain};

/// An asynchronous function from `Request` to a `Response`.
///
/// The `Service` trait is a simplified interface making it easy to write
Expand Down Expand Up @@ -79,66 +83,6 @@ use std::sync::Arc;
/// println!("Redis response: {:?}", await(resp));
/// ```
///
/// # Middleware
///
/// More often than not, all the pieces needed for writing robust, scalable
/// network applications are the same no matter the underlying protocol. By
/// unifying the API for both clients and servers in a protocol agnostic way,
/// it is possible to write middleware that provide these pieces in a
/// reusable way.
///
/// For example, take timeouts as an example:
///
/// ```rust,ignore
/// use tokio::Service;
/// use futures::Future;
/// use std::time::Duration;
///
/// // Not yet implemented, but soon :)
/// use tokio::timer::{Timer, Expired};
///
/// pub struct Timeout<T> {
/// upstream: T,
/// delay: Duration,
/// timer: Timer,
/// }
///
/// impl<T> Timeout<T> {
/// pub fn new(upstream: T, delay: Duration) -> Timeout<T> {
/// Timeout {
/// upstream: upstream,
/// delay: delay,
/// timer: Timer::default(),
/// }
/// }
/// }
///
/// impl<T> Service for Timeout<T>
/// where T: Service,
/// T::Error: From<Expired>,
/// {
/// type Request = T::Request;
/// type Response = T::Response;
/// type Error = T::Error;
/// type Future = Box<Future<Item = Self::Response, Error = Self::Error>>;
///
/// fn call(&self, req: Self::Req) -> Self::Future {
/// let timeout = self.timer.timeout(self.delay)
/// .and_then(|timeout| Err(Self::Error::from(timeout)));
///
/// self.upstream.call(req)
/// .select(timeout)
/// .map(|(v, _)| v)
/// .map_err(|(e, _)| e)
/// .boxed()
/// }
/// }
///
/// ```
///
/// The above timeout implementation is decoupled from the underlying protocol
/// and is also decoupled from client or server concerns. In other words, the
/// same timeout middleware could be used in either a client or a server.
pub trait Service {

/// Requests handled by the service.
Expand All @@ -155,6 +99,14 @@ pub trait Service {

/// Process the request and return the response asynchronously.
fn call(&self, req: Self::Request) -> Self::Future;

/// Wrap this Service in a Middleware component.
fn wrap<M>(self, middleware: M) -> M::WrappedService where
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically speaking the middleware wraps the service, but the service doesn't wrap the middleware, so I guess that the method should be named wrapped_by(). This is slightly confusing, because the middleware also has a method named wrap().

I don't have a perfect alternative name, but it might here are some ideas: apply() , filter() , using() , with() wrapped()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm OK w/ the name wrap here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think wrap works in both directions, in the sense of wrap in {middleware} and wrap around {service}. But I'm also fine with whatever name.

M: Middleware<Self>,
Self: Sized,
{
middleware.wrap(self)
}
}

/// Creates new `Service` values.
Expand Down
138 changes: 138 additions & 0 deletions src/middleware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use std::marker::PhantomData;

use Service;

/// Often, many of the pieces needed for writing network applications
/// can be reused across multiple services. The `Middleware` trait can
/// be used to write reusable components that can be applied to very
/// different kinds of services; for example, it can be applied to
/// services operating on different protocols, and to both the client
/// and server side of a network transaction.
///
/// # Timeouts
///
/// Take timeouts as an example:
///
/// ```rust,ignore
/// use tokio::Service;
/// use tokio::Middleware;
/// use futures::Future;
/// use std::time::Duration;
///
/// // Not yet implemented, but soon :)
/// use tokio::timer::{Timer, Expired};
///
///
/// pub struct Timeout {
/// delay: Duration,
/// timer: Timer,
/// }
///
/// impl Timeout {
/// fn timeout(&self) -> impl Future<Item = (), Error = Expired> {
/// self.timer.timeout(self.delay)
/// }
/// }
///
/// impl<S> Middleware<S> for Timeout
/// where S: Service,
/// S::Error: From<Expired>,
/// {
/// type WrappedService = TimeoutService<S>;
///
/// fn wrap(self, upstream: S) -> TimeoutService<S> {
/// TimeoutService { timeout: self, upstream }
/// }
/// }
///
///
/// // This service implements the Timeout behavior.
/// pub struct TimeoutService<S> {
/// upstream: S,
/// timeout: Timeout,
/// }
///
/// impl<S> Service for TimeoutService<S>
/// where S: Service,
/// S::Error: From<Expired>,
/// {
/// type Request = S::Request;
/// type Response = S::Response;
/// type Error = S::Error;
/// type Future = Box<Future<Item = Self::Response, Error = Self::Error>>;
///
/// fn call(&self, req: Self::Req) -> Self::Future {
/// let timeout = self.timeout.timeout()
/// .and_then(|timeout| Err(Self::Error::from(timeout)));
///
/// self.upstream.call(req)
/// .select(timeout)
/// .map(|(v, _)| v)
/// .map_err(|(e, _)| e)
/// .boxed()
/// }
/// }
///
/// ```
///
/// The above timeout implementation is decoupled from the underlying protocol
/// and is also decoupled from client or server concerns. In other words, the
/// same timeout middleware could be used in either a client or a server.
pub trait Middleware<S: Service> {
/// The service produced by wrapping this middleware around another
/// service.
type WrappedService: Service;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to bike shed, I think it could be nice to come up w/ a shorter name for this associated type... I would say Upstream, but up and down seem to get everyone confused...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just call it Wrapped?


/// Wrap the middlware around a Service it is able to wrap.
///
/// This produces a service of the `WrappedService` associated
/// type, which itself is another service that could possibly be
/// wrapped in other middleware.
fn wrap(self, service: S) -> Self::WrappedService;

/// Chain two middleware together. The lefthand side of this
/// operation is the "inner" middleware and the righthand side is
/// the "outer" middleware.
///
/// When wrapping a middleware chain around a service, first the
/// inner middleware is wrapped around that service, and then the
/// outer middleware is wrapped around the service produced by the
/// inner middleware.
///
/// This allows you to build middleware chains before knowing
/// exactly which service that chain applies to.
fn chain<M>(self, middleware: M) -> MiddlewareChain<S, Self, M> where
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative you could use a builder to create MiddlewareChain. That would allow MiddlewareChain to live in a separate crate.

let middleware = MiddlewareChainBuilder::new(middleware1)
    .chain(middleware2)
    .chain(middleware3);

let service = Myservice::new().wrap(middleware);

I wonder how often MiddlewareChain will actually be used. It might be easier to write a function that applies the necessary middleware to the service or to wrap each service independently. If MiddlewareChain to lives in a separate crate, then it is easier to experiment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of like the idea of keeping the "chain" concept off the Middleware trait... though I don't know if it should be in a separate crate or not.

Also, I think finagle calls this concept a "stack", but I am not super adept at reading scala...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I have use for chain in the framework I'm writing. HTTP services are constructed through a DSL, and then users can in that DSL declare a middleware to be applied to them. It can be easier for them if they can chain middleware together to produce a the middleware they're going to be applying. They never have access to the Service type directly so they can't just use wrap.

M: Middleware<Self::WrappedService>,
Self: Sized,
{
MiddlewareChain {
inner_middleware: self,
outer_middleware: middleware,
_marker: PhantomData,
}
}
}

/// Two middleware, chained together. This type is produced by the
/// `chain` method on the Middleware trait.
pub struct MiddlewareChain<S, InnerM, OuterM> where
S: Service,
InnerM: Middleware<S>,
OuterM: Middleware<InnerM::WrappedService>,
{
inner_middleware: InnerM,
outer_middleware: OuterM,
_marker: PhantomData<S>,
}

impl<S, InnerM, OuterM> Middleware<S> for MiddlewareChain<S, InnerM, OuterM> where
S: Service,
InnerM: Middleware<S>,
OuterM: Middleware<InnerM::WrappedService>,
{
type WrappedService = OuterM::WrappedService;

fn wrap(self, service: S) -> Self::WrappedService {
service.wrap(self.inner_middleware).wrap(self.outer_middleware)
}
}