#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
#[cfg(feature = "std")]
use std::{string::String, vec::Vec};
mod builder;
mod consequence;
mod ghost;
mod payload;
mod signature;
mod status;
mod verb;
pub use builder::LogLineBuilder;
pub use consequence::{Escalation, FailureHandling, Outcome};
pub use ghost::GhostRecord;
pub use payload::Payload;
pub use signature::{SignError, Signable, Signature, Signer};
pub use status::Status;
pub use verb::{Verb, VerbRegistry};
use thiserror::Error;
pub const TUPLE_FIELD_COUNT: usize = 9;
#[derive(Error, Debug, PartialEq)]
pub enum LogLineError {
#[error("Missing consequence invariant: {0}")]
MissingInvariant(&'static str),
#[error("Missing field: {0}")]
MissingField(&'static str),
#[error("Invalid status transition: {from:?} → {to:?}")]
InvalidTransition { from: Status, to: Status },
#[error("Cannot ghost a committed LogLine")]
AlreadyCommitted,
#[error("Signing error")]
Signing,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LogLine {
pub who: String,
pub did: Verb,
pub this: Payload,
pub when: u64,
pub confirmed_by: Option<String>,
pub if_ok: Outcome,
pub if_doubt: Escalation,
pub if_not: FailureHandling,
pub status: Status,
}
impl LogLine {
pub fn builder() -> LogLineBuilder {
LogLineBuilder::new()
}
pub fn verify_invariants(&self) -> Result<(), LogLineError> {
if self.who.is_empty() {
return Err(LogLineError::MissingField("who"));
}
if self.when == 0 {
return Err(LogLineError::MissingField("when"));
}
if self.if_ok.is_empty() {
return Err(LogLineError::MissingInvariant("if_ok"));
}
if self.if_doubt.is_empty() {
return Err(LogLineError::MissingInvariant("if_doubt"));
}
if self.if_not.is_empty() {
return Err(LogLineError::MissingInvariant("if_not"));
}
Ok(())
}
pub fn sign(self, signer: &dyn Signer) -> Result<Self, LogLineError> {
match self.status {
Status::Draft | Status::Pending => {
let _sig = signer
.sign(&self.to_signable_bytes())
.map_err(|_| LogLineError::Signing)?;
Ok(self)
}
_ => Err(LogLineError::InvalidTransition {
from: self.status,
to: Status::Committed,
}),
}
}
pub fn freeze(mut self) -> Result<Self, LogLineError> {
status::ensure(Status::Draft, Status::Pending, self.status)?;
self.verify_invariants()?;
self.status = Status::Pending;
Ok(self)
}
pub fn freeze_with_registry(self, registry: &dyn VerbRegistry) -> Result<Self, LogLineError> {
if !registry.is_allowed(&self.did) {
return Err(LogLineError::MissingField("did (unknown verb)"));
}
self.freeze()
}
pub fn commit(mut self, signer: &dyn Signer) -> Result<Self, LogLineError> {
status::ensure(Status::Pending, Status::Committed, self.status)?;
let _sig = signer
.sign(&self.to_signable_bytes())
.map_err(|_| LogLineError::Signing)?;
self.status = Status::Committed;
Ok(self)
}
pub fn abandon(self, reason: Option<String>) -> Result<GhostRecord, LogLineError> {
match self.status {
Status::Committed => Err(LogLineError::AlreadyCommitted),
Status::Draft | Status::Pending => Ok(GhostRecord::from(self, reason)),
_ => Ok(GhostRecord::from(self, reason)),
}
}
pub fn abandon_signed(
self,
signer: &dyn Signer,
reason: Option<String>,
) -> Result<GhostRecord, LogLineError> {
match self.status {
Status::Committed => Err(LogLineError::AlreadyCommitted),
Status::Draft | Status::Pending => {
let _sig = signer
.sign(&self.to_signable_bytes())
.map_err(|_| LogLineError::Signing)?;
Ok(GhostRecord::from(self, reason))
}
_ => Ok(GhostRecord::from(self, reason)),
}
}
pub fn to_signable_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(self.who.as_bytes());
out.extend_from_slice(b"|");
out.extend_from_slice(self.did.as_str().as_bytes());
out.extend_from_slice(b"|");
out.extend_from_slice(self.when.to_string().as_bytes());
out.extend_from_slice(b"|");
out.extend_from_slice(self.status.as_str().as_bytes());
out.extend_from_slice(b"|");
if let Some(c) = &self.confirmed_by {
out.extend_from_slice(c.as_bytes());
}
out.extend_from_slice(b"|");
out.extend_from_slice(self.this.kind().as_bytes());
out
}
}