Skip to main content

punktf_lib/visit/deploy/
deployment.rs

1//! Models and structs used by and for the deployment process.
2
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::fmt;
6use std::path::{Path, PathBuf};
7use std::time::{Duration, SystemTime, SystemTimeError};
8
9use serde::{Deserialize, Serialize};
10
11use crate::profile::dotfile::Dotfile;
12use crate::profile::Priority;
13
14/// Contains the status of a deployed item.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub enum ItemStatus {
17	/// The item was successfully created.
18	Success,
19	/// The item deployment failed.
20	Failed(Cow<'static, str>),
21	/// The item deployment was skipped.
22	Skipped(Cow<'static, str>),
23}
24
25impl ItemStatus {
26	/// Marks the item operation as successful.
27	pub const fn success() -> Self {
28		Self::Success
29	}
30
31	/// Marks the item operation as failed.
32	pub fn failed<S: Into<Cow<'static, str>>>(reason: S) -> Self {
33		Self::Failed(reason.into())
34	}
35
36	/// Indicates that the item operation was skipped.
37	pub fn skipped<S: Into<Cow<'static, str>>>(reason: S) -> Self {
38		Self::Skipped(reason.into())
39	}
40
41	/// Checks if the item operation was successful.
42	pub fn is_success(&self) -> bool {
43		self == &Self::Success
44	}
45
46	/// Checks if the item operation has failed.
47	pub const fn is_failed(&self) -> bool {
48		matches!(self, &Self::Failed(_))
49	}
50
51	/// Checks if the item operation was skipped.
52	pub const fn is_skipped(&self) -> bool {
53		matches!(self, &Self::Skipped(_))
54	}
55}
56
57impl fmt::Display for ItemStatus {
58	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59		match self {
60			Self::Success => f.write_str("Success"),
61			Self::Failed(reason) => write!(f, "Failed: {reason}"),
62			Self::Skipped(reason) => write!(f, "Skipped: {reason}"),
63		}
64	}
65}
66
67impl<E> From<E> for ItemStatus
68where
69	E: std::error::Error,
70{
71	fn from(value: E) -> Self {
72		Self::failed(value.to_string())
73	}
74}
75
76/// Defines the type of dotfile.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub enum DeployedDotfileKind {
79	/// A normal dotfile.
80	Dotfile(Dotfile),
81	/// A dotfile that is contained in a directory that is deployed.
82	///
83	/// PathBuf is the deploy path of the `parent` dotfile.
84	/// The parent should always be of type `Dotfile(_)`.
85	Child(PathBuf),
86}
87
88impl DeployedDotfileKind {
89	/// Checks whether the deployed dotfile type is a normal dotfile.
90	pub const fn is_dotfile(&self) -> bool {
91		matches!(self, Self::Dotfile(_))
92	}
93
94	/// Checks whether the deployed dotfile type is a child dotfile.
95	pub const fn is_child(&self) -> bool {
96		matches!(self, Self::Child(_))
97	}
98}
99
100/// Stores the result of a dotfile deployment operation.
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct DeployedDotfile {
103	/// The status of the deployed dotfile.
104	pub status: ItemStatus,
105
106	/// The kind of the deployed dotfile.
107	pub kind: DeployedDotfileKind,
108}
109
110impl DeployedDotfile {
111	/// Returns the status of the dotfile operation.
112	pub const fn status(&self) -> &ItemStatus {
113		&self.status
114	}
115
116	/// Returns the kind of the dotfile operation.
117	pub const fn kind(&self) -> &DeployedDotfileKind {
118		&self.kind
119	}
120}
121
122impl AsRef<ItemStatus> for DeployedDotfile {
123	fn as_ref(&self) -> &ItemStatus {
124		self.status()
125	}
126}
127
128/// Stores the result of a symlink deployment operation.
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct DeployedSymlink {
131	/// The status of the deployed symlink.
132	pub status: ItemStatus,
133
134	/// The source path of the link.
135	pub source: PathBuf,
136}
137
138impl DeployedSymlink {
139	/// Returns the status of the link operation.
140	pub const fn status(&self) -> &ItemStatus {
141		&self.status
142	}
143
144	/// Returns the source path of the link .
145	pub fn source(&self) -> &Path {
146		self.source.as_path()
147	}
148}
149
150impl AsRef<ItemStatus> for DeployedSymlink {
151	fn as_ref(&self) -> &ItemStatus {
152		self.status()
153	}
154}
155
156/// Describes the status of a profile deployment.
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158pub enum DeploymentStatus {
159	/// The profile is deployed successfully.
160	Success,
161	/// There were errors during the deployment.
162	Failed(Cow<'static, str>),
163}
164
165impl DeploymentStatus {
166	/// Returns success.
167	pub const fn success() -> Self {
168		Self::Success
169	}
170
171	/// Returns a failure.
172	pub fn failed<S: Into<Cow<'static, str>>>(reason: S) -> Self {
173		Self::Failed(reason.into())
174	}
175
176	/// Checks if the deployment was successful.
177	pub fn is_success(&self) -> bool {
178		self == &Self::Success
179	}
180
181	/// Checks if the deployment has failed.
182	pub const fn is_failed(&self) -> bool {
183		matches!(self, &Self::Failed(_))
184	}
185}
186
187impl fmt::Display for DeploymentStatus {
188	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189		match self {
190			Self::Success => f.write_str("Success"),
191			Self::Failed(reason) => write!(f, "Failed: {reason}"),
192		}
193	}
194}
195
196impl<E> From<E> for DeploymentStatus
197where
198	E: std::error::Error,
199{
200	fn from(value: E) -> Self {
201		Self::failed(value.to_string())
202	}
203}
204
205/// Describes the deployment of a profile.
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207pub struct Deployment {
208	/// The time the deployment was started.
209	time_start: SystemTime,
210
211	/// The time the deployment was finished.
212	time_end: SystemTime,
213
214	/// The status of the deployment.
215	status: DeploymentStatus,
216
217	/// The dotfiles that were deployed.
218	dotfiles: HashMap<PathBuf, DeployedDotfile>,
219
220	/// The links that were deployed.
221	symlinks: HashMap<PathBuf, DeployedSymlink>,
222}
223
224impl Deployment {
225	/// Returns the time the deployment was started.
226	pub const fn time_start(&self) -> &SystemTime {
227		&self.time_start
228	}
229
230	/// Returns the time the deployment was finished.
231	pub const fn time_end(&self) -> &SystemTime {
232		&self.time_end
233	}
234
235	/// Returns the duration the deployment took.
236	pub fn duration(&self) -> Result<Duration, SystemTimeError> {
237		self.time_end.duration_since(self.time_start)
238	}
239
240	/// Returns the status of the deployment.
241	pub const fn status(&self) -> &DeploymentStatus {
242		&self.status
243	}
244
245	/// Returns the dotfiles.
246	pub const fn dotfiles(&self) -> &HashMap<PathBuf, DeployedDotfile> {
247		&self.dotfiles
248	}
249
250	/// Returns the symlinks.
251	pub const fn symlinks(&self) -> &HashMap<PathBuf, DeployedSymlink> {
252		&self.symlinks
253	}
254
255	/// Builds the deployment.
256	pub fn build() -> DeploymentBuilder {
257		DeploymentBuilder::default()
258	}
259}
260
261/// A builder for a [`Deployment`].
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct DeploymentBuilder {
264	/// The start time of the deployment.
265	///
266	/// This used to keep track of the total execution time of the deployment
267	/// process.
268	time_start: SystemTime,
269
270	/// All dotfiles which were already process by the deployment process.
271	dotfiles: HashMap<PathBuf, DeployedDotfile>,
272
273	/// All symlinks which were already process by the deployment process.
274	symlinks: HashMap<PathBuf, DeployedSymlink>,
275}
276
277impl DeploymentBuilder {
278	/// Adds a dotfile with the given `status` to the builder.
279	pub fn add_dotfile(
280		&mut self,
281		path: PathBuf,
282		dotfile: Dotfile,
283		status: ItemStatus,
284	) -> &mut Self {
285		self.dotfiles.insert(
286			path,
287			DeployedDotfile {
288				kind: DeployedDotfileKind::Dotfile(dotfile),
289				status,
290			},
291		);
292
293		self
294	}
295
296	/// Adds the child of a dotfile directory with the given `status` to the
297	/// builder.
298	pub fn add_child(&mut self, path: PathBuf, parent: PathBuf, status: ItemStatus) -> &mut Self {
299		self.dotfiles.insert(
300			path,
301			DeployedDotfile {
302				kind: DeployedDotfileKind::Child(parent),
303				status,
304			},
305		);
306
307		self
308	}
309
310	/// Adds a symlink with the given `status` to the builder.
311	pub fn add_link(&mut self, source: PathBuf, target: PathBuf, status: ItemStatus) -> &mut Self {
312		self.symlinks
313			.insert(target, DeployedSymlink { source, status });
314
315		self
316	}
317
318	/// Checks if the builder already contains a dotfile for the given `path`.
319	pub fn contains<P: AsRef<Path>>(&self, path: P) -> bool {
320		self.dotfiles.contains_key(path.as_ref())
321	}
322
323	/// Gets any dotfile already deployed at `path`.
324	///
325	/// This function ignores the status of the dotfile.
326	pub fn get_dotfile<P: AsRef<Path>>(&self, path: P) -> Option<&Dotfile> {
327		let mut value = self.dotfiles.get(path.as_ref())?;
328
329		loop {
330			match &value.kind {
331				DeployedDotfileKind::Dotfile(dotfile) => return Some(dotfile),
332				DeployedDotfileKind::Child(parent_path) => {
333					value = self.dotfiles.get(parent_path)?
334				}
335			}
336		}
337	}
338
339	/// Gets any dotfile already deployed at `path`.
340	///
341	/// This function only returns a dotfile with [`ItemStatus::Success`].
342	pub fn get_deployed_dotfile<P: AsRef<Path>>(&self, path: P) -> Option<&Dotfile> {
343		let mut value = self.dotfiles.get(path.as_ref())?;
344
345		loop {
346			if !value.status.is_success() {
347				return None;
348			}
349
350			match &value.kind {
351				DeployedDotfileKind::Dotfile(dotfile) => return Some(dotfile),
352				DeployedDotfileKind::Child(parent_path) => {
353					value = self.dotfiles.get(parent_path)?
354				}
355			}
356		}
357	}
358
359	/// Gets the priority of the dotfile already deployed at `path`.
360	///
361	/// This function only evaluates a dotfile with [`ItemStatus::Success`].
362	pub fn get_priority<P: AsRef<Path>>(&self, path: P) -> Option<&Priority> {
363		self.get_deployed_dotfile(path)
364			.and_then(|d| d.priority.as_ref())
365	}
366
367	/// Checks if a dotfile was already successfully deployed at `path`.
368	///
369	/// This function only evaluates a dotfile with [`ItemStatus::Success`].
370	pub fn is_deployed<P: AsRef<Path>>(&self, path: P) -> Option<bool> {
371		self.dotfiles
372			.get(path.as_ref())
373			.map(|dotfile| dotfile.status.is_success())
374	}
375
376	/// Consumes self and creates a [`Deployment`] from it.
377	///
378	/// This will try to guess the state of the deployment by looking for any
379	/// failed deployed dotfile.
380	pub fn finish(self) -> Deployment {
381		let failed_dotfiles = self
382			.dotfiles
383			.values()
384			.filter(|d| d.status.is_failed())
385			.count();
386
387		let failed_links = self
388			.symlinks
389			.values()
390			.filter(|d| d.status.is_failed())
391			.count();
392
393		let status = if failed_dotfiles > 0 {
394			DeploymentStatus::failed(format!(
395				"Deployment of {failed_dotfiles} dotfiles and {failed_links} links failed"
396			))
397		} else {
398			DeploymentStatus::Success
399		};
400
401		Deployment {
402			time_start: self.time_start,
403			time_end: SystemTime::now(),
404			status,
405			dotfiles: self.dotfiles,
406			symlinks: self.symlinks,
407		}
408	}
409
410	/// Consumes self and creates a [`Deployment`] from it.
411	///
412	/// This will mark the deployment as success.
413	pub fn success(self) -> Deployment {
414		Deployment {
415			time_start: self.time_start,
416			time_end: SystemTime::now(),
417			status: DeploymentStatus::Success,
418			dotfiles: self.dotfiles,
419			symlinks: self.symlinks,
420		}
421	}
422
423	/// Consumes self and creates a [`Deployment`] from it.
424	///
425	/// This will mark the deployment as failed with the reason given with
426	/// `reason`.
427	pub fn failed<S: Into<Cow<'static, str>>>(self, reason: S) -> Deployment {
428		Deployment {
429			time_start: self.time_start,
430			time_end: SystemTime::now(),
431			status: DeploymentStatus::Failed(reason.into()),
432			dotfiles: self.dotfiles,
433			symlinks: self.symlinks,
434		}
435	}
436}
437
438impl Default for DeploymentBuilder {
439	fn default() -> Self {
440		Self {
441			time_start: SystemTime::now(),
442			dotfiles: HashMap::new(),
443			symlinks: HashMap::new(),
444		}
445	}
446}
447
448#[cfg(test)]
449mod tests {
450	use color_eyre::Result;
451
452	use super::*;
453
454	#[test]
455	fn deployment_builder() -> Result<()> {
456		crate::tests::setup_test_env();
457
458		let builder = Deployment::build();
459		let deployment = builder.success();
460
461		assert!(deployment.status().is_success());
462		assert!(deployment.duration()? >= Duration::from_secs(0));
463
464		Ok(())
465	}
466}