greentic_types/
session.rs

1//! Session identity and cursor helpers.
2
3use alloc::borrow::ToOwned;
4use alloc::string::String;
5
6#[cfg(feature = "schemars")]
7use schemars::JsonSchema;
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11use crate::{FlowId, TenantCtx};
12
13/// Unique key referencing a persisted session.
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16#[cfg_attr(feature = "schemars", derive(JsonSchema))]
17#[cfg_attr(feature = "serde", serde(transparent))]
18pub struct SessionKey(pub String);
19
20impl SessionKey {
21    /// Returns the session key as a string slice.
22    pub fn as_str(&self) -> &str {
23        &self.0
24    }
25
26    /// Creates a new session key from the supplied string.
27    pub fn new(value: impl Into<String>) -> Self {
28        Self(value.into())
29    }
30
31    /// Generates a random session key using [`uuid`], when enabled.
32    #[cfg(feature = "uuid")]
33    pub fn generate() -> Self {
34        Self(uuid::Uuid::new_v4().to_string())
35    }
36}
37
38impl From<String> for SessionKey {
39    fn from(value: String) -> Self {
40        Self(value)
41    }
42}
43
44impl From<&str> for SessionKey {
45    fn from(value: &str) -> Self {
46        Self(value.to_owned())
47    }
48}
49
50impl core::fmt::Display for SessionKey {
51    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52        f.write_str(self.as_str())
53    }
54}
55
56#[cfg(feature = "uuid")]
57impl From<uuid::Uuid> for SessionKey {
58    fn from(value: uuid::Uuid) -> Self {
59        Self(value.to_string())
60    }
61}
62
63const DEFAULT_CANONICAL_ANCHOR: &str = "conversation";
64const DEFAULT_CANONICAL_USER: &str = "user";
65
66/// Build the canonical `{tenant}:{provider}:{anchor}:{user}` session key.
67///
68/// All canonical adapters are expected to follow this format so pause/resume semantics remain
69/// deterministic across ingress providers. The anchor defaults to `conversation` and the user
70/// defaults to `user` when those fields are not supplied.
71pub fn canonical_session_key(
72    tenant: impl AsRef<str>,
73    provider: impl AsRef<str>,
74    anchor: Option<&str>,
75    user: Option<&str>,
76) -> SessionKey {
77    SessionKey::new(format!(
78        "{}:{}:{}:{}",
79        tenant.as_ref(),
80        provider.as_ref(),
81        anchor.unwrap_or(DEFAULT_CANONICAL_ANCHOR),
82        user.unwrap_or(DEFAULT_CANONICAL_USER)
83    ))
84}
85
86/// Cursor pointing at a session's position in a flow graph.
87#[derive(Clone, Debug, PartialEq, Eq, Hash)]
88#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
89#[cfg_attr(feature = "schemars", derive(JsonSchema))]
90pub struct SessionCursor {
91    /// Identifier of the node currently owning the session.
92    pub node_pointer: String,
93    /// Optional wait reason emitted by the node.
94    #[cfg_attr(
95        feature = "serde",
96        serde(default, skip_serializing_if = "Option::is_none")
97    )]
98    pub wait_reason: Option<String>,
99    /// Optional marker describing pending outbox operations.
100    #[cfg_attr(
101        feature = "serde",
102        serde(default, skip_serializing_if = "Option::is_none")
103    )]
104    pub outbox_marker: Option<String>,
105}
106
107impl SessionCursor {
108    /// Creates a new cursor pointing at the provided node identifier.
109    pub fn new(node_pointer: impl Into<String>) -> Self {
110        Self {
111            node_pointer: node_pointer.into(),
112            wait_reason: None,
113            outbox_marker: None,
114        }
115    }
116
117    /// Assigns a wait reason to the cursor.
118    pub fn with_wait_reason(mut self, reason: impl Into<String>) -> Self {
119        self.wait_reason = Some(reason.into());
120        self
121    }
122
123    /// Assigns an outbox marker to the cursor.
124    pub fn with_outbox_marker(mut self, marker: impl Into<String>) -> Self {
125        self.outbox_marker = Some(marker.into());
126        self
127    }
128}
129
130/// Persisted session payload describing how to resume a flow.
131#[derive(Clone, Debug, PartialEq, Eq)]
132#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
133#[cfg_attr(feature = "schemars", derive(JsonSchema))]
134pub struct SessionData {
135    /// Tenant context associated with the session.
136    pub tenant_ctx: TenantCtx,
137    /// Flow identifier being executed.
138    pub flow_id: FlowId,
139    /// Cursor pinpointing where execution paused.
140    pub cursor: SessionCursor,
141    /// Serialized execution context/state snapshot.
142    pub context_json: String,
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn canonical_session_key_includes_components() {
151        let key = canonical_session_key("tenant", "webhook", Some("room-1"), Some("user-5"));
152        assert_eq!(key.as_str(), "tenant:webhook:room-1:user-5");
153    }
154
155    #[test]
156    fn canonical_session_key_defaults_anchor_and_user() {
157        let key = canonical_session_key("tenant", "webhook", None, None);
158        assert_eq!(key.as_str(), "tenant:webhook:conversation:user");
159    }
160}