use std::collections::BTreeMap;
use std::time::Duration;
use aion_core::{CarryContract, InvariantSpec, ToleranceSpec, WorkloopArming, WorkloopSpec};
use aion_package::{InvariantContract, ToleranceContract, WorkloopContract};
use super::error::WorkloopError;
pub fn spec_from_contract(
workflow_type: &str,
contract: &WorkloopContract,
) -> Result<WorkloopSpec, WorkloopError> {
let arming = arming_from(workflow_type, contract)?;
let invariants = contract
.invariants
.iter()
.map(|invariant| invariant_from(workflow_type, invariant))
.collect::<Result<Vec<_>, _>>()?;
let retention = Duration::from_secs(contract.retention_seconds);
let carry = CarryContract::new(
contract
.carries
.iter()
.map(|carry| (carry.name.clone(), carry.default.clone()))
.collect::<BTreeMap<_, _>>(),
)
.map_err(|error| WorkloopError::Engine {
reason: format!(
"the deployed workloop declaration for `{workflow_type}` carries a seed the engine \
cannot write into generation 1's start payload: {error}"
),
})?;
WorkloopSpec::with_carry(arming, invariants, retention, carry).map_err(|error| {
WorkloopError::Engine {
reason: format!(
"the deployed workloop declaration for `{workflow_type}` cannot be armed: \
{error}. Nothing was started and no registration was written"
),
}
})
}
fn arming_from(
workflow_type: &str,
contract: &WorkloopContract,
) -> Result<WorkloopArming, WorkloopError> {
let signals = contract.arms.clone();
match (contract.cadence_seconds, signals.is_empty()) {
(Some(seconds), true) => WorkloopArming::every(Duration::from_secs(seconds)),
(Some(seconds), false) => {
WorkloopArming::every_with_signals(Duration::from_secs(seconds), signals)
}
(None, false) => WorkloopArming::signal_only(signals),
(None, true) => {
return Err(WorkloopError::Engine {
reason: format!(
"the deployed workloop declaration for `{workflow_type}` arms on neither a \
cadence nor a signal, so nothing would ever wake it. The language refuses \
this (C5), so an archive carrying it was built by a toolchain that did not \
check it"
),
});
}
}
.map_err(|error| WorkloopError::Engine {
reason: format!(
"the deployed workloop declaration for `{workflow_type}` cannot be armed: {error}"
),
})
}
fn tolerance_refusal(workflow_type: &str, invariant: &str, reason: &str) -> WorkloopError {
WorkloopError::Engine {
reason: format!(
"invariant `{invariant}` of the deployed workloop `{workflow_type}` declares a \
tolerance the engine cannot act on: {reason}"
),
}
}
fn invariant_from(
workflow_type: &str,
contract: &InvariantContract,
) -> Result<InvariantSpec, WorkloopError> {
let mut windows = None;
let mut unconfirmed_for = None;
for tolerance in &contract.tolerances {
match tolerance {
ToleranceContract::Windows { count } => windows = Some(*count),
ToleranceContract::UnconfirmedFor { seconds } => {
unconfirmed_for = Some(Duration::from_secs(*seconds));
}
}
}
let tolerance = match (windows, unconfirmed_for) {
(Some(count), Some(duration)) => ToleranceSpec::both(count, duration).map_err(|error| {
tolerance_refusal(workflow_type, &contract.name, &error.to_string())
})?,
(Some(count), None) => ToleranceSpec::count(count),
(None, Some(duration)) => ToleranceSpec::duration(duration).map_err(|error| {
tolerance_refusal(workflow_type, &contract.name, &error.to_string())
})?,
(None, None) => {
return Err(WorkloopError::Engine {
reason: format!(
"invariant `{}` of the deployed workloop `{workflow_type}` declares no \
tolerance, and tolerance has no default (R2.3) — the engine cannot invent \
the threshold at which it alarms",
contract.name
),
});
}
};
let confirms = contract
.confirms
.clone()
.ok_or_else(|| WorkloopError::Engine {
reason: format!(
"invariant `{}` of the deployed workloop `{workflow_type}` names no confirming \
route, so nothing in the document could ever confirm it and it would alarm at \
every window forever",
contract.name
),
})?;
Ok(InvariantSpec {
name: contract.name.clone(),
record_type: contract.record_type.clone(),
tolerance,
confirms: vec![confirms],
})
}