Motore

repository·main·Indexed 42 days ago

https://github.com/cloudwego/motore

An async middleware abstraction for building robust networking clients and servers in Rust. Inspired by the Tower library, Motore leverages AFIT (Async Functions in Traits) and RPITIT (Return Position Impl Trait in Traits) to simplify asynchronous programming via its core Service and Layer traits. It includes a ServiceBuilder for declarative middleware composition and utilities like MapErr and Identity layers. Version 0.4.3.

Tokens
6.9K
Snippets
29
Records
34
Agent score
52%

What's inside Motore

  1. What is the Service trait in Motore?

    main

    The core abstraction in Motore is the Service trait. It is an async middleware abstraction designed to reduce the mental burden of writing asynchronous code by leveraging AFIT (Async Functions in Traits) and RPITIT (Return Position Impl Trait in Traits). This approach avoids the need for manual Boxing of futures, making the code more concise and performant.

    To implement a service, you define how it processes a request given a context (Cx) and returns a Result containing either a Response or an Error asynchronously.

    pub trait Service<Cx, Request> {
        /// Responses given by the service.
        type Response;
        /// Errors produced by the service.
        type Error;
    
        /// Process the request and return the response asynchronously.
        async fn call(&self, cx: &mut Cx, req: Request) -> Result<Self::Response, Self::Error>;
    }
  2. What is `LayerFn`?

    main

    LayerFn is a wrapper around a closure that implements the Layer trait. It serves as a lightweight utility to avoid the boilerplate of creating a custom struct every time you want to implement a Layer.

    It is created via the layer_fn constructor. Its primary purpose is to take an existing Service and return a new Service (the Out type in the Layer implementation) by applying the closure logic.

  3. Compose middleware using ServiceBuilder

    main

    The ServiceBuilder provides a declarative interface for composing multiple middleware layers to be applied to a Service. You can stack layers using .layer(), .option_layer(), or .layer_fn(), and then apply the resulting stack to a service using .service() or .service_fn().

    Layers are applied in the order they are added, wrapping the inner service. The final service is produced by calling .service(inner_service) at the end of the builder chain.

    use motore::ServiceBuilder;
    use std::time::Duration;
    
    // Assuming 'inner_service' is an existing Service
    let middleware_stack = ServiceBuilder::new()
        .timeout(Some(Duration::from_secs(5)))
        .layer_fn(|svc| {
            // custom logic to wrap service
            svc
        })
        .service(inner_service);
  4. How Service and Layer work together in Motore

    main

    Motore is built around two core abstractions designed for building composable networking clients and servers:

    1. Service trait: Represents an asynchronous function that takes a request and returns either a response or an error. It can model both clients and servers.
    2. Layer trait: Used to compose middleware with Services. A Layer is a function that takes a Service of one type and returns a Service of a different type (e.g., adding a timeout or rate limiting).

    Middleware (like timeouts, rate limiting, or load balancing) is implemented as a Service that wraps an inner service, applying additional behavior before or after the inner service is called. This allows for protocol-agnostic, composable components.

  5. Use `async fn` in traits instead of the `#[service]` macro

    main

    The #[service] procedural macro is deprecated. Previously, it was used to simplify the implementation of the Service trait by handling async transformations and type aliases.

    Because async fn in traits is now stable in Rust, you should no longer use the #[service] macro. Instead, implement the call method directly using async fn within your Service implementation. This achieves the same result without the need for the macro.

    use motore::{service, Service};
    
    // DEPRECATED: Do not use #[service]
    // #[service]
    // impl<Cx, Req, I> Service<Cx, Req> for S<I> {
    //     async fn call(&self, cx: &mut Cx, req: Req) -> Result<I::Response, I::Error> {
    //         self.inner.call(cx, req).await
    //     }
    // }
    
    // RECOMMENDED: Use native async fn in trait
    impl<Cx, Req, I> Service<Cx, Req> for S<I>
    where
        Req: Send + 'static,
        I: Send + 'static + Service<Cx, Req> + Sync,
        Cx: Send + 'static,
    {
        async fn call(&self, cx: &mut Cx, req: Req) -> Result<I::Response, I::Error> {
            self.inner.call(cx, req).await
        }
    }
  6. How to implement an async Service middleware

    main

    You can implement the Service trait to create middleware (like a timeout layer). Because Motore uses AFIT, you can define the async fn call directly within the implementation block without needing to return a pinned, boxed future manually.

    In the example below, a Timeout struct wraps an inner service S and uses tokio::select! to race the inner service call against a sleep timer.

    pub struct Timeout<S> {
        inner: S,
        duration: Duration,
    }
    
    impl<Cx, Req, S> Service<Cx, Req> for Timeout<S>
    where
        Req: 'static + Send,
        S: Service<Cx, Req> + 'static + Send + Sync,
        Cx: 'static + Send,
        S::Error: Send + Sync + Into<BoxError>,
    {
        type Response = S::Response;
    
        type Error = BoxError;
    
        async fn call(&self, cx: &mut Cx, req: Req) -> Result<Self::Response, Self::Error> {
            let sleep = tokio::time::sleep(self.duration);
            tokio::select! {
                r = self.inner.call(cx, req) => {
                    r.map_err(Into::into)
                },
                _ = sleep => Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "service time out").into()),
            }
        }
    }
  7. Convert ServiceBuilder into a Service

    main

    To finalize the builder and produce a concrete service, use one of the following:

    1. .service(S): Wraps an existing service S with the composed layers.
    2. .service_fn(F): Wraps an async function F (converted to a ServiceFn) with the composed layers.
    // Using an existing service
    let service = ServiceBuilder::new()
        .timeout(None)
        .service(inner_service);
    
    // Using an async function
    let service = ServiceBuilder::new()
        .service_fn(|req| async move {
            // handle request
            Ok(Response::new())
        });
  8. Use ServiceExt to transform service errors and responses

    main

    The ServiceExt trait provides convenient adapter methods for any type implementing the Service trait. It allows you to transform the output of a service using functional mapping, similar to standard Result methods.

    • map_err: Transforms the service's Error type into a different type using a provided closure. Use this to unify error types across different services.
    • map_response: Transforms the service's Response type into a different type. Use this to chain computations or reshape data immediately after a service resolves.
    // Example usage pattern
    let transformed_service = original_service
        .map_err(|e| MyCustomError::from(e))
        .map_response(|res| MyCustomResponse::from(res));
  9. Create a Layer using `layer_fn`

    main

    The layer_fn helper allows you to implement the Layer trait using a simple function or closure instead of defining a new struct. This is useful for creating middleware that wraps a Service with additional logic (like logging, authentication, or metrics).

    When you call layer_fn(f), it returns a LayerFn which, when its .layer(inner) method is called, executes the provided function f with the inner service as an argument.

    use motore::layer::{Layer, layer_fn};
    use motore::service::{service_fn, Service, ServiceFn};
    use std::convert::Infallible;
    use std::fmt;
    
    // 1. Define your middleware service
    pub struct LogService<S> {
        target: &'static str,
        service: S,
    }
    
    impl<S, Cx, Request> Service<Cx, Request> for LogService<S>
    where
        S: Service<Cx, Request> + Send + Sync,
        Request: fmt::Debug + Send,
        Cx: Send,
    {
        type Response = S::Response;
        type Error = S::Error;
    
        async fn call(&self, cx: &mut Cx, req: Request) -> Result<Self::Response, Self::Error> {
            println!("req = {:?}, target = {:?}", req, self.target);
            self.service.call(cx, req).await
        }
    }
    
    // 2. Create the layer using layer_fn
    let log_layer = layer_fn(|service| LogService {
        service,
        target: "motore-docs",
    });
    
    // 3. Apply the layer to a service
    #[derive(Debug)]
    pub struct MotoreContext;
    async fn handle(cx: &mut MotoreContext, req: String) -> Result<String, Infallible> {
        Ok::<_, Infallible>(req.to_uppercase())
    }
    
    let mut uppercase_service = service_fn(handle);
    let wrapped_service = log_layer.layer(uppercase_service);
  10. Convert services to trait objects with BoxService

    main

    Use BoxService<Cx, T, U, E> to turn a concrete Service into a trait object. This is useful when you need to:

    1. Use a dynamic response type.
    2. Store different service implementations in the same collection.
    3. Erase the specific type of the service implementation.

    BoxService::new(s) creates a new boxed service. If the service_send feature is enabled, the contained service must be Send + Sync + 'static.

    // Example usage pattern
    let boxed_service = BoxService::new(my_concrete_service);