Skip to main content

ephemeris_forge/
plan.rs

1//! Planning and application independent of a forge transport.
2
3use std::fmt;
4
5use crate::{Backend, Planned, Refusal, RepoRef, Report};
6
7/// A pure reconciliation plan bound to one target repository.
8pub struct Plan<B: Backend> {
9    repository: RepoRef,
10    operations: Vec<B::Operation>,
11    report: Report,
12}
13
14/// A planning or application error.
15#[derive(Debug, thiserror::Error)]
16pub enum PlanError<E> {
17    /// The backend could not read the forge or the configuration is invalid.
18    #[error("the backend could not read the forge or the configuration is invalid")]
19    Backend(#[source] E),
20    /// A write failed. Earlier operations may already have succeeded.
21    #[error(
22        "{failed} failed after writing [{}] and skipping [{}]",
23        completed.join(", "),
24        skipped.join(", ")
25    )]
26    Partial {
27        /// Successful operations before the failure.
28        completed: Vec<String>,
29        /// The operation that failed.
30        failed: String,
31        /// Operations not attempted after the failure.
32        skipped: Vec<String>,
33        /// The adapter error for the failed operation.
34        #[source]
35        source: E,
36    },
37    /// The plan cannot be applied.
38    #[error(transparent)]
39    Refused(#[from] Refusal),
40}
41
42impl<B: Backend> fmt::Debug for Plan<B>
43where
44    B::Operation: fmt::Debug,
45{
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.debug_struct("Plan")
48            .field("repository", &self.repository)
49            .field("operations", &self.operations)
50            .field("report", &self.report)
51            .finish()
52    }
53}
54
55impl<B: Backend> Plan<B> {
56    /// Read a repository and construct its reconciliation plan.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error when the configuration is invalid or the adapter
61    /// cannot read the target repository.
62    pub async fn for_repository(
63        repository: RepoRef,
64        config: &B::Config,
65        backend: &B,
66    ) -> Result<Self, PlanError<B::Error>>
67    where
68        B: Sync,
69    {
70        B::validate(config).map_err(PlanError::Backend)?;
71        let live = backend
72            .read(&repository, config)
73            .await
74            .map_err(PlanError::Backend)?;
75        Ok(Self::for_snapshot(repository, config, &live))
76    }
77
78    /// Construct a plan from a previously read snapshot.
79    #[must_use]
80    pub fn for_snapshot(repository: RepoRef, config: &B::Config, live: &B::Live) -> Self {
81        let Planned { operations, report } = B::plan(config, live);
82        Self {
83            repository,
84            operations,
85            report,
86        }
87    }
88
89    /// The repository this plan targets.
90    #[must_use]
91    pub const fn repository(&self) -> &RepoRef {
92        &self.repository
93    }
94
95    /// The operations in their stable application order.
96    #[must_use]
97    pub fn operations(&self) -> &[B::Operation] {
98        &self.operations
99    }
100
101    /// The change report.
102    #[must_use]
103    pub const fn report(&self) -> &Report {
104        &self.report
105    }
106
107    /// The reason the plan cannot be applied, if any.
108    #[must_use]
109    pub fn refusal(&self) -> Option<Refusal> {
110        self.report.refusal(&self.repository)
111    }
112
113    /// Apply each operation in order.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error when the plan locks the caller out, the repository is
118    /// archived or an operation fails.
119    pub async fn apply(&self, backend: &B) -> Result<(), PlanError<B::Error>>
120    where
121        B: Sync,
122    {
123        if let Some(refusal) = self.refusal() {
124            return Err(refusal.into());
125        }
126        let mut completed = Vec::new();
127        for (index, operation) in self.operations.iter().enumerate() {
128            if let Err(source) = backend.apply(&self.repository, operation).await {
129                return Err(PlanError::Partial {
130                    completed,
131                    failed: operation.to_string(),
132                    skipped: self
133                        .operations
134                        .get(index.saturating_add(1)..)
135                        .unwrap_or(&[])
136                        .iter()
137                        .map(ToString::to_string)
138                        .collect(),
139                    source,
140                });
141            }
142            completed.push(operation.to_string());
143        }
144        Ok(())
145    }
146}