cloudiful-redactor 0.5.0

Structured text redaction with reversible sessions for secrets, domains, URLs, and related sensitive values.
Documentation
use anyhow::Error as AnyError;
use async_trait::async_trait;
use thiserror::Error;

use crate::RedactionSession;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredSession {
    pub session: RedactionSession,
    pub version: String,
}

#[derive(Debug, Error)]
pub enum SessionStoreError {
    #[error("session version conflict")]
    Conflict,
    #[error("session store unavailable: {0}")]
    Unavailable(#[source] AnyError),
    #[error("stored session is invalid: {0}")]
    CorruptData(#[source] AnyError),
    #[error("stored session encryption failed: {0}")]
    Crypto(#[source] AnyError),
}

#[async_trait]
pub trait SessionStore: Send + Sync {
    async fn load_latest(
        &self,
        external_id: &str,
    ) -> Result<Option<StoredSession>, SessionStoreError>;

    async fn save_latest(
        &self,
        external_id: &str,
        session: &RedactionSession,
        expected_version: Option<&str>,
    ) -> Result<String, SessionStoreError>;
}

pub fn require_external_id(external_id: Option<&str>) -> anyhow::Result<&str> {
    let value = external_id
        .filter(|value| !value.trim().is_empty())
        .ok_or_else(|| {
            anyhow::anyhow!("external_id is required for stateful session operations")
        })?;
    if value.len() > 256 {
        anyhow::bail!("external_id must not exceed 256 UTF-8 bytes");
    }
    Ok(value)
}

#[cfg(test)]
mod tests {
    use super::require_external_id;

    #[test]
    fn validates_external_id_boundaries() {
        assert!(require_external_id(None).is_err());
        assert!(require_external_id(Some("   ")).is_err());
        assert!(require_external_id(Some(&"x".repeat(257))).is_err());
        assert_eq!(require_external_id(Some("thread-1")).unwrap(), "thread-1");
    }
}