lxy 0.1.1

A convenient async http and RPC framework in Rust
Documentation
//! Request-scoped context storage
//!
//! > Requires the `request-context` feature to be enabled.
//!
//! This module provides a [tower_layer::Layer] with request-scoped context data.
//!
//! The [RequestContext] will be generally available and consistent during the lifetime of a request.
//!
//!
//! # Example
//!
//! ```rust
//! use lxy::context::get_context;
//! use lxy::http::Router;
//! use lxy::Result;
//!
//! let mut router = Router::new();
//!
//! router
//!   .get("/", async || -> Result<&'static str> {
//!     let ctx = get_context()?;
//!     dbg!("Request ID: {:?}", ctx.request_id());
//!     Ok("Hello, World!")
//!   });
//! ```
mod errors;
pub(crate) mod layer;
mod request_context;
mod request_id;

use axum::extract::FromRequestParts;
use std::future::Future;
use tokio::task_local;

use crate::error::{Error, Result, TypedError};

pub use errors::*;
pub use request_context::RequestContext;
pub use request_id::*;

task_local! {
    static REQUEST_CONTEXT: RequestContext;
}

/// Run a future within the given request context.
///
/// Afterwards, all the upcoming calls to [get_context()] will return the provided context.
pub(crate) async fn run_with_context<Fut, R>(context: RequestContext, fut: Fut) -> R
where
  Fut: Future<Output = R>,
{
  REQUEST_CONTEXT.scope(context, fut).await
}

/// Retrieves the current request context.
///
/// # Errors
///
/// Returns [`OutOfScope`] if called outside a request handler.
///
/// # Examples
///
/// ```rust
/// use lxy::context::{get_context};
/// use lxy::Result;
///
/// async fn handler() -> Result<()> {
///     let ctx = get_context()?;
///     println!("Request ID: {:?}", ctx.request_id());
///     Ok(())
/// }
/// ```
pub fn get_context() -> Result<RequestContext> {
  REQUEST_CONTEXT
    .try_with(|ctx| ctx.clone())
    .map_err(|_| OutOfScope::error("get_context() called outside request scope"))
}

impl<S> FromRequestParts<S> for RequestContext
where
  S: Send + Sync,
{
  type Rejection = Error;

  async fn from_request_parts(
    _parts: &mut axum::http::request::Parts,
    _state: &S,
  ) -> Result<Self, Self::Rejection> {
    get_context()
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use http::Method;

  #[tokio::test]
  async fn test_context() {
    let ctx = RequestContext::new();
    ctx.set(Method::GET);

    run_with_context(ctx, async {
      let ctx = get_context().unwrap();
      assert!(ctx.get::<Method>() == Some(Method::GET));
    })
    .await;
  }

  #[tokio::test]
  async fn test_nested_context() {
    let outer_ctx = RequestContext::new();
    outer_ctx.set(Method::GET);

    run_with_context(outer_ctx.clone(), async {
      let ctx = get_context().unwrap();
      assert!(ctx.get::<Method>() == Some(Method::GET));

      let inner_ctx = RequestContext::new();
      inner_ctx.set(Method::POST);

      run_with_context(inner_ctx.clone(), async {
        let ctx = get_context().unwrap();
        assert!(ctx.get::<Method>() == Some(Method::POST));
      })
      .await;

      let ctx = get_context().unwrap();
      assert!(ctx.get::<Method>() == Some(Method::GET));
    })
    .await;
  }

  #[test]
  fn test_get_context_outside() {
    use crate::error::ErrorCode;
    match get_context() {
      Ok(_) => panic!("should return error outside request context"),
      Err(err) => {
        assert!(err.is_code(ErrorCode::Internal));
        assert!(err.is(OutOfScope));
      }
    }
  }
}