use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, SystemTimeError};
use serde::{Deserialize, Serialize};
use crate::profile::dotfile::Dotfile;
use crate::profile::Priority;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ItemStatus {
Success,
Failed(Cow<'static, str>),
Skipped(Cow<'static, str>),
}
impl ItemStatus {
pub const fn success() -> Self {
Self::Success
}
pub fn failed<S: Into<Cow<'static, str>>>(reason: S) -> Self {
Self::Failed(reason.into())
}
pub fn skipped<S: Into<Cow<'static, str>>>(reason: S) -> Self {
Self::Skipped(reason.into())
}
pub fn is_success(&self) -> bool {
self == &Self::Success
}
pub const fn is_failed(&self) -> bool {
matches!(self, &Self::Failed(_))
}
pub const fn is_skipped(&self) -> bool {
matches!(self, &Self::Skipped(_))
}
}
impl fmt::Display for ItemStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Success => f.write_str("Success"),
Self::Failed(reason) => write!(f, "Failed: {}", reason),
Self::Skipped(reason) => write!(f, "Skipped: {}", reason),
}
}
}
impl<E> From<E> for ItemStatus
where
E: std::error::Error,
{
fn from(value: E) -> Self {
Self::failed(value.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeployedDotfileKind {
Dotfile(Dotfile),
Child(PathBuf),
}
impl DeployedDotfileKind {
pub const fn is_dotfile(&self) -> bool {
matches!(self, Self::Dotfile(_))
}
pub const fn is_child(&self) -> bool {
matches!(self, Self::Child(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeployedDotfile {
pub status: ItemStatus,
pub kind: DeployedDotfileKind,
}
impl DeployedDotfile {
pub const fn status(&self) -> &ItemStatus {
&self.status
}
pub const fn kind(&self) -> &DeployedDotfileKind {
&self.kind
}
}
impl AsRef<ItemStatus> for DeployedDotfile {
fn as_ref(&self) -> &ItemStatus {
self.status()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeployedSymlink {
pub status: ItemStatus,
pub source: PathBuf,
}
impl DeployedSymlink {
pub const fn status(&self) -> &ItemStatus {
&self.status
}
pub fn source(&self) -> &Path {
self.source.as_path()
}
}
impl AsRef<ItemStatus> for DeployedSymlink {
fn as_ref(&self) -> &ItemStatus {
self.status()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeploymentStatus {
Success,
Failed(Cow<'static, str>),
}
impl DeploymentStatus {
pub const fn success() -> Self {
Self::Success
}
pub fn failed<S: Into<Cow<'static, str>>>(reason: S) -> Self {
Self::Failed(reason.into())
}
pub fn is_success(&self) -> bool {
self == &Self::Success
}
pub const fn is_failed(&self) -> bool {
matches!(self, &Self::Failed(_))
}
}
impl fmt::Display for DeploymentStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Success => f.write_str("Success"),
Self::Failed(reason) => write!(f, "Failed: {}", reason),
}
}
}
impl<E> From<E> for DeploymentStatus
where
E: std::error::Error,
{
fn from(value: E) -> Self {
Self::failed(value.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Deployment {
time_start: SystemTime,
time_end: SystemTime,
status: DeploymentStatus,
dotfiles: HashMap<PathBuf, DeployedDotfile>,
symlinks: HashMap<PathBuf, DeployedSymlink>,
}
impl Deployment {
pub const fn time_start(&self) -> &SystemTime {
&self.time_start
}
pub const fn time_end(&self) -> &SystemTime {
&self.time_end
}
pub fn duration(&self) -> Result<Duration, SystemTimeError> {
self.time_end.duration_since(self.time_start)
}
pub const fn status(&self) -> &DeploymentStatus {
&self.status
}
pub const fn dotfiles(&self) -> &HashMap<PathBuf, DeployedDotfile> {
&self.dotfiles
}
pub const fn symlinks(&self) -> &HashMap<PathBuf, DeployedSymlink> {
&self.symlinks
}
pub fn build() -> DeploymentBuilder {
DeploymentBuilder::default()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeploymentBuilder {
time_start: SystemTime,
dotfiles: HashMap<PathBuf, DeployedDotfile>,
symlinks: HashMap<PathBuf, DeployedSymlink>,
}
impl DeploymentBuilder {
pub fn add_dotfile(
&mut self,
path: PathBuf,
dotfile: Dotfile,
status: ItemStatus,
) -> &mut Self {
self.dotfiles.insert(
path,
DeployedDotfile {
kind: DeployedDotfileKind::Dotfile(dotfile),
status,
},
);
self
}
pub fn add_child(&mut self, path: PathBuf, parent: PathBuf, status: ItemStatus) -> &mut Self {
self.dotfiles.insert(
path,
DeployedDotfile {
kind: DeployedDotfileKind::Child(parent),
status,
},
);
self
}
pub fn add_link(&mut self, source: PathBuf, target: PathBuf, status: ItemStatus) -> &mut Self {
self.symlinks
.insert(target, DeployedSymlink { source, status });
self
}
pub fn contains<P: AsRef<Path>>(&self, path: P) -> bool {
self.dotfiles.contains_key(path.as_ref())
}
pub fn get_dotfile<P: AsRef<Path>>(&self, path: P) -> Option<&Dotfile> {
let mut value = self.dotfiles.get(path.as_ref())?;
loop {
match &value.kind {
DeployedDotfileKind::Dotfile(dotfile) => return Some(dotfile),
DeployedDotfileKind::Child(parent_path) => {
value = self.dotfiles.get(parent_path)?
}
}
}
}
pub fn get_deployed_dotfile<P: AsRef<Path>>(&self, path: P) -> Option<&Dotfile> {
let mut value = self.dotfiles.get(path.as_ref())?;
loop {
if !value.status.is_success() {
return None;
}
match &value.kind {
DeployedDotfileKind::Dotfile(dotfile) => return Some(dotfile),
DeployedDotfileKind::Child(parent_path) => {
value = self.dotfiles.get(parent_path)?
}
}
}
}
pub fn get_priority<P: AsRef<Path>>(&self, path: P) -> Option<&Priority> {
self.get_deployed_dotfile(path)
.and_then(|d| d.priority.as_ref())
}
pub fn is_deployed<P: AsRef<Path>>(&self, path: P) -> Option<bool> {
self.dotfiles
.get(path.as_ref())
.map(|dotfile| dotfile.status.is_success())
}
pub fn finish(self) -> Deployment {
let failed_dotfiles = self
.dotfiles
.values()
.filter(|d| d.status.is_failed())
.count();
let failed_links = self
.symlinks
.values()
.filter(|d| d.status.is_failed())
.count();
let status = if failed_dotfiles > 0 {
DeploymentStatus::failed(format!(
"Deployment of {} dotfiles and {} links failed",
failed_dotfiles, failed_links
))
} else {
DeploymentStatus::Success
};
Deployment {
time_start: self.time_start,
time_end: SystemTime::now(),
status,
dotfiles: self.dotfiles,
symlinks: self.symlinks,
}
}
pub fn success(self) -> Deployment {
Deployment {
time_start: self.time_start,
time_end: SystemTime::now(),
status: DeploymentStatus::Success,
dotfiles: self.dotfiles,
symlinks: self.symlinks,
}
}
pub fn failed<S: Into<Cow<'static, str>>>(self, reason: S) -> Deployment {
Deployment {
time_start: self.time_start,
time_end: SystemTime::now(),
status: DeploymentStatus::Failed(reason.into()),
dotfiles: self.dotfiles,
symlinks: self.symlinks,
}
}
}
impl Default for DeploymentBuilder {
fn default() -> Self {
Self {
time_start: SystemTime::now(),
dotfiles: HashMap::new(),
symlinks: HashMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use color_eyre::Result;
use super::*;
#[test]
fn deployment_builder() -> Result<()> {
crate::tests::setup_test_env();
let builder = Deployment::build();
let deployment = builder.success();
assert!(deployment.status().is_success());
assert!(deployment.duration()? >= Duration::from_secs(0));
Ok(())
}
}