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}