use std::fmt;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum ExitClass {
Success = 0,
NothingAvailable = 1,
Usage = 2,
Network = 4,
Io = 5,
Interrupted = 130,
}
impl ExitClass {
#[must_use]
pub const fn code(self) -> u8 {
self as u8
}
}
impl fmt::Display for ExitClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.code())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorId {
FileUnreadable,
CatalogMalformed,
CatalogEmptySelection,
CatalogRestrictedOnly,
GroupUnknown,
ExtensionInvalid,
FilterInvalid,
NameInvalid,
NameListEmpty,
BootstrapUnavailable,
NetworkUnreachable,
OutputUnwritable,
}
impl ErrorId {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::FileUnreadable => "file.unreadable",
Self::CatalogMalformed => "catalog.malformed",
Self::CatalogEmptySelection => "catalog.empty_selection",
Self::CatalogRestrictedOnly => "catalog.restricted_only",
Self::GroupUnknown => "group.unknown",
Self::ExtensionInvalid => "extension.invalid",
Self::FilterInvalid => "filter.invalid",
Self::NameInvalid => "name.invalid",
Self::NameListEmpty => "name.list_empty",
Self::BootstrapUnavailable => "bootstrap.unavailable",
Self::NetworkUnreachable => "network.unreachable",
Self::OutputUnwritable => "output.unwritable",
}
}
}
impl fmt::Display for ErrorId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{path} could not be read")]
FileUnreadable {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("the catalog data could not be parsed")]
CatalogMalformed {
#[source]
source: Box<serde_json::Error>,
},
#[error("the filters you gave leave no extension to check")]
CatalogEmptySelection,
#[error("every extension matching those filters is one the public cannot register under")]
CatalogRestrictedOnly { hidden: usize },
#[error("`{name}` is not a known group")]
GroupUnknown {
name: String,
closest_groups: Vec<String>,
},
#[error("`{extension}` is not a usable domain extension")]
ExtensionInvalid { extension: String },
#[error("`{value}` is not a usable {setting}")]
FilterInvalid { setting: String, value: String },
#[error("`{name}` is not a usable domain name: {reason}")]
NameInvalid { name: String, reason: String },
#[error("no name was given to check")]
NameListEmpty,
#[error("the registry server list could not be fetched and no cached copy is usable")]
BootstrapUnavailable {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("the network could not be reached")]
NetworkUnreachable {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("output could not be written to {target}")]
OutputUnwritable {
target: String,
#[source]
source: std::io::Error,
},
}
impl Error {
#[must_use]
pub const fn id(&self) -> ErrorId {
match self {
Self::FileUnreadable { .. } => ErrorId::FileUnreadable,
Self::CatalogMalformed { .. } => ErrorId::CatalogMalformed,
Self::CatalogEmptySelection => ErrorId::CatalogEmptySelection,
Self::CatalogRestrictedOnly { .. } => ErrorId::CatalogRestrictedOnly,
Self::GroupUnknown { .. } => ErrorId::GroupUnknown,
Self::ExtensionInvalid { .. } => ErrorId::ExtensionInvalid,
Self::FilterInvalid { .. } => ErrorId::FilterInvalid,
Self::NameInvalid { .. } => ErrorId::NameInvalid,
Self::NameListEmpty => ErrorId::NameListEmpty,
Self::BootstrapUnavailable { .. } => ErrorId::BootstrapUnavailable,
Self::NetworkUnreachable { .. } => ErrorId::NetworkUnreachable,
Self::OutputUnwritable { .. } => ErrorId::OutputUnwritable,
}
}
#[must_use]
pub const fn exit_class(&self) -> ExitClass {
match self {
Self::FileUnreadable { .. }
| Self::CatalogMalformed { .. }
| Self::CatalogEmptySelection
| Self::CatalogRestrictedOnly { .. }
| Self::GroupUnknown { .. }
| Self::ExtensionInvalid { .. }
| Self::FilterInvalid { .. }
| Self::NameInvalid { .. }
| Self::NameListEmpty => ExitClass::Usage,
Self::BootstrapUnavailable { .. } | Self::NetworkUnreachable { .. } => {
ExitClass::Network
}
Self::OutputUnwritable { .. } => ExitClass::Io,
}
}
#[must_use]
pub fn remedy(&self) -> String {
match self {
Self::FileUnreadable { .. } => {
"Check that the file exists and that you can read it.".to_owned()
}
Self::CatalogMalformed { .. } => {
"The supplied catalog file is not valid. Remove it to use the bundled one."
.to_owned()
}
Self::CatalogEmptySelection => {
"Loosen a filter, or run `reserve groups` to see what you can pick.".to_owned()
}
Self::CatalogRestrictedOnly { hidden } => {
let zones = if *hidden == 1 { "zone" } else { "zones" };
format!("Pass --include-restricted to see the {hidden} matching {zones}.")
}
Self::GroupUnknown { closest_groups, .. } => {
if closest_groups.is_empty() {
"Run `reserve groups` to see every group.".to_owned()
} else {
format!("Did you mean {}?", closest_groups.join(", "))
}
}
Self::ExtensionInvalid { .. } => {
"Give the extension without a leading dot, such as `com` or `co.uk`.".to_owned()
}
Self::FilterInvalid { .. } => {
"Run `reserve --help` to see the accepted values.".to_owned()
}
Self::NameInvalid { .. } => {
"Give the name without the extension, such as `example`.".to_owned()
}
Self::NameListEmpty => "Pass at least one name to check.".to_owned(),
Self::BootstrapUnavailable { .. } => {
"Check your connection, or pass `--source text` to skip the registry list."
.to_owned()
}
Self::NetworkUnreachable { .. } => "Check your connection and try again.".to_owned(),
Self::OutputUnwritable { source, .. }
if source.kind() == std::io::ErrorKind::AlreadyExists =>
{
"The line above says what is in the way and what to do about it.".to_owned()
}
Self::OutputUnwritable { .. } => {
"Check that the target is writable and has free space.".to_owned()
}
}
}
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exit_codes_are_the_documented_contract() {
assert_eq!(ExitClass::Success.code(), 0);
assert_eq!(ExitClass::NothingAvailable.code(), 1);
assert_eq!(ExitClass::Usage.code(), 2);
assert_eq!(ExitClass::Network.code(), 4);
assert_eq!(ExitClass::Io.code(), 5);
assert_eq!(ExitClass::Interrupted.code(), 130);
assert_eq!(
[
ExitClass::Success,
ExitClass::NothingAvailable,
ExitClass::Usage,
ExitClass::Network,
ExitClass::Io,
ExitClass::Interrupted,
]
.len(),
6,
"the documented contract is exactly these six codes"
);
}
#[test]
fn every_error_has_an_id_and_a_remedy() {
let cases = [
Error::CatalogEmptySelection,
Error::NameListEmpty,
Error::ExtensionInvalid {
extension: "..".to_owned(),
},
];
for case in cases {
assert!(!case.id().as_str().is_empty());
assert!(!case.remedy().is_empty());
}
}
#[test]
fn usage_errors_map_to_the_usage_exit_class() {
assert_eq!(Error::NameListEmpty.exit_class(), ExitClass::Usage);
assert_eq!(Error::CatalogEmptySelection.exit_class(), ExitClass::Usage);
}
#[test]
fn unknown_group_suggests_the_closest_names() {
let error = Error::GroupUnknown {
name: "tec".to_owned(),
closest_groups: vec!["tech".to_owned()],
};
assert!(error.remedy().contains("tech"));
}
}