use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct WorkflowDefinition {
pub schema: String,
pub metadata: WorkflowMetadata,
#[serde(default)]
pub limits: WorkflowLimits,
#[serde(default)]
pub actor_slots: Vec<ActorSlot>,
#[serde(default)]
pub runtimes: Vec<RuntimeRequirement>,
#[serde(default)]
pub worksets: Vec<WorksetTemplate>,
pub initial: String,
pub states: Vec<GraphState>,
pub transitions: Vec<Transition>,
}
impl WorkflowDefinition {
pub fn has_supported_schema(&self, supported: &crate::SchemaId) -> bool {
self.schema == supported.as_str()
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct WorkflowMetadata {
pub id: String,
pub name: String,
pub version: String,
#[serde(default)]
pub description: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct WorkflowLimits {
pub max_parallelism: u8,
pub max_workset_items: u16,
pub max_attempts: u8,
}
impl Default for WorkflowLimits {
fn default() -> Self {
Self {
max_parallelism: 8,
max_workset_items: 256,
max_attempts: 3,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum BindingKind {
Actor,
Runtime,
Workspace,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ActorSlot {
pub id: String,
pub kind: BindingKind,
pub label: String,
#[serde(default = "default_true")]
pub required: bool,
#[serde(default)]
pub session_policy: SessionPolicy,
#[serde(default)]
pub entry: bool,
#[serde(default)]
pub fallback: SlotFallbackPolicy,
}
impl ActorSlot {
pub fn required_actor(id: impl Into<String>, label: impl Into<String>) -> Self {
Self {
id: id.into(),
kind: BindingKind::Actor,
label: label.into(),
required: true,
session_policy: SessionPolicy::New,
entry: true,
fallback: SlotFallbackPolicy::default(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct SlotFallbackPolicy {
pub after_transient_attempts: u8,
pub on_quota: bool,
}
impl Default for SlotFallbackPolicy {
fn default() -> Self {
Self {
after_transient_attempts: 2,
on_quota: true,
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum SessionPolicy {
#[default]
New,
Resume,
Sticky,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct RuntimeKind(pub String);
impl RuntimeKind {
pub fn new(kind: impl Into<String>) -> Self {
Self(kind.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for RuntimeKind {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl From<&str> for RuntimeKind {
fn from(kind: &str) -> Self {
Self(kind.to_owned())
}
}
impl From<String> for RuntimeKind {
fn from(kind: String) -> Self {
Self(kind)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeRequirement {
pub id: String,
pub kind: RuntimeKind,
#[serde(default)]
pub version_requirement: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct WorksetTemplate {
pub id: String,
pub item_binding: String,
#[serde(default)]
pub predecessor_field: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GraphStateKind {
Pass,
Choice,
Fork,
Join,
Authorization,
Actor,
Script,
Workset,
Succeed,
Fail,
Blocked,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GraphState {
pub id: String,
pub kind: GraphStateKind,
pub label: String,
#[serde(default)]
pub instruction: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub binding: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub entry: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workset: Option<String>,
#[serde(default)]
pub retry: RetryPolicy,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct RetryPolicy {
pub max_attempts: u8,
pub transient_only: bool,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 1,
transient_only: true,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Transition {
pub id: String,
pub from: String,
pub to: String,
pub event: TransitionEvent,
#[serde(default, skip_serializing_if = "TransitionMode::is_flow")]
pub mode: TransitionMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guard: Option<GuardExpression>,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum TransitionMode {
#[default]
Flow,
Callback,
}
impl TransitionMode {
pub const fn is_flow(&self) -> bool {
matches!(self, Self::Flow)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PendingCallback {
pub state_id: String,
pub state_visit: u64,
pub transition_id: String,
pub event: TransitionEvent,
pub target: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum CallbackDecision {
Enter(String),
Terminate,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum TransitionEvent {
Complete,
Success,
Failure,
}
impl TransitionEvent {
pub const fn as_str(self) -> &'static str {
match self {
Self::Complete => "complete",
Self::Success => "success",
Self::Failure => "failure",
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GuardExpression {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub equals: Option<Value>,
#[serde(default)]
pub exists: bool,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum StrategyRunStatus {
Pending,
AuthorizationRequired,
RuntimeMissing,
Running,
Waiting,
Retryable,
CancelRequested,
CancelInDoubt,
Blocked,
Cancelled,
Failed,
Completed,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum FailureClass {
Transient,
Permanent,
Authority,
Runtime,
Sandbox,
InDoubt,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FallbackReceipt {
pub fallback_from: String,
pub fallback_to: String,
pub reason: String,
pub attempts: u8,
}
const fn default_true() -> bool {
true
}
pub const INPUT_ADAPTER_VERSION: u32 = 1;
pub const SHARED_CONTEXT_RESOURCE: &str = "context";
pub const SHARED_WORKSETS_RESOURCE: &str = "worksets";
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResultRef {
pub run_id: String,
pub node_id: String,
pub node_visit: u64,
pub digest: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub producers: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PredecessorInput {
pub node_id: String,
pub node_visit: u64,
pub result: ResultRef,
}
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SharedWriter {
pub run_id: String,
pub node_id: String,
pub node_visit: u64,
pub command_id: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SharedResourceRef {
pub resource_id: String,
pub revision: u64,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub keys: BTreeMap<String, u64>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InputBinding {
pub run_id: String,
pub node_id: String,
pub node_visit: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub predecessors: Vec<PredecessorInput>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shared: Vec<SharedResourceRef>,
pub input_adapter_version: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub arrival_order: Vec<String>,
}
impl InputBinding {
pub fn key(node_id: &str, node_visit: u64) -> String {
format!("{node_id}\0{node_visit}")
}
pub fn digest(&self) -> String {
let bytes = serde_json::to_vec(self).unwrap_or_default();
sha256_hex(&bytes)
}
pub fn shared_revision(&self, resource_id: &str) -> Option<u64> {
self.shared
.iter()
.find(|reference| reference.resource_id == resource_id)
.map(|reference| reference.revision)
}
pub fn shared_key_revision(&self, resource_id: &str, key: &str) -> Option<u64> {
self.shared
.iter()
.find(|reference| reference.resource_id == resource_id)
.and_then(|reference| reference.keys.get(key).copied())
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum MergePolicy {
#[default]
KeyUnion,
Exclusive,
Cas,
Accumulate,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SharedResourceDecl {
pub id: String,
#[serde(default)]
pub merge: MergePolicy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub writer: Option<String>,
}
impl SharedResourceDecl {
pub fn key_union(id: impl Into<String>) -> Self {
Self {
id: id.into(),
merge: MergePolicy::KeyUnion,
writer: None,
}
}
pub fn accumulate(id: impl Into<String>) -> Self {
Self {
id: id.into(),
merge: MergePolicy::Accumulate,
writer: None,
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ContributionOrder {
#[default]
Declaration,
Arrival,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InputPlan {
pub input_adapter_version: u32,
#[serde(default)]
pub resources: Vec<SharedResourceDecl>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub order_sensitive_nodes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub isolated_nodes: Vec<String>,
}
impl Default for InputPlan {
fn default() -> Self {
Self {
input_adapter_version: INPUT_ADAPTER_VERSION,
resources: vec![
SharedResourceDecl::key_union(SHARED_CONTEXT_RESOURCE),
SharedResourceDecl::key_union(SHARED_WORKSETS_RESOURCE),
],
order_sensitive_nodes: Vec::new(),
isolated_nodes: Vec::new(),
}
}
}
impl InputPlan {
pub fn resource(&self, id: &str) -> Option<&SharedResourceDecl> {
self.resources
.iter()
.find(|declaration| declaration.id == id)
}
pub fn contribution_order(&self, node_id: &str) -> ContributionOrder {
if self
.order_sensitive_nodes
.iter()
.any(|node| node == node_id)
{
ContributionOrder::Arrival
} else {
ContributionOrder::Declaration
}
}
pub fn reads_shared_state(&self, node_id: &str) -> bool {
!self.isolated_nodes.iter().any(|node| node == node_id)
}
}
fn sha256_hex(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
let digest = Sha256::digest(bytes);
digest.iter().map(|byte| format!("{byte:02x}")).collect()
}