use crate::journal::frame::SaturatingFrom;
use std::fmt;
use lgwks_std::wire::{Archive, Deserialize, Serialize};
use super::MAX_EVIDENCE_REFS;
pub const MAX_CHECKPOINT_STEPS: usize = 256;
pub const MAX_CHECKPOINT_NOTES: usize = 64;
pub const MAX_CHECKPOINT_EVIDENCE: usize = MAX_EVIDENCE_REFS;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Archive, Serialize, Deserialize)]
#[rkyv(crate = lgwks_std::wire::rkyv)]
#[non_exhaustive]
pub enum CorrectionKind {
Override,
Refusal,
}
impl CorrectionKind {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Override => "Override",
Self::Refusal => "Refusal",
}
}
}
impl fmt::Display for CorrectionKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.label())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
#[rkyv(crate = lgwks_std::wire::rkyv)]
pub struct Correction {
kind: CorrectionKind,
text: String,
}
impl Correction {
#[must_use]
pub fn new(kind: CorrectionKind, text: &str) -> Self {
Self {
kind,
text: text.to_owned(),
}
}
#[must_use]
pub const fn kind(&self) -> CorrectionKind {
self.kind
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Archive, Serialize, Deserialize)]
#[rkyv(crate = lgwks_std::wire::rkyv)]
#[non_exhaustive]
pub enum EffectNoteKind {
NotApplied,
Applied,
Unknown,
}
impl EffectNoteKind {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::NotApplied => "NotApplied",
Self::Applied => "Applied",
Self::Unknown => "Unknown",
}
}
#[must_use]
pub const fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
}
impl fmt::Display for EffectNoteKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.label())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
#[rkyv(crate = lgwks_std::wire::rkyv)]
pub struct EffectNote {
reference: String,
kind: EffectNoteKind,
}
impl EffectNote {
#[must_use]
pub fn new(reference: &str, kind: EffectNoteKind) -> Self {
Self {
reference: reference.to_owned(),
kind,
}
}
#[must_use]
pub fn reference(&self) -> &str {
&self.reference
}
#[must_use]
pub const fn kind(&self) -> EffectNoteKind {
self.kind
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Archive, Serialize, Deserialize)]
#[rkyv(crate = lgwks_std::wire::rkyv)]
pub struct Checkpoint {
steps: Vec<String>,
corrections: Vec<Correction>,
effects: Vec<EffectNote>,
evidence: Vec<String>,
}
impl Checkpoint {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn steps(&self) -> &[String] {
&self.steps
}
#[must_use]
pub fn corrections(&self) -> &[Correction] {
&self.corrections
}
#[must_use]
pub fn effects(&self) -> &[EffectNote] {
&self.effects
}
#[must_use]
pub fn evidence(&self) -> &[String] {
&self.evidence
}
#[must_use]
pub fn completed(&self, step: &str) -> bool {
self.steps.iter().any(|path| path == step)
}
#[must_use]
pub fn unknowns(&self) -> usize {
self.effects
.iter()
.filter(|note| note.kind().is_unknown())
.count()
}
#[must_use]
pub fn present(&self) -> &[String] {
&self.evidence
}
pub fn complete(&mut self, step: &str) -> Result<(), CheckpointError> {
if self.completed(step) {
return Ok(());
}
check_push(&self.steps, MAX_CHECKPOINT_STEPS, "completed steps")?;
self.steps.push(step.to_owned());
Ok(())
}
pub fn correct(&mut self, kind: CorrectionKind, text: &str) -> Result<(), CheckpointError> {
self.corrections.push(Correction::new(kind, text));
check_push(&self.corrections, MAX_CHECKPOINT_NOTES, "user corrections")
}
pub fn observe_effect(
&mut self,
reference: &str,
kind: EffectNoteKind,
) -> Result<(), CheckpointError> {
let note = EffectNote::new(reference, kind);
match self
.effects
.iter_mut()
.find(|held| held.reference() == reference)
{
Some(held) => {
*held = note;
Ok(())
}
None => {
check_push(&self.effects, MAX_CHECKPOINT_NOTES, "effect notes")?;
self.effects.push(note);
Ok(())
}
}
}
pub fn record_evidence(&mut self, reference: &str) -> Result<(), CheckpointError> {
if self.evidence.iter().any(|held| held == reference) {
return Ok(());
}
check_push(
&self.evidence,
MAX_CHECKPOINT_EVIDENCE,
"evidence references",
)?;
self.evidence.push(reference.to_owned());
Ok(())
}
}
fn check_push<T>(list: &[T], ceiling: usize, what: &'static str) -> Result<(), CheckpointError> {
let would_hold = list.len().saturating_add(1);
if would_hold > ceiling {
let refusal = Err(CheckpointError::Limit {
what,
got: u64::saturating_from(would_hold),
limit: u64::saturating_from(ceiling),
});
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "check_push: returning an error to the caller");
return refusal;
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CheckpointError {
Limit {
what: &'static str,
got: u64,
limit: u64,
},
Archive,
}
impl fmt::Display for CheckpointError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Limit { what, got, limit } => {
write!(formatter, "Limit: {what} would hold {got}, past {limit}")
}
Self::Archive => {
formatter.write_str("Archive: the bytes are not a complete checkpoint archive")
}
}
}
}
impl std::error::Error for CheckpointError {}
impl From<CheckpointError> for super::Refusal {
fn from(error: CheckpointError) -> Self {
match error {
CheckpointError::Limit { what, got, limit } => Self::Limit { what, got, limit },
CheckpointError::Archive => Self::Malformed {
cause: "a checkpoint archive that is not a complete one",
at: 0,
},
}
}
}
impl From<CheckpointError> for crate::script::FlowError {
fn from(error: CheckpointError) -> Self {
Self::failed(error)
}
}
impl Checkpoint {
pub fn to_record(&self) -> Result<Vec<u8>, lgwks_std::wire::WireError> {
crate::script::run_store::Durable::to_record(self)
}
pub fn from_record(bytes: &[u8]) -> Result<Self, lgwks_std::wire::WireError> {
crate::script::run_store::Durable::from_record(bytes)
}
}