Skip to main content

acdp_primitives/
wire_error.rs

1//! Wire error envelope returned by the registry on all error responses.
2//!
3//! Lives in `acdp-primitives` (rather than alongside the other wire types)
4//! because [`crate::error::AcdpError`] references it in its `Registry`
5//! variant and in [`crate::error::AcdpError::from_wire_error`] — and
6//! `AcdpError` is the foundational error type every higher crate depends
7//! on.
8
9use serde::{Deserialize, Serialize};
10
11/// Wire error envelope returned by the registry on all error responses.
12///
13/// Code values match the ACDP error registry (RFC-ACDP-0007 §5).
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct WireError {
17    pub error: WireErrorBody,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct WireErrorBody {
23    /// Error code from the ACDP error registry.
24    pub code: String,
25    /// Human-readable message.
26    pub message: String,
27    /// Machine-readable details (e.g. `{"reason": "lineage_mismatch"}`).
28    ///
29    /// Optional in `acdp-error.schema.json` and, when present, a JSON
30    /// object (`"type": "object"`) — not nullable. `de_present_object`
31    /// rejects an explicit `"details": null` and any non-object value.
32    #[serde(
33        default,
34        skip_serializing_if = "Option::is_none",
35        deserialize_with = "crate::serde_helpers::de_present_object"
36    )]
37    pub details: Option<serde_json::Value>,
38}
39
40impl WireErrorBody {
41    /// Typed accessor for `details.reason` on `superseded_target` errors.
42    pub fn supersession_reason(&self) -> Option<crate::error::SupersessionReason> {
43        self.details
44            .as_ref()
45            .and_then(|d| d.get("reason"))
46            .and_then(|v| serde_json::from_value(v.clone()).ok())
47    }
48
49    /// `details.unreachable_ctx_id` (set on `lineage_walk_failed`).
50    pub fn unreachable_ctx_id(&self) -> Option<&str> {
51        self.details
52            .as_ref()
53            .and_then(|d| d.get("unreachable_ctx_id"))
54            .and_then(|v| v.as_str())
55    }
56
57    /// `details.idempotency_key` (set on `duplicate_publish`).
58    pub fn idempotency_key(&self) -> Option<&str> {
59        self.details
60            .as_ref()
61            .and_then(|d| d.get("idempotency_key"))
62            .and_then(|v| v.as_str())
63    }
64
65    /// `details.original_ctx_id` (set on `duplicate_publish`).
66    pub fn original_ctx_id(&self) -> Option<&str> {
67        self.details
68            .as_ref()
69            .and_then(|d| d.get("original_ctx_id"))
70            .and_then(|v| v.as_str())
71    }
72}
73
74impl std::fmt::Display for WireError {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "{}: {}", self.error.code, self.error.message)
77    }
78}