use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::expr::{Dur, Expr, Tpl};
use crate::service::ServiceResult;
use crate::write_hooks::WriteCtx;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Backoff {
Exponential,
Fixed(i64),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RetrySpec {
pub max_attempts: u32,
pub backoff: Backoff,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Outcome {
#[serde(default)]
pub set: Vec<(String, Tpl)>,
#[serde(default)]
pub next: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct FailureSpec {
#[serde(default)]
pub next: Option<String>,
#[serde(default)]
pub set: Vec<(String, Tpl)>,
#[serde(default)]
pub retry: Option<RetrySpec>,
#[serde(default)]
pub on_exhausted: Option<Outcome>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Branch {
#[serde(default)]
pub when: Option<Expr>,
#[serde(default)]
pub set: Vec<(String, Tpl)>,
pub next: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WaitKey {
pub event_field: String,
pub context: Expr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Terminal {
Completed,
Failed,
Expired,
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmitSpec {
pub event: String,
pub data: Tpl,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum StepKind {
Action {
action: String,
#[serde(default)]
entity: Option<String>,
#[serde(default)]
id: Option<Tpl>,
#[serde(default)]
params: Vec<(String, Tpl)>,
on_success: Outcome,
#[serde(default)]
on_timeout: Option<Outcome>,
},
Transition {
entity: String,
id: Tpl,
verb: String,
#[serde(default)]
params: Vec<(String, Tpl)>,
on_success: Outcome,
},
Condition {
branches: Vec<Branch>,
},
Wait {
#[serde(default)]
event: Option<String>,
#[serde(default)]
key: Vec<WaitKey>,
#[serde(default)]
timeout: Option<Dur>,
#[serde(default)]
on_event: Option<Outcome>,
#[serde(default)]
on_timeout: Option<Outcome>,
},
Terminal {
status: Terminal,
#[serde(default)]
reason: Option<Tpl>,
#[serde(default)]
emit: Option<EmitSpec>,
#[serde(default)]
compensate: bool,
#[serde(default)]
result: Option<Tpl>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StepSpec {
pub name: String,
#[serde(default)]
pub guard: Option<Expr>,
#[serde(flatten)]
pub kind: StepKind,
#[serde(default)]
pub on_failure: Option<FailureSpec>,
#[serde(default)]
pub call_timeout: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompStep {
pub name: String,
#[serde(default)]
pub when: Option<Expr>,
pub action: String,
#[serde(default)]
pub entity: Option<String>,
#[serde(default)]
pub id: Option<Tpl>,
#[serde(default)]
pub params: Vec<(String, Tpl)>,
#[serde(default)]
pub verb: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WorkflowSpec {
pub name: String,
pub version: i32,
pub trigger_event: String,
#[serde(default)]
pub extract: Vec<(String, Expr)>,
#[serde(default)]
pub context: Vec<(String, Value)>,
pub persistence: bool,
#[serde(default)]
pub timeout: Option<Dur>,
#[serde(default)]
pub retry: Option<RetrySpec>,
pub steps: Vec<StepSpec>,
#[serde(default)]
pub compensation: Vec<CompStep>,
}
impl WorkflowSpec {
pub fn from_json(text: &str) -> Self {
match serde_json::from_str(text) {
Ok(spec) => spec,
Err(e) => panic!("a generated workflow spec does not parse (generator and backbone-core disagree): {e}"),
}
}
pub fn step(&self, name: &str) -> Option<&StepSpec> {
self.steps.iter().find(|s| s.name == name)
}
pub fn events(&self) -> Vec<&str> {
let mut out = vec![self.trigger_event.as_str()];
for step in &self.steps {
if let StepKind::Wait { event: Some(e), .. } = &step.kind {
if !out.contains(&e.as_str()) {
out.push(e.as_str());
}
}
}
out
}
}
#[derive(Debug)]
pub struct Call<'a> {
pub step: &'a str,
pub action: &'a str,
pub entity: Option<&'a str>,
pub id: Option<String>,
pub verb: Option<&'a str>,
pub params: &'a Value,
pub context: &'a Value,
}
#[async_trait]
pub trait WorkflowDefinition: Send + Sync {
fn spec(&self) -> &WorkflowSpec;
async fn perform(&self, ctx: &WriteCtx, call: &Call<'_>) -> ServiceResult<Value>;
async fn function(&self, ctx: &WriteCtx, name: &str, args: &[Value]) -> ServiceResult<Value> {
let _ = (ctx, args);
Err(crate::service::ServiceError::Internal(format!(
"function `{name}` is not implemented"
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_wait_keeps_its_timeout_apart_from_the_steps_call_timeout() {
let spec = WorkflowSpec::from_json(
&json!({
"name": "W", "version": 1, "trigger_event": "E", "persistence": true,
"steps": [
{ "name": "w", "kind": "wait", "event": "X", "key": [], "timeout": { "secs": 900 },
"on_event": { "next": "d" }, "on_timeout": { "next": "d" }, "call_timeout": 30 },
{ "name": "d", "kind": "terminal", "status": "completed" }
]
})
.to_string(),
);
let wait = spec.step("w").unwrap();
assert_eq!(wait.call_timeout, Some(30));
assert!(matches!(
&wait.kind,
StepKind::Wait {
timeout: Some(Dur::Secs(900)),
..
}
));
assert_eq!(spec.events(), vec!["E", "X"]);
}
}