use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value};
use crate::workflow::run::RunRecord;
use crate::workflow::{RunId, RunSelector, RunStatus, WorkflowId, WorkflowState};
use crate::{AppError, WorkspaceId};
#[non_exhaustive]
#[derive(Clone, Deserialize, PartialEq, Serialize)]
pub struct ActionOutcome {
pub node: String,
pub ordinal: u64,
pub result: Map<String, Value>,
}
pub enum CheckpointKind {
Initial,
Control,
Action,
Failed {
node: String,
ordinal: u64,
},
Terminal,
}
#[non_exhaustive]
pub struct Checkpoint {
pub run: RunId,
pub occurrence: String,
pub sequence: u64,
pub kind: CheckpointKind,
pub workflow: WorkflowId,
pub revision: String,
pub action_ordinal: Option<u64>,
pub position: Option<String>,
pub state: WorkflowState,
pub status: RunStatus,
pub outcomes: Vec<ActionOutcome>,
pub lineage: Option<RunLineage>,
}
#[non_exhaustive]
pub struct RunLineage {
pub source: RunId,
pub source_occurrence: String,
pub checkpoint: u64,
pub action_ordinal: Option<u64>,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(
tag = "kind",
content = "value",
rename_all = "snake_case",
deny_unknown_fields
)]
pub enum CheckpointSelector {
Latest,
Sequence(u64),
Action(u64),
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResumePosition {
AfterCompleted,
AtSaved,
Entrypoint,
}
pub struct RestoreOptions {
pub source: RunSelector,
pub checkpoint: CheckpointSelector,
pub overrides: WorkflowState,
pub position: ResumePosition,
pub fallback: Option<WorkflowState>,
}
#[non_exhaustive]
pub struct HistoryPage {
pub checkpoints: Vec<Checkpoint>,
pub next: Option<HistoryCursor>,
}
#[derive(Clone, Debug)]
pub struct HistoryCursor {
workspace: WorkspaceId,
workflow: WorkflowId,
run: RunId,
generation: u64,
creation: String,
cutoff: u64,
after: u64,
}
impl HistoryCursor {
pub(crate) fn issue(
head: &RunRecord,
generation: u64,
creation: &str,
after: u64,
) -> Result<Self, AppError> {
Self {
workspace: head.workspace_id.clone(),
workflow: head.workflow_id.clone(),
run: head.id.clone(),
generation,
creation: creation.to_owned(),
cutoff: head.checkpoint.sequence,
after,
}
.validated()
}
pub(crate) fn continue_from(
&self,
head: &RunRecord,
generation: u64,
creation: &str,
) -> Result<(u64, u64), AppError> {
if self.workspace != head.workspace_id
|| self.workflow != head.workflow_id
|| self.run != head.id
|| self.generation != generation
|| self.creation != creation
|| self.cutoff > head.checkpoint.sequence
{
return Err(AppError::InvalidInput(
"history cursor scope is invalid".into(),
));
}
Ok((self.cutoff, self.after))
}
pub(crate) fn advance_to(&self, after: u64) -> Result<Self, AppError> {
if after <= self.after {
return Err(AppError::Integration("history page did not advance".into()));
}
Self {
after,
..self.clone()
}
.validated()
}
fn validated(self) -> Result<Self, AppError> {
if self.generation == 0
|| self.creation.len() != 32
|| !self
.creation
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
|| self.after >= self.cutoff
{
return Err(AppError::InvalidInput("invalid history cursor".into()));
}
Ok(self)
}
}
impl Serialize for HistoryCursor {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
(
1u8,
&self.workspace,
&self.workflow,
&self.run,
self.generation,
&self.creation,
self.cutoff,
self.after,
)
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for HistoryCursor {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let (version, workspace, workflow, run, generation, creation, cutoff, after) =
<(u8, WorkspaceId, WorkflowId, RunId, u64, String, u64, u64)>::deserialize(
deserializer,
)?;
if version != 1 {
return Err(serde::de::Error::custom("invalid history cursor"));
}
Self {
workspace,
workflow,
run,
generation,
creation,
cutoff,
after,
}
.validated()
.map_err(serde::de::Error::custom)
}
}