Skip to main content

ahri_tre_protocol/
public_error.rs

1use serde::{Deserialize, Serialize};
2
3/// Stable JSON error body used by HTTP control-plane routes.
4///
5/// The HTTP status carries transport semantics; this envelope gives browser
6/// clients a small, safe machine-readable error classification without
7/// exposing adapter, runtime, or credential details.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct HttpPublicErrorEnvelope {
11    pub ok: bool,
12    pub error: HttpPublicError,
13}
14
15impl HttpPublicErrorEnvelope {
16    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
17        Self {
18            ok: false,
19            error: HttpPublicError {
20                code: code.into(),
21                message: message.into(),
22                issues: Vec::new(),
23            },
24        }
25    }
26
27    pub fn validation(
28        code: impl Into<String>,
29        message: impl Into<String>,
30        issues: Vec<ValidationIssue>,
31    ) -> Self {
32        Self {
33            ok: false,
34            error: HttpPublicError {
35                code: code.into(),
36                message: message.into(),
37                issues,
38            },
39        }
40    }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct HttpPublicError {
46    pub code: String,
47    pub message: String,
48    #[serde(default, skip_serializing_if = "Vec::is_empty")]
49    pub issues: Vec<ValidationIssue>,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum ProtocolErrorCode {
55    ValidationFailed,
56    NotFound,
57    Conflict,
58    Unauthorized,
59    Forbidden,
60    PreconditionFailed,
61    UnsupportedProtocolVersion,
62    UnsupportedOperation,
63    OperationFailed,
64    OperationNotReady,
65    OperationResultUnavailable,
66    RateLimited,
67    Internal,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct ProtocolError {
72    #[serde(default, skip_serializing_if = "Vec::is_empty")]
73    pub adapter_observations: Vec<crate::diagnostics::DiagnosticFinding>,
74    pub code: ProtocolErrorCode,
75    pub message: String,
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub target: Option<String>,
78    #[serde(default, skip_serializing_if = "Vec::is_empty")]
79    pub issues: Vec<ValidationIssue>,
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub details: Option<ProtocolErrorDetails>,
82}
83
84impl ProtocolError {
85    pub fn new(code: ProtocolErrorCode, message: impl Into<String>) -> Self {
86        Self {
87            adapter_observations: Vec::new(),
88            code,
89            message: message.into(),
90            target: None,
91            issues: Vec::new(),
92            details: None,
93        }
94    }
95
96    pub fn validation(message: impl Into<String>, issues: Vec<ValidationIssue>) -> Self {
97        Self {
98            adapter_observations: Vec::new(),
99            code: ProtocolErrorCode::ValidationFailed,
100            message: message.into(),
101            target: None,
102            issues,
103            details: None,
104        }
105    }
106
107    pub fn with_target(mut self, target: impl Into<String>) -> Self {
108        self.target = Some(target.into());
109        self
110    }
111
112    pub fn with_details(mut self, details: ProtocolErrorDetails) -> Self {
113        self.details = Some(details);
114        self
115    }
116}
117
118impl std::fmt::Display for ProtocolError {
119    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        formatter.write_str(&self.message)
121    }
122}
123
124impl std::error::Error for ProtocolError {}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct ValidationIssue {
128    pub path: String,
129    pub code: ValidationIssueCode,
130    pub message: String,
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub details: Option<ValidationIssueDetails>,
133}
134
135impl ValidationIssue {
136    pub fn new(
137        path: impl Into<String>,
138        code: ValidationIssueCode,
139        message: impl Into<String>,
140    ) -> Self {
141        Self {
142            path: path.into(),
143            code,
144            message: message.into(),
145            details: None,
146        }
147    }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "snake_case")]
152pub enum ValidationIssueCode {
153    Required,
154    InvalidFormat,
155    InvalidValue,
156    OutOfRange,
157    TooLong,
158    TooShort,
159    TooManyItems,
160    TooFewItems,
161    Duplicate,
162    Conflict,
163    Unsupported,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(tag = "kind", rename_all = "snake_case")]
168pub enum ValidationIssueDetails {
169    Limit { limit: u64 },
170    Range { min: Option<i64>, max: Option<i64> },
171    AllowedValues { values: Vec<String> },
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(tag = "kind", rename_all = "snake_case")]
176pub enum ProtocolErrorDetails {
177    ClientInventory {
178        observations: Vec<crate::session::SessionStatusPayload>,
179    },
180    ClientContext {
181        remote_session: Box<crate::session::SessionStatusPayload>,
182        reference: crate::refs::SessionRef,
183    },
184    IdempotencyConflict,
185    PreconditionFailed {
186        precondition: String,
187    },
188    UnsupportedProtocolVersion {
189        requested: String,
190        supported: String,
191    },
192    OperationResultUnavailable {
193        availability: String,
194    },
195    RetryAfter {
196        seconds: u64,
197    },
198}