Skip to main content

silicon_browser_shared/
error.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::RequestId;
6
7/// Successful API response envelope.
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
9pub struct Envelope<T> {
10    pub data: T,
11    #[serde(default, skip_serializing_if = "Option::is_none")]
12    pub request_id: Option<RequestId>,
13}
14
15impl<T> Envelope<T> {
16    pub fn new(data: T) -> Self {
17        Self { data, request_id: None }
18    }
19
20    pub fn with_request_id(data: T, request_id: impl Into<RequestId>) -> Self {
21        Self { data, request_id: Some(request_id.into()) }
22    }
23
24    pub fn map<U>(self, map: impl FnOnce(T) -> U) -> Envelope<U> {
25        Envelope { data: map(self.data), request_id: self.request_id }
26    }
27}
28
29/// Machine-readable field error embedded in an API error.
30#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
31pub struct FieldError {
32    pub field: String,
33    pub message: String,
34}
35
36/// Stable error payload. `code` intentionally remains open for future services.
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
38pub struct ApiError {
39    pub code: String,
40    pub message: String,
41    #[serde(default, skip_serializing_if = "Vec::is_empty")]
42    pub fields: Vec<FieldError>,
43    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
44    pub details: BTreeMap<String, String>,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub request_id: Option<RequestId>,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub retry_after_ms: Option<u64>,
49}
50
51/// Failed API response envelope.
52#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
53pub struct ApiErrorEnvelope {
54    pub error: ApiError,
55}
56
57impl ApiErrorEnvelope {
58    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
59        Self {
60            error: ApiError {
61                code: code.into(),
62                message: message.into(),
63                fields: Vec::new(),
64                details: BTreeMap::new(),
65                request_id: None,
66                retry_after_ms: None,
67            },
68        }
69    }
70}
71
72#[cfg(test)]
73mod api_envelope_tests {
74    use super::*;
75
76    /// Test group: wire envelopes keep request correlation while mapping data.
77    #[test]
78    fn envelope_maps_without_losing_request_id() {
79        let response = Envelope::with_request_id(4, "request-1").map(|value| value * 2);
80        assert_eq!(response.data, 8);
81        assert_eq!(response.request_id.as_deref(), Some("request-1"));
82    }
83
84    /// Test group: error wire format has one predictable top-level key.
85    #[test]
86    fn error_envelope_serializes_under_error_key() {
87        let json = serde_json::to_value(ApiErrorEnvelope::new("not_found", "missing")).unwrap();
88        assert_eq!(json["error"]["code"], "not_found");
89        assert!(json.get("data").is_none());
90    }
91}