layered 0.1.0

Build composable async services with layered middleware.
Documentation

Layered

crate.io docs.rs MSRV CI Coverage License

Layered Services

Build composable async services with layered middleware.

This crate provides the Service trait and a layer system for adding cross-cutting concerns like timeouts, retries, and logging.

Why not Tower?

Tower predates async fn in traits, requiring manual Future types or boxing and poll_ready back-pressure semantics. Tower’s &mut self also requires cloning for concurrent requests. This crate uses async fn with &self, enabling simpler middleware and natural concurrency. Tower interop is available via the tower-service feature.

Quick Start

A Service transforms an input into an output asynchronously:

use layered::Service;

struct Greeter;

impl Service<String> for Greeter {
    type Out = String;

    async fn execute(&self, name: String) -> Self::Out {
        format!("Hello, {name}!")
    }
}

Use Execute to turn any async function into a service:

use layered::{Execute, Service};

let greeter = Execute::new(|name: String| async move {
    format!("Hello, {name}!")
});

assert_eq!(greeter.execute("World".into()).await, "Hello, World!");

Key Concepts

  • Service: An async function In → Out that processes inputs.
  • Middleware: A service that wraps another service to add behavior (logging, timeouts, retries).
  • Layer: A factory that wraps any service with middleware. Stack layers using tuples like (layer1, layer2, service).

Layers and Middleware

A Layer wraps a service with additional behavior:

use layered::{Execute, Layer, Service, Stack};

// A simple logging layer
struct LogLayer;

impl<S> Layer<S> for LogLayer {
    type Service = LogService<S>;

    fn layer(&self, inner: S) -> Self::Service {
        LogService(inner)
    }
}

struct LogService<S>(S);

impl<S, In: Send + std::fmt::Display> Service<In> for LogService<S>
where
    S: Service<In>,
{
    type Out = S::Out;

    async fn execute(&self, input: In) -> Self::Out {
        println!("Input: {input}");
        self.0.execute(input).await
    }
}

// Stack layers with the service (layers apply outer to inner)
let service = (
    LogLayer,
    Execute::new(|x: i32| async move { x * 2 }),
).build();

let result = service.execute(21).await;

Thread Safety

All services must implement Send and Sync, and returned futures must be Send. This ensures compatibility with multi-threaded async runtimes like Tokio.

Features

  • intercept: Enables Intercept middleware
  • dynamic-service: Enables DynamicService for type erasure
  • tower-service: Enables Tower interoperability via the tower module