1use std::fmt;
4
5use crate::{Backend, Planned, Refusal, RepoRef, Report};
6
7pub struct Plan<B: Backend> {
9 repository: RepoRef,
10 operations: Vec<B::Operation>,
11 report: Report,
12}
13
14#[derive(Debug, thiserror::Error)]
16pub enum PlanError<E> {
17 #[error("the backend could not read the forge or the configuration is invalid")]
19 Backend(#[source] E),
20 #[error(
22 "{failed} failed after writing [{}] and skipping [{}]",
23 completed.join(", "),
24 skipped.join(", ")
25 )]
26 Partial {
27 completed: Vec<String>,
29 failed: String,
31 skipped: Vec<String>,
33 #[source]
35 source: E,
36 },
37 #[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 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 #[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 #[must_use]
91 pub const fn repository(&self) -> &RepoRef {
92 &self.repository
93 }
94
95 #[must_use]
97 pub fn operations(&self) -> &[B::Operation] {
98 &self.operations
99 }
100
101 #[must_use]
103 pub const fn report(&self) -> &Report {
104 &self.report
105 }
106
107 #[must_use]
109 pub fn refusal(&self) -> Option<Refusal> {
110 self.report.refusal(&self.repository)
111 }
112
113 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}