use crate::contract::events::{BoundaryReportEvent, BoundaryStartedEvent};
use crate::contract::ids::{AttemptId, BackendId, BoundaryPlanHash};
use crate::contract::plan::{BoundaryPlan, BoundarySpec, PlanError};
use crate::contract::registry::{BackendRegistry, BoundaryPlanner};
use crate::contract::report::BoundaryReport;
mod sealed {
pub trait Sealed {}
}
pub trait BoundaryState: sealed::Sealed {}
#[derive(Debug)]
pub struct Spec {
spec: BoundarySpec,
backend: BackendId,
}
#[derive(Debug)]
pub struct Planned {
plan: BoundaryPlan,
}
#[derive(Debug)]
pub struct Started {
plan: BoundaryPlan,
attempt: AttemptId,
}
#[derive(Debug)]
pub struct Reported {
plan: BoundaryPlan,
attempt: AttemptId,
report: BoundaryReport,
}
impl sealed::Sealed for Spec {}
impl sealed::Sealed for Planned {}
impl sealed::Sealed for Started {}
impl sealed::Sealed for Reported {}
impl BoundaryState for Spec {}
impl BoundaryState for Planned {}
impl BoundaryState for Started {}
impl BoundaryState for Reported {}
#[derive(Debug)]
pub struct Boundary<S: BoundaryState> {
state: S,
}
impl Boundary<Spec> {
#[must_use]
pub fn propose(spec: BoundarySpec, backend: BackendId) -> Self {
Self {
state: Spec { spec, backend },
}
}
#[must_use]
pub fn spec(&self) -> &BoundarySpec {
&self.state.spec
}
#[must_use]
pub fn backend(&self) -> &BackendId {
&self.state.backend
}
pub fn plan(self, registry: &BackendRegistry) -> Result<Boundary<Planned>, PlanError> {
let Spec { spec, backend } = self.state;
let plan = BoundaryPlanner::new(registry).plan(&spec, &backend)?;
Ok(Boundary {
state: Planned { plan },
})
}
}
impl Boundary<Planned> {
#[must_use]
pub fn as_plan(&self) -> &BoundaryPlan {
&self.state.plan
}
#[must_use]
pub fn plan_id(&self) -> BoundaryPlanHash {
self.state.plan.plan_id
}
#[must_use]
pub fn start(self, attempt: AttemptId) -> Boundary<Started> {
let Planned { plan } = self.state;
Boundary {
state: Started { plan, attempt },
}
}
}
impl Boundary<Started> {
#[must_use]
pub fn attempt(&self) -> AttemptId {
self.state.attempt
}
#[must_use]
pub fn as_plan(&self) -> &BoundaryPlan {
&self.state.plan
}
#[must_use]
pub fn plan_id(&self) -> BoundaryPlanHash {
self.state.plan.plan_id
}
#[must_use]
pub fn started_event(&self) -> BoundaryStartedEvent {
BoundaryStartedEvent {
plan: self.state.plan.clone(),
}
}
pub fn into_reported(
self,
report: BoundaryReport,
) -> Result<Boundary<Reported>, LifecycleError> {
let Started { plan, attempt } = self.state;
if report.body.plan_id != plan.plan_id {
return Err(LifecycleError::ReportPlanMismatch {
expected: plan.plan_id,
found: report.body.plan_id,
});
}
Ok(Boundary {
state: Reported {
plan,
attempt,
report,
},
})
}
}
impl Boundary<Reported> {
#[must_use]
pub fn attempt(&self) -> AttemptId {
self.state.attempt
}
#[must_use]
pub fn as_plan(&self) -> &BoundaryPlan {
&self.state.plan
}
#[must_use]
pub fn plan_id(&self) -> BoundaryPlanHash {
self.state.plan.plan_id
}
#[must_use]
pub fn report(&self) -> &BoundaryReport {
&self.state.report
}
#[must_use]
pub fn report_event(&self) -> BoundaryReportEvent {
BoundaryReportEvent {
report: self.state.report.clone(),
}
}
#[must_use]
pub fn into_report_event(self) -> BoundaryReportEvent {
BoundaryReportEvent {
report: self.state.report,
}
}
#[must_use]
pub fn into_parts(self) -> (BoundaryPlan, AttemptId, BoundaryReport) {
let Reported {
plan,
attempt,
report,
} = self.state;
(plan, attempt, report)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum LifecycleError {
ReportPlanMismatch {
expected: BoundaryPlanHash,
found: BoundaryPlanHash,
},
}
impl std::fmt::Display for LifecycleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ReportPlanMismatch { expected, found } => write!(
f,
"report plan id {found:?} does not match the started attempt's plan id {expected:?}"
),
}
}
}
impl std::error::Error for LifecycleError {}