cloudiful_redactor/session/
store.rs1use 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}