Skip to main content

cloudiful_redactor/session/
store.rs

1use anyhow::Error as AnyError;
2use async_trait::async_trait;
3use thiserror::Error;
4
5use crate::RedactionSession;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct StoredSession {
9    pub session: RedactionSession,
10    pub version: String,
11}
12
13#[derive(Debug, Error)]
14pub enum SessionStoreError {
15    #[error("session version conflict")]
16    Conflict,
17    #[error("session store unavailable: {0}")]
18    Unavailable(#[source] AnyError),
19    #[error("stored session is invalid: {0}")]
20    CorruptData(#[source] AnyError),
21    #[error("stored session encryption failed: {0}")]
22    Crypto(#[source] AnyError),
23}
24
25#[async_trait]
26pub trait SessionStore: Send + Sync {
27    async fn load_latest(
28        &self,
29        external_id: &str,
30    ) -> Result<Option<StoredSession>, SessionStoreError>;
31
32    async fn save_latest(
33        &self,
34        external_id: &str,
35        session: &RedactionSession,
36        expected_version: Option<&str>,
37    ) -> Result<String, SessionStoreError>;
38}
39
40pub fn require_external_id(external_id: Option<&str>) -> anyhow::Result<&str> {
41    let value = external_id
42        .filter(|value| !value.trim().is_empty())
43        .ok_or_else(|| {
44            anyhow::anyhow!("external_id is required for stateful session operations")
45        })?;
46    if value.len() > 256 {
47        anyhow::bail!("external_id must not exceed 256 UTF-8 bytes");
48    }
49    Ok(value)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::require_external_id;
55
56    #[test]
57    fn validates_external_id_boundaries() {
58        assert!(require_external_id(None).is_err());
59        assert!(require_external_id(Some("   ")).is_err());
60        assert!(require_external_id(Some(&"x".repeat(257))).is_err());
61        assert_eq!(require_external_id(Some("thread-1")).unwrap(), "thread-1");
62    }
63}