arcature 2026.1.0

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! `Session` — high-level session ergonomics (A9).
//!
//! Wraps `tower_sessions::Session` with the ergonomic API specified in
//! PROGRAM.md: `put`, `get`, `forget`, `regenerate`. This is a genuine Axum
//! `FromRequestParts` extractor — Axum remains the handler runtime.
//!
//! # Example
//!
//! ```ignore
//! async fn set_theme(session: Session) -> Result<Empty> {
//!     session.put("theme", "dark").await?;
//!     Ok(Empty)
//! }
//!
//! async fn get_theme(session: Session) -> Result<String> {
//!     Ok(session.get::<String>("theme").await?.unwrap_or_else(|| "light".to_string()))
//! }
//! ```

use std::convert::Infallible;

use axum::extract::FromRequestParts;
use serde::Serialize;
use serde::de::DeserializeOwned;

/// A typed error from session operations.
#[derive(Debug)]
pub struct SessionError(pub String);

impl std::fmt::Display for SessionError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "session error: {}", self.0)
    }
}

impl std::error::Error for SessionError {}

/// High-level session ergonomics over `tower_sessions::Session`.
///
/// Wraps the raw session with the `put`/`get`/`forget`/`regenerate` API.
/// The underlying `tower_sessions::Session` is accessible via
/// [`Session::raw`] for escape-hatch access.
pub struct Session(pub(crate) crate::auth::tower_sessions::Session);

impl Session {
    /// Store a value in the session.
    pub async fn put<T: Serialize>(&self, key: &str, value: T) -> Result<(), SessionError> {
        self.0
            .insert(key, value)
            .await
            .map_err(|e| SessionError(e.to_string()))
    }

    /// Get a value from the session.
    pub async fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, SessionError> {
        self.0
            .get(key)
            .await
            .map_err(|e| SessionError(e.to_string()))
    }

    /// Remove a value from the session, returning it if present.
    pub async fn forget<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, SessionError> {
        self.0
            .remove(key)
            .await
            .map_err(|e| SessionError(e.to_string()))
    }

    /// Regenerate the session ID. Use after login to prevent session
    /// fixation. Calls `tower_sessions::Session::cycle_id`.
    pub async fn regenerate(&self) -> Result<(), SessionError> {
        self.0
            .cycle_id()
            .await
            .map_err(|e| SessionError(e.to_string()))
    }

    /// Flush all session data (equivalent to logout).
    pub async fn flush(&self) -> Result<(), SessionError> {
        self.0
            .flush()
            .await
            .map_err(|e| SessionError(e.to_string()))
    }

    /// Access the raw `tower_sessions::Session` for escape-hatch use.
    pub fn raw(&self) -> &crate::auth::tower_sessions::Session {
        &self.0
    }
}

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

    async fn from_request_parts(
        parts: &mut axum::http::request::Parts,
        state: &S,
    ) -> Result<Self, Self::Rejection> {
        let session = crate::auth::tower_sessions::Session::from_request_parts(parts, state)
            .await
            .map_err(|_| unreachable!("Session extraction is infallible"))?;
        Ok(Session(session))
    }
}