use sha2::{Digest, Sha256};
use super::{ConversationResetState, Session, SessionKind};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConversationGenerationObservation(Option<ConversationResetState>);
impl ConversationGenerationObservation {
pub fn check_canonical(&self, canonical: &Session) -> Result<(), ConversationGenerationError> {
canonical.validate_conversation_state()?;
if self.0 != canonical.conversation_reset {
return Err(ConversationGenerationError::StaleGeneration);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ConversationGenerationError {
#[error("Session conversation reset state is invalid or unsupported")]
InvalidState,
#[error("Session conversation generation belongs to a different lifetime")]
LifetimeChanged,
#[error("Session conversation generation changed; reload the complete canonical Session")]
StaleGeneration,
}
impl Session {
pub fn conversation_generation_observation(&self) -> ConversationGenerationObservation {
ConversationGenerationObservation(self.conversation_reset.clone())
}
pub fn conversation_revision(&self) -> u64 {
self.conversation_reset
.as_ref()
.map_or(0, |state| state.conversation_revision)
}
pub fn validate_conversation_state(&self) -> Result<(), ConversationGenerationError> {
let Some(state) = self.conversation_reset.as_ref() else {
return Ok(());
};
if self.kind != SessionKind::Root
|| self.parent_session_id.is_some()
|| self.parent_created_at.is_some()
|| self.spawn_depth != 0
|| (!self.root_session_id.is_empty() && self.root_session_id != self.id)
|| state.conversation_revision == 0
|| !state.valid_for(&self.root_mode_birth_token())
{
return Err(ConversationGenerationError::InvalidState);
}
Ok(())
}
pub fn check_conversation_generation(
&self,
canonical: &Session,
) -> Result<(), ConversationGenerationError> {
self.validate_conversation_state()?;
canonical.validate_conversation_state()?;
if self.conversation_reset.is_none() && canonical.conversation_reset.is_none() {
return Ok(());
}
if self.id != canonical.id
|| self.created_at != canonical.created_at
|| self.kind != canonical.kind
|| self.authority_identity != canonical.authority_identity
{
return Err(ConversationGenerationError::LifetimeChanged);
}
if self.conversation_reset != canonical.conversation_reset {
return Err(ConversationGenerationError::StaleGeneration);
}
Ok(())
}
pub fn conversation_reset_sha256(&self) -> Result<Option<String>, ConversationGenerationError> {
self.validate_conversation_state()?;
let Some(state) = self.conversation_reset.as_ref() else {
return Ok(None);
};
let bytes =
serde_json::to_vec(state).map_err(|_| ConversationGenerationError::InvalidState)?;
let mut digest = Sha256::new();
digest.update(b"bamboo.conversation-reset.ledger.v1\0");
digest.update(bytes);
Ok(Some(hex::encode(digest.finalize())))
}
}
#[cfg(test)]
#[path = "conversation_generation_tests.rs"]
mod tests;