Skip to main content

apimock_routing/
error.rs

1//! Errors surfaced by the routing crate.
2//!
3//! # Why routing has its own error type
4//!
5//! Before 5.0, every failure in apimock funnelled into a single
6//! `AppError`. That's natural for a single-crate project but awkward
7//! across a workspace, because `apimock-routing` shouldn't have to know
8//! about TLS failures or listener-address parsing — those are
9//! server-layer concerns. Each of the three crates now defines its own
10//! error variants at the right abstraction level:
11//!
12//! - `apimock-routing::RoutingError`  — rule-set read / parse
13//! - `apimock-config::ConfigError`    — config read / parse, middleware
14//!   compile, path resolution; wraps `RoutingError` when the failure
15//!   came from a rule set
16//! - `apimock-server::ServerError`    — TLS load, listener address
17//!
18//! The façade crate (`apimock`) re-exports all three under one
19//! convenience alias (`AppError`) for existing consumers.
20//!
21//! # `#[non_exhaustive]` and `kind()` (RFC 041)
22//!
23//! `RoutingError` is `#[non_exhaustive]` and gains a `kind()` accessor
24//! returning `RoutingErrorKind`, one variant per `RoutingError` variant
25//! — the same treatment applied to the other five public error enums
26//! in this workspace. See `apimock_config::error`'s module doc for the
27//! full reasoning (why `#[non_exhaustive]`, why `kind()`, and why it's
28//! deliberately not the same taxonomy as `apimock::cmd::envelope::ErrorKind`).
29
30use std::{io, path::PathBuf};
31
32/// Result alias used inside this crate.
33pub type RoutingResult<T> = Result<T, RoutingError>;
34
35/// All fatal errors produced by routing-layer operations.
36#[derive(Debug, thiserror::Error)]
37#[non_exhaustive]
38pub enum RoutingError {
39    /// A rule-set TOML file could not be read.
40    #[error("failed to read rule set file `{path}`: {source}")]
41    RuleSetRead {
42        path: PathBuf,
43        #[source]
44        source: io::Error,
45    },
46
47    /// A rule-set TOML file could not be parsed.
48    #[error("invalid rule set TOML in `{path}`{canonical_display}: {source}", canonical_display = match canonical {
49        Some(p) => format!(" ({})", p.display()),
50        None => String::new(),
51    })]
52    RuleSetParse {
53        path: PathBuf,
54        canonical: Option<PathBuf>,
55        // Boxed (RFC 041): `toml::de::Error` is 88 bytes, making this
56        // variant 136 — measured as the exact cause of this crate's
57        // `clippy::result_large_err` suppression. `#[source]` still
58        // reaches through the box; representation change only.
59        #[source]
60        source: Box<toml::de::Error>,
61    },
62}
63
64/// `RoutingError`'s failure class.
65#[non_exhaustive]
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum RoutingErrorKind {
68    RuleSetRead,
69    RuleSetParse,
70}
71
72impl RoutingError {
73    pub fn kind(&self) -> RoutingErrorKind {
74        match self {
75            RoutingError::RuleSetRead { .. } => RoutingErrorKind::RuleSetRead,
76            RoutingError::RuleSetParse { .. } => RoutingErrorKind::RuleSetParse,
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use std::error::Error as _;
85
86    fn a_toml_parse_error() -> toml::de::Error {
87        toml::from_str::<toml::Value>("not valid toml =====")
88            .expect_err("deliberately malformed TOML must fail to parse")
89    }
90
91    // ── RFC 041 § 6: boxing must not change Display / source() ────────
92
93    #[test]
94    fn rule_set_parse_display_matches_pre_boxing_format() {
95        let source = a_toml_parse_error();
96        let expected_source_display = source.to_string();
97        let err = RoutingError::RuleSetParse {
98            path: PathBuf::from("rules.toml"),
99            canonical: None,
100            source: Box::new(source),
101        };
102        assert_eq!(
103            err.to_string(),
104            format!("invalid rule set TOML in `rules.toml`: {expected_source_display}")
105        );
106    }
107
108    #[test]
109    fn rule_set_parse_source_reaches_the_boxed_toml_error() {
110        let source = a_toml_parse_error();
111        let source_display = source.to_string();
112        let err = RoutingError::RuleSetParse {
113            path: PathBuf::from("rules.toml"),
114            canonical: None,
115            source: Box::new(source),
116        };
117        let reached = err.source().expect("RuleSetParse always carries a source");
118        assert_eq!(reached.to_string(), source_display);
119    }
120
121    #[test]
122    fn routing_error_kind_matches_every_variant() {
123        assert_eq!(
124            RoutingError::RuleSetRead {
125                path: PathBuf::from("x"),
126                source: io::Error::other("x"),
127            }
128            .kind(),
129            RoutingErrorKind::RuleSetRead
130        );
131        assert_eq!(
132            RoutingError::RuleSetParse {
133                path: PathBuf::from("x"),
134                canonical: None,
135                source: Box::new(a_toml_parse_error()),
136            }
137            .kind(),
138            RoutingErrorKind::RuleSetParse
139        );
140    }
141}