1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub enum ItemStatus {
17 Success,
19 Failed(Cow<'static, str>),
21 Skipped(Cow<'static, str>),
23}
24
25impl ItemStatus {
26 pub const fn success() -> Self {
28 Self::Success
29 }
30
31 pub fn failed<S: Into<Cow<'static, str>>>(reason: S) -> Self {
33 Self::Failed(reason.into())
34 }
35
36 pub fn skipped<S: Into<Cow<'static, str>>>(reason: S) -> Self {
38 Self::Skipped(reason.into())
39 }
40
41 pub fn is_success(&self) -> bool {
43 self == &Self::Success
44 }
45
46 pub const fn is_failed(&self) -> bool {
48 matches!(self, &Self::Failed(_))
49 }
50
51 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub enum DeployedDotfileKind {
79 Dotfile(Dotfile),
81 Child(PathBuf),
86}
87
88impl DeployedDotfileKind {
89 pub const fn is_dotfile(&self) -> bool {
91 matches!(self, Self::Dotfile(_))
92 }
93
94 pub const fn is_child(&self) -> bool {
96 matches!(self, Self::Child(_))
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct DeployedDotfile {
103 pub status: ItemStatus,
105
106 pub kind: DeployedDotfileKind,
108}
109
110impl DeployedDotfile {
111 pub const fn status(&self) -> &ItemStatus {
113 &self.status
114 }
115
116 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct DeployedSymlink {
131 pub status: ItemStatus,
133
134 pub source: PathBuf,
136}
137
138impl DeployedSymlink {
139 pub const fn status(&self) -> &ItemStatus {
141 &self.status
142 }
143
144 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158pub enum DeploymentStatus {
159 Success,
161 Failed(Cow<'static, str>),
163}
164
165impl DeploymentStatus {
166 pub const fn success() -> Self {
168 Self::Success
169 }
170
171 pub fn failed<S: Into<Cow<'static, str>>>(reason: S) -> Self {
173 Self::Failed(reason.into())
174 }
175
176 pub fn is_success(&self) -> bool {
178 self == &Self::Success
179 }
180
181 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207pub struct Deployment {
208 time_start: SystemTime,
210
211 time_end: SystemTime,
213
214 status: DeploymentStatus,
216
217 dotfiles: HashMap<PathBuf, DeployedDotfile>,
219
220 symlinks: HashMap<PathBuf, DeployedSymlink>,
222}
223
224impl Deployment {
225 pub const fn time_start(&self) -> &SystemTime {
227 &self.time_start
228 }
229
230 pub const fn time_end(&self) -> &SystemTime {
232 &self.time_end
233 }
234
235 pub fn duration(&self) -> Result<Duration, SystemTimeError> {
237 self.time_end.duration_since(self.time_start)
238 }
239
240 pub const fn status(&self) -> &DeploymentStatus {
242 &self.status
243 }
244
245 pub const fn dotfiles(&self) -> &HashMap<PathBuf, DeployedDotfile> {
247 &self.dotfiles
248 }
249
250 pub const fn symlinks(&self) -> &HashMap<PathBuf, DeployedSymlink> {
252 &self.symlinks
253 }
254
255 pub fn build() -> DeploymentBuilder {
257 DeploymentBuilder::default()
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct DeploymentBuilder {
264 time_start: SystemTime,
269
270 dotfiles: HashMap<PathBuf, DeployedDotfile>,
272
273 symlinks: HashMap<PathBuf, DeployedSymlink>,
275}
276
277impl DeploymentBuilder {
278 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 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 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 pub fn contains<P: AsRef<Path>>(&self, path: P) -> bool {
320 self.dotfiles.contains_key(path.as_ref())
321 }
322
323 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 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 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 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 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 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 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}