manta_server/
wire_conv.rs

1//! Conversions between wire types (`manta-shared`) and backend types
2//! (`manta-backend-dispatcher`).
3//!
4//! # Why this module exists
5//!
6//! Manta uses two error types (see `CLAUDE.md`'s two-tier error rule):
7//!
8//! - `manta_shared::common::error::MantaError` — produced by shared
9//!   helpers (config loader, audit, JWT, kafka).
10//! - `manta_backend_dispatcher::error::Error` — used everywhere in
11//!   the server's service / backend_dispatcher layers.
12//!
13//! Service-layer code calls into `manta-shared` helpers but needs to
14//! produce `BackendError` results. Rust's orphan rule blocks the
15//! obvious `impl From<MantaError> for BackendError` because both
16//! types are foreign to this crate, so this module exposes a free
17//! function instead: callers write
18//! `.map_err(wire_conv::to_backend)?`.
19//!
20//! Lives server-side rather than in `manta-shared` because
21//! `manta-shared` has no knowledge of the backend dispatcher crate.
22//!
23//! # Scope
24//!
25//! Only error-type mapping is needed at runtime. A `NodeDetails`
26//! converter isn't required: the only place the two `NodeDetails`
27//! types meet is the HTTP wire, and the JSON serialisation is
28//! identical between `csm_rs::node::types::NodeDetails` and
29//! `manta_shared::types::dto::NodeDetails`.
30
31use manta_backend_dispatcher::error::Error as BackendError;
32use manta_shared::common::error::MantaError;
33
34/// Map a `MantaError` (returned by manta-shared's pure helpers) onto
35/// the structured `BackendError` that the server's service layer uses.
36///
37/// **Exhaustiveness** is enforced at compile time: `MantaError` is
38/// not `#[non_exhaustive]`, so adding a new variant breaks this
39/// `match` with E0004. A reviewer suggesting the test suite is the
40/// only line of defence misread the structure — the tests below pin
41/// per-variant *payload* preservation, not exhaustiveness, and they
42/// stay relevant only as a guard against silent renames within the
43/// existing arms (e.g. `BackendError::Message` → `BackendError::Other`).
44pub fn to_backend(e: MantaError) -> BackendError {
45  match e {
46    MantaError::IoError(e) => BackendError::IoError(e),
47    MantaError::ConfigError(e) => BackendError::ConfigError(e),
48    MantaError::TomlEditError(e) => BackendError::TomlEditError(e),
49    MantaError::SerdeError(e) => BackendError::SerdeError(e),
50    MantaError::NetError(e) => BackendError::NetError(e),
51    MantaError::YamlError(e) => BackendError::YamlError(e),
52    MantaError::NotFound(s) => BackendError::NotFound(s),
53    MantaError::MissingField(s) => BackendError::MissingField(s),
54    MantaError::JwtMalformed(s) => BackendError::JwtMalformed(s),
55    MantaError::KafkaError(s) => BackendError::KafkaError(s),
56    MantaError::InvalidPattern(s) => BackendError::InvalidPattern(s),
57    MantaError::TemplateError(s) => BackendError::TemplateError(s),
58    MantaError::Other(s) => BackendError::Message(s),
59  }
60}
61
62#[cfg(test)]
63mod tests {
64  use super::*;
65
66  // The string-bearing variants are 1:1 renames. A test per variant
67  // pins the variant name AND the payload preservation, so a mistyped
68  // arm (NotFound → BadRequest, say) would surface immediately.
69  #[test]
70  #[allow(clippy::type_complexity)]
71  fn string_variants_preserve_payload_and_variant() {
72    let cases: &[(MantaError, fn(&BackendError) -> bool)] = &[
73      (
74        MantaError::NotFound("a".into()),
75        |e| matches!(e, BackendError::NotFound(s) if s == "a"),
76      ),
77      (
78        MantaError::MissingField("b".into()),
79        |e| matches!(e, BackendError::MissingField(s) if s == "b"),
80      ),
81      (
82        MantaError::JwtMalformed("c".into()),
83        |e| matches!(e, BackendError::JwtMalformed(s) if s == "c"),
84      ),
85      (
86        MantaError::KafkaError("d".into()),
87        |e| matches!(e, BackendError::KafkaError(s) if s == "d"),
88      ),
89      (
90        MantaError::InvalidPattern("e".into()),
91        |e| matches!(e, BackendError::InvalidPattern(s) if s == "e"),
92      ),
93      (
94        MantaError::TemplateError("f".into()),
95        |e| matches!(e, BackendError::TemplateError(s) if s == "f"),
96      ),
97    ];
98    for (input, predicate) in cases {
99      let label = format!("{input:?}");
100      let mapped = to_backend(match input {
101        MantaError::NotFound(s) => MantaError::NotFound(s.clone()),
102        MantaError::MissingField(s) => MantaError::MissingField(s.clone()),
103        MantaError::JwtMalformed(s) => MantaError::JwtMalformed(s.clone()),
104        MantaError::KafkaError(s) => MantaError::KafkaError(s.clone()),
105        MantaError::InvalidPattern(s) => MantaError::InvalidPattern(s.clone()),
106        MantaError::TemplateError(s) => MantaError::TemplateError(s.clone()),
107        _ => unreachable!(),
108      });
109      assert!(
110        predicate(&mapped),
111        "wrong mapping for {label}: got {mapped:?}"
112      );
113    }
114  }
115
116  // `Other` is the only RENAMED arm: MantaError::Other → BackendError::Message.
117  // Easy to silently change to `BackendError::Other` if someone "fixes" it
118  // and breaks every caller that depends on the catch-all being 500.
119  #[test]
120  fn other_maps_to_message() {
121    let mapped = to_backend(MantaError::Other("oops".into()));
122    assert!(
123      matches!(&mapped, BackendError::Message(s) if s == "oops"),
124      "Other must map to Message (became {mapped:?})"
125    );
126  }
127
128  // The `#[from]`-bearing variants forward their inner error. Pin the
129  // variant name; the inner type is checked by the compiler at compile
130  // time so we don't need to reconstruct an exact payload.
131  #[test]
132  fn io_error_maps_to_backend_io_error() {
133    let inner = std::io::Error::other("disk on fire");
134    let mapped = to_backend(MantaError::IoError(inner));
135    assert!(
136      matches!(mapped, BackendError::IoError(_)),
137      "IoError must round-trip to BackendError::IoError"
138    );
139  }
140
141  #[test]
142  fn serde_error_maps_to_backend_serde_error() {
143    let inner =
144      serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
145    let mapped = to_backend(MantaError::SerdeError(inner));
146    assert!(matches!(mapped, BackendError::SerdeError(_)));
147  }
148
149  #[test]
150  fn yaml_error_maps_to_backend_yaml_error() {
151    let inner =
152      serde_yaml::from_str::<serde_yaml::Value>("\t:bad").unwrap_err();
153    let mapped = to_backend(MantaError::YamlError(inner));
154    assert!(matches!(mapped, BackendError::YamlError(_)));
155  }
156}