use alloc::vec::Vec;
use crate::wire::ConversationId;
use super::conversation_codec as codec;
use super::operation_event::ConversationOperation;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ConversationGenesis {
conversation_id: ConversationId,
}
impl ConversationGenesis {
#[must_use]
pub const fn new(conversation_id: ConversationId) -> Self {
Self { conversation_id }
}
#[must_use]
pub const fn conversation_id(self) -> ConversationId {
self.conversation_id
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct ParticipantConversation {
genesis: ConversationGenesis,
next_event_ordinal: u64,
genesis_validated: bool,
}
impl ParticipantConversation {
#[must_use]
pub const fn from_genesis(genesis: ConversationGenesis) -> Self {
Self {
genesis,
next_event_ordinal: 0,
genesis_validated: false,
}
}
#[must_use]
pub const fn conversation_id(&self) -> ConversationId {
self.genesis.conversation_id
}
#[must_use]
pub const fn next_event_ordinal(&self) -> u64 {
self.next_event_ordinal
}
#[must_use]
pub const fn genesis_validated(&self) -> bool {
self.genesis_validated
}
#[must_use]
pub const fn decide_genesis_validation(self) -> ConversationDecision {
if self.genesis_validated {
return ConversationDecision::Refused(ConversationRefusal {
conversation: self,
reason: ConversationRefusalReason::GenesisAlreadyValidated,
});
}
let Some(resulting_event_ordinal) = self.next_event_ordinal.checked_add(1) else {
let ordinal = self.next_event_ordinal;
return ConversationDecision::Refused(ConversationRefusal {
conversation: self,
reason: ConversationRefusalReason::EventOrdinalExhausted { ordinal },
});
};
let event = ConversationEvent {
header: ConversationEventHeader {
conversation_id: self.genesis.conversation_id,
ordinal: self.next_event_ordinal,
},
body: ConversationEventBody::GenesisValidated,
};
ConversationDecision::Commit(ConversationCommit {
conversation: self,
event,
resulting_event_ordinal,
})
}
#[must_use]
pub const fn decide_operation(self, operation: ConversationOperation) -> ConversationDecision {
if !self.genesis_validated {
return ConversationDecision::Refused(ConversationRefusal {
conversation: self,
reason: ConversationRefusalReason::GenesisNotValidated,
});
}
let actual = operation.conversation_id();
if actual != self.genesis.conversation_id {
let expected = self.genesis.conversation_id;
return ConversationDecision::Refused(ConversationRefusal {
conversation: self,
reason: ConversationRefusalReason::OperationConversationMismatch {
expected,
actual,
},
});
}
let Some(resulting_event_ordinal) = self.next_event_ordinal.checked_add(1) else {
let ordinal = self.next_event_ordinal;
return ConversationDecision::Refused(ConversationRefusal {
conversation: self,
reason: ConversationRefusalReason::EventOrdinalExhausted { ordinal },
});
};
let event = ConversationEvent {
header: ConversationEventHeader {
conversation_id: self.genesis.conversation_id,
ordinal: self.next_event_ordinal,
},
body: ConversationEventBody::Operation(operation),
};
ConversationDecision::Commit(ConversationCommit {
conversation: self,
event,
resulting_event_ordinal,
})
}
#[allow(
clippy::needless_pass_by_value,
reason = "replay consumes the non-Clone decoded occurrence so its private body cannot be reused as live transition authority"
)]
pub fn replay(mut self, event: ConversationEvent) -> Result<Self, ConversationReplayFailure> {
let resulting_event_ordinal = match self.validate_event(&event) {
Ok(resulting_event_ordinal) => resulting_event_ordinal,
Err(reason) => {
return Err(ConversationReplayFailure {
conversation: self,
reason,
});
}
};
self.apply_validated_event(&event, resulting_event_ordinal);
Ok(self)
}
fn validate_event(&self, event: &ConversationEvent) -> Result<u64, ConversationReplayError> {
if event.header.conversation_id != self.genesis.conversation_id {
return Err(ConversationReplayError::ConversationMismatch {
expected: self.genesis.conversation_id,
actual: event.header.conversation_id,
});
}
if event.header.ordinal != self.next_event_ordinal {
return Err(ConversationReplayError::OrdinalMismatch {
expected: self.next_event_ordinal,
actual: event.header.ordinal,
});
}
match event.body {
ConversationEventBody::GenesisValidated if self.genesis_validated => {
Err(ConversationReplayError::GenesisAlreadyValidated)
}
ConversationEventBody::Operation(_) if !self.genesis_validated => {
Err(ConversationReplayError::GenesisNotValidated)
}
ConversationEventBody::GenesisValidated | ConversationEventBody::Operation(_) => {
event.header.ordinal.checked_add(1).ok_or(
ConversationReplayError::EventOrdinalExhausted {
ordinal: event.header.ordinal,
},
)
}
}
}
const fn apply_validated_event(
&mut self,
event: &ConversationEvent,
resulting_event_ordinal: u64,
) {
match &event.body {
ConversationEventBody::GenesisValidated => {
self.genesis_validated = true;
self.next_event_ordinal = resulting_event_ordinal;
}
ConversationEventBody::Operation(_) => {
self.next_event_ordinal = resulting_event_ordinal;
}
}
}
#[cfg(test)]
pub(super) const fn from_test_state(
genesis: ConversationGenesis,
next_event_ordinal: u64,
genesis_validated: bool,
) -> Self {
Self {
genesis,
next_event_ordinal,
genesis_validated,
}
}
}
#[derive(Debug, PartialEq, Eq)]
struct ConversationEventHeader {
conversation_id: ConversationId,
ordinal: u64,
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum ConversationEventBody {
GenesisValidated,
Operation(ConversationOperation),
}
#[derive(Debug, PartialEq, Eq)]
pub struct ConversationEvent {
header: ConversationEventHeader,
body: ConversationEventBody,
}
impl ConversationEvent {
#[must_use]
pub const fn conversation_id(&self) -> ConversationId {
self.header.conversation_id
}
#[must_use]
pub const fn ordinal(&self) -> u64 {
self.header.ordinal
}
#[must_use]
pub const fn encoded_len(&self) -> usize {
codec::EVENT_HEADER_LEN + codec::encoded_body_len(&self.body) as usize
}
#[must_use]
pub fn encode_canonical(&self) -> Vec<u8> {
codec::encode_event(self.header.conversation_id, self.header.ordinal, &self.body)
}
pub fn decode_canonical(input: &[u8]) -> Result<Self, ConversationEventDecodeError> {
let (conversation_id, ordinal, body) = codec::decode_event(input)?;
Ok(Self {
header: ConversationEventHeader {
conversation_id,
ordinal,
},
body,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConversationEventDecodeError {
Truncated {
required: usize,
available: usize,
},
InvalidMagic,
UnsupportedCodec {
major: u16,
minor: u16,
},
UnknownEventKind {
tag: u16,
},
NonCanonicalLength {
declared_body_len: u32,
actual_body_len: usize,
},
NonCanonicalBody {
tag: u16,
},
LengthOverflow,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConversationReplayError {
ConversationMismatch {
expected: ConversationId,
actual: ConversationId,
},
OrdinalMismatch {
expected: u64,
actual: u64,
},
GenesisAlreadyValidated,
GenesisNotValidated,
EventOrdinalExhausted {
ordinal: u64,
},
}
#[derive(Debug, PartialEq, Eq)]
pub struct ConversationReplayFailure {
conversation: ParticipantConversation,
reason: ConversationReplayError,
}
impl ConversationReplayFailure {
#[must_use]
pub const fn reason(&self) -> ConversationReplayError {
self.reason
}
#[must_use]
pub const fn into_conversation(self) -> ParticipantConversation {
self.conversation
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConversationRefusalReason {
GenesisAlreadyValidated,
GenesisNotValidated,
OperationConversationMismatch {
expected: ConversationId,
actual: ConversationId,
},
EventOrdinalExhausted {
ordinal: u64,
},
}
#[derive(Debug, PartialEq, Eq)]
pub struct ConversationRefusal {
conversation: ParticipantConversation,
reason: ConversationRefusalReason,
}
impl ConversationRefusal {
#[must_use]
pub const fn reason(&self) -> ConversationRefusalReason {
self.reason
}
#[must_use]
pub const fn into_conversation(self) -> ParticipantConversation {
self.conversation
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum ConversationDecision {
Commit(ConversationCommit),
Refused(ConversationRefusal),
}
#[derive(Debug, PartialEq, Eq)]
pub struct ConversationCommit {
conversation: ParticipantConversation,
event: ConversationEvent,
resulting_event_ordinal: u64,
}
impl ConversationCommit {
#[must_use]
pub const fn event(&self) -> &ConversationEvent {
&self.event
}
#[must_use]
pub const fn commit(mut self) -> ParticipantConversation {
self.conversation
.apply_validated_event(&self.event, self.resulting_event_ordinal);
self.conversation
}
#[must_use]
pub const fn abort(self) -> ParticipantConversation {
self.conversation
}
}