use std::{io, path::PathBuf};
use crate::view::NodeId;
pub type ConfigResult<T> = Result<T, ConfigError>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ConfigError {
#[error("failed to read config file `{path}`: {source}")]
ConfigRead {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("invalid TOML in `{path}`{canonical_display}: {source}", canonical_display = match canonical {
Some(p) => format!(" ({})", p.display()),
None => String::new(),
})]
ConfigParse {
path: PathBuf,
canonical: Option<PathBuf>,
#[source]
source: Box<toml::de::Error>,
},
#[error("failed to resolve path `{path}`: {source}")]
PathResolve {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("configuration validation failed: {reason}")]
Validation { reason: String },
#[error(transparent)]
RuleSet(#[from] apimock_routing::RoutingError),
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigErrorKind {
Read,
Parse,
PathResolve,
Validation,
RuleSet,
}
impl ConfigError {
pub fn kind(&self) -> ConfigErrorKind {
match self {
ConfigError::ConfigRead { .. } => ConfigErrorKind::Read,
ConfigError::ConfigParse { .. } => ConfigErrorKind::Parse,
ConfigError::PathResolve { .. } => ConfigErrorKind::PathResolve,
ConfigError::Validation { .. } => ConfigErrorKind::Validation,
ConfigError::RuleSet(_) => ConfigErrorKind::RuleSet,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WorkspaceError {
#[error(transparent)]
Config(#[from] ConfigError),
#[error("workspace root `{path}` is not a valid apimock workspace: {reason}")]
InvalidRoot { path: PathBuf, reason: String },
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkspaceErrorKind {
Config,
InvalidRoot,
}
impl WorkspaceError {
pub fn kind(&self) -> WorkspaceErrorKind {
match self {
WorkspaceError::Config(_) => WorkspaceErrorKind::Config,
WorkspaceError::InvalidRoot { .. } => WorkspaceErrorKind::InvalidRoot,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ApplyError {
#[error("unknown node id: {id}")]
UnknownNode { id: NodeId },
#[error("node {id} is not of the expected kind for this command: {reason}")]
WrongNodeKind { id: NodeId, reason: String },
#[error("invalid edit payload: {reason}")]
InvalidPayload { reason: String },
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApplyErrorKind {
UnknownNode,
WrongNodeKind,
InvalidPayload,
}
impl ApplyError {
pub fn kind(&self) -> ApplyErrorKind {
match self {
ApplyError::UnknownNode { .. } => ApplyErrorKind::UnknownNode,
ApplyError::WrongNodeKind { .. } => ApplyErrorKind::WrongNodeKind,
ApplyError::InvalidPayload { .. } => ApplyErrorKind::InvalidPayload,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SaveError {
#[error("failed to serialise `{path}`: {source}")]
Serialize {
path: PathBuf,
#[source]
source: toml::ser::Error,
},
#[error("failed to write `{path}`: {source}")]
Write {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("internal inconsistency: {reason}")]
Inconsistent { reason: String },
#[error("`{path}` changed on disk since it was loaded; reload before saving")]
Conflict { path: PathBuf },
#[error("failed to read `{path}` to check for external changes: {source}")]
Read {
path: PathBuf,
#[source]
source: io::Error,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SaveErrorKind {
Serialize,
Write,
Inconsistent,
Conflict,
Read,
}
impl SaveError {
pub fn kind(&self) -> SaveErrorKind {
match self {
SaveError::Serialize { .. } => SaveErrorKind::Serialize,
SaveError::Write { .. } => SaveErrorKind::Write,
SaveError::Inconsistent { .. } => SaveErrorKind::Inconsistent,
SaveError::Conflict { .. } => SaveErrorKind::Conflict,
SaveError::Read { .. } => SaveErrorKind::Read,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::ser::Error as _;
use std::error::Error as _;
fn a_toml_parse_error() -> toml::de::Error {
toml::from_str::<toml::Value>("not valid toml =====")
.expect_err("deliberately malformed TOML must fail to parse")
}
#[test]
fn config_parse_display_matches_pre_boxing_format() {
let source = a_toml_parse_error();
let expected_source_display = source.to_string();
let err = ConfigError::ConfigParse {
path: PathBuf::from("apimock.toml"),
canonical: None,
source: Box::new(source),
};
assert_eq!(
err.to_string(),
format!("invalid TOML in `apimock.toml`: {expected_source_display}")
);
}
#[test]
fn config_parse_display_includes_canonical_path_when_present() {
let source = a_toml_parse_error();
let expected_source_display = source.to_string();
let err = ConfigError::ConfigParse {
path: PathBuf::from("apimock.toml"),
canonical: Some(PathBuf::from("/abs/apimock.toml")),
source: Box::new(source),
};
assert_eq!(
err.to_string(),
format!(
"invalid TOML in `apimock.toml` (/abs/apimock.toml): {expected_source_display}"
)
);
}
#[test]
fn config_parse_source_reaches_the_boxed_toml_error() {
let source = a_toml_parse_error();
let source_display = source.to_string();
let err = ConfigError::ConfigParse {
path: PathBuf::from("apimock.toml"),
canonical: None,
source: Box::new(source),
};
let reached = err.source().expect("ConfigParse always carries a source");
assert_eq!(reached.to_string(), source_display);
}
#[test]
fn config_error_kind_matches_every_variant() {
assert_eq!(
ConfigError::ConfigRead {
path: PathBuf::from("x"),
source: io::Error::other("x"),
}
.kind(),
ConfigErrorKind::Read
);
assert_eq!(
ConfigError::ConfigParse {
path: PathBuf::from("x"),
canonical: None,
source: Box::new(a_toml_parse_error()),
}
.kind(),
ConfigErrorKind::Parse
);
assert_eq!(
ConfigError::PathResolve {
path: PathBuf::from("x"),
source: io::Error::other("x"),
}
.kind(),
ConfigErrorKind::PathResolve
);
assert_eq!(
ConfigError::Validation {
reason: "x".to_owned()
}
.kind(),
ConfigErrorKind::Validation
);
assert_eq!(
ConfigError::RuleSet(apimock_routing::RoutingError::RuleSetRead {
path: PathBuf::from("x"),
source: io::Error::other("x"),
})
.kind(),
ConfigErrorKind::RuleSet
);
}
#[test]
fn workspace_error_kind_matches_every_variant() {
assert_eq!(
WorkspaceError::Config(ConfigError::Validation {
reason: "x".to_owned()
})
.kind(),
WorkspaceErrorKind::Config
);
assert_eq!(
WorkspaceError::InvalidRoot {
path: PathBuf::from("x"),
reason: "x".to_owned(),
}
.kind(),
WorkspaceErrorKind::InvalidRoot
);
}
#[test]
fn apply_error_kind_matches_every_variant() {
assert_eq!(
ApplyError::UnknownNode { id: NodeId::new() }.kind(),
ApplyErrorKind::UnknownNode
);
assert_eq!(
ApplyError::WrongNodeKind {
id: NodeId::new(),
reason: "x".to_owned(),
}
.kind(),
ApplyErrorKind::WrongNodeKind
);
assert_eq!(
ApplyError::InvalidPayload {
reason: "x".to_owned(),
}
.kind(),
ApplyErrorKind::InvalidPayload
);
}
#[test]
fn save_error_kind_matches_every_variant() {
assert_eq!(
SaveError::Serialize {
path: PathBuf::from("x"),
source: toml::ser::Error::custom("x"),
}
.kind(),
SaveErrorKind::Serialize
);
assert_eq!(
SaveError::Write {
path: PathBuf::from("x"),
source: io::Error::other("x"),
}
.kind(),
SaveErrorKind::Write
);
assert_eq!(
SaveError::Inconsistent {
reason: "x".to_owned(),
}
.kind(),
SaveErrorKind::Inconsistent
);
assert_eq!(
SaveError::Conflict {
path: PathBuf::from("x"),
}
.kind(),
SaveErrorKind::Conflict
);
assert_eq!(
SaveError::Read {
path: PathBuf::from("x"),
source: io::Error::other("x"),
}
.kind(),
SaveErrorKind::Read
);
}
}