use std::path::PathBuf;
use smol_str::SmolStr;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Source path {0:?} does not exist")]
SourceMissing(PathBuf),
#[error("Source path {0:?} is a symlink; point at the real file or directory instead")]
SourceIsSymlink(PathBuf),
#[error("Source path {0:?} contains no regular files to import")]
SourceEmpty(PathBuf),
#[error(
"Path {0:?} is a regular file; `~/.config/<x>` imports expect a directory. Use `~/.<file>` for single-file dotfiles, or move this into its own `~/.config/<x>/` directory first"
)]
ExpectedDirectory(PathBuf),
#[error("Path {0:?} is not under $HOME")]
PathNotUnderHome(PathBuf),
#[error(
"Path {0:?} is the zenops repo (or inside it); refusing to import the repo into itself"
)]
CannotImportZenopsRepo(PathBuf),
#[error(
"No zenops config found at {0:?} — run `zenops init` first (or `zenops init <url>` to clone an existing one)"
)]
ZenopsRepoMissing(PathBuf),
#[error(
"Path layout {0:?} is not supported by `zenops import`; only ~/.config/<x> or ~/.<x> are recognized — point at the parent (e.g. `import .config/foo` instead of `.config/foo/themes`, or `.ssh` instead of `.ssh/config`)"
)]
UnsupportedLayout(String),
#[error("Cannot derive a valid pkg key from {0:?}; pass --pkg <KEY>")]
NoDerivablePkgKey(String),
#[error("Destination {0:?} already exists; refusing to overwrite")]
DestExists(PathBuf),
#[error(
"Path {path:?} matches multiple managed configs ({}); resolve the duplicate in config.toml first",
candidates.iter().map(SmolStr::as_str).collect::<Vec<_>>().join(", "),
)]
AmbiguousConfigMatch {
path: PathBuf,
candidates: Vec<SmolStr>,
},
#[error(
"{} cannot be combined with importing a file into an already-managed config",
flags.join(", "),
)]
ExtendFlagsInvalid {
flags: Vec<&'static str>,
},
#[error(
"Importing a subdirectory of an already-managed config isn't supported yet; pass a single file under {0:?}"
)]
ExtendDirectoryNotSupported(PathBuf),
#[error(
"Path {0:?} is a directory nested under `~/.config/<pkg>/`; pass a single file to import just that file, or `~/.config/<pkg>` to import the whole directory"
)]
NestedDirectoryNotSupported(PathBuf),
#[error(
"pkg `{0}` is new — pass --brew <PKG>... to set the install hint, or --no-install-hint to skip"
)]
MissingInstallHint(String),
#[error("--brew package name must not be empty")]
EmptyBrewPackage,
#[error(
"--source override {0:?} resolves outside the zenops repo; pass a path relative to ~/.config/zenops (e.g. `configs/<key>`)"
)]
SourceOverrideEscapesRepo(PathBuf),
#[error(
"pkg `{pkg}` already exists with managed configs; pass --pkg <NEW-KEY> to import under a different name, or --source <REL> to add another config to the same pkg"
)]
PkgKeyTaken {
pkg: SmolStr,
},
#[error(
"import requires a terminal for prompts; pass --yes (and --brew or --no-install-hint for new pkgs) to run non-interactively"
)]
NeedsTty,
#[error("Import aborted")]
Aborted,
#[error("Failed to parse {0:?}: {1}")]
ConfigParse(PathBuf, #[source] toml_edit::TomlError),
#[error("Import I/O error at {0:?}: {1}")]
Io(PathBuf, #[source] std::io::Error),
#[error("Failed to copy {src:?} -> {dst:?}: {source}")]
Copy {
src: PathBuf,
dst: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Failed to remove original {0:?}: {1}")]
RemoveOriginal(PathBuf, #[source] std::io::Error),
#[error("Failed to create symlink {symlink:?} -> {real:?}: {source}")]
Symlink {
real: PathBuf,
symlink: PathBuf,
#[source]
source: std::io::Error,
},
}
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::SourceMissing(l), Self::SourceMissing(r)) => l == r,
(Self::SourceIsSymlink(l), Self::SourceIsSymlink(r)) => l == r,
(Self::SourceEmpty(l), Self::SourceEmpty(r)) => l == r,
(Self::ExpectedDirectory(l), Self::ExpectedDirectory(r)) => l == r,
(Self::PathNotUnderHome(l), Self::PathNotUnderHome(r)) => l == r,
(Self::CannotImportZenopsRepo(l), Self::CannotImportZenopsRepo(r)) => l == r,
(Self::ZenopsRepoMissing(l), Self::ZenopsRepoMissing(r)) => l == r,
(Self::UnsupportedLayout(l), Self::UnsupportedLayout(r)) => l == r,
(Self::NoDerivablePkgKey(l), Self::NoDerivablePkgKey(r)) => l == r,
(Self::DestExists(l), Self::DestExists(r)) => l == r,
(Self::EmptyBrewPackage, Self::EmptyBrewPackage) => true,
(Self::SourceOverrideEscapesRepo(l), Self::SourceOverrideEscapesRepo(r)) => l == r,
(Self::PkgKeyTaken { pkg: l }, Self::PkgKeyTaken { pkg: r }) => l == r,
(
Self::AmbiguousConfigMatch {
path: l_path,
candidates: l_cands,
},
Self::AmbiguousConfigMatch {
path: r_path,
candidates: r_cands,
},
) => l_path == r_path && l_cands == r_cands,
(Self::ExtendFlagsInvalid { flags: l }, Self::ExtendFlagsInvalid { flags: r }) => {
l == r
}
(Self::ExtendDirectoryNotSupported(l), Self::ExtendDirectoryNotSupported(r)) => l == r,
(Self::NestedDirectoryNotSupported(l), Self::NestedDirectoryNotSupported(r)) => l == r,
(Self::MissingInstallHint(l), Self::MissingInstallHint(r)) => l == r,
(Self::NeedsTty, Self::NeedsTty) => true,
(Self::Aborted, Self::Aborted) => true,
(Self::ConfigParse(l0, l1), Self::ConfigParse(r0, r1)) => {
l0 == r0 && l1.to_string() == r1.to_string()
}
(Self::Io(l0, l1), Self::Io(r0, r1)) => l0 == r0 && l1.kind() == r1.kind(),
(
Self::Copy {
src: l_src,
dst: l_dst,
source: l_io,
},
Self::Copy {
src: r_src,
dst: r_dst,
source: r_io,
},
) => l_src == r_src && l_dst == r_dst && l_io.kind() == r_io.kind(),
(Self::RemoveOriginal(l0, l1), Self::RemoveOriginal(r0, r1)) => {
l0 == r0 && l1.kind() == r1.kind()
}
(
Self::Symlink {
real: l_real,
symlink: l_sym,
source: l_io,
},
Self::Symlink {
real: r_real,
symlink: r_sym,
source: r_io,
},
) => l_real == r_real && l_sym == r_sym && l_io.kind() == r_io.kind(),
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use std::io;
use std::path::PathBuf;
use similar_asserts::assert_eq;
use super::*;
fn io(kind: io::ErrorKind) -> io::Error {
io::Error::from(kind)
}
#[test]
fn unit_variants_compare_equal_to_themselves() {
assert_eq!(Error::NeedsTty, Error::NeedsTty);
assert_eq!(Error::Aborted, Error::Aborted);
}
#[test]
fn path_variants_eq_and_ne() {
assert_eq!(
Error::SourceMissing(PathBuf::from("/a")),
Error::SourceMissing(PathBuf::from("/a"))
);
assert_ne!(
Error::SourceMissing(PathBuf::from("/a")),
Error::SourceMissing(PathBuf::from("/b"))
);
assert_eq!(
Error::DestExists(PathBuf::from("/a")),
Error::DestExists(PathBuf::from("/a"))
);
}
#[test]
fn unsupported_layout_eq_and_ne() {
assert_eq!(
Error::UnsupportedLayout(".config/foo/themes".into()),
Error::UnsupportedLayout(".config/foo/themes".into())
);
assert_ne!(
Error::UnsupportedLayout(".config/foo/themes".into()),
Error::UnsupportedLayout("dotfiles/zsh".into())
);
}
#[test]
fn io_eq_compares_path_and_kind() {
let a = Error::Io(PathBuf::from("/a"), io(io::ErrorKind::Other));
let b = Error::Io(PathBuf::from("/a"), io(io::ErrorKind::Other));
let c = Error::Io(PathBuf::from("/a"), io(io::ErrorKind::NotFound));
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn copy_eq_compares_paths_and_kind() {
let a = Error::Copy {
src: PathBuf::from("/s"),
dst: PathBuf::from("/d"),
source: io(io::ErrorKind::Other),
};
let b = Error::Copy {
src: PathBuf::from("/s"),
dst: PathBuf::from("/d"),
source: io(io::ErrorKind::Other),
};
let c = Error::Copy {
src: PathBuf::from("/s"),
dst: PathBuf::from("/d"),
source: io(io::ErrorKind::PermissionDenied),
};
assert_eq!(a, b);
assert_ne!(a, c);
}
}