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}