manta_server/
wire_conv.rs

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