agent_first_data/
error_catalog.rs1use crate::{BuildError, ErrorBuilder, Event, json_error};
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10use std::fmt;
11
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub struct ErrorSpec {
15 code: String,
16 message: String,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 hint: Option<String>,
19 #[serde(default)]
20 retryable: bool,
21}
22
23impl ErrorSpec {
24 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
26 Self {
27 code: code.into(),
28 message: message.into(),
29 hint: None,
30 retryable: false,
31 }
32 }
33
34 pub fn hint(mut self, hint: impl Into<String>) -> Self {
36 let hint = hint.into();
37 self.hint = (!hint.is_empty()).then_some(hint);
38 self
39 }
40
41 pub const fn retryable(mut self, retryable: bool) -> Self {
43 self.retryable = retryable;
44 self
45 }
46
47 pub fn code(&self) -> &str {
48 &self.code
49 }
50
51 pub fn message(&self) -> &str {
52 &self.message
53 }
54
55 pub fn hint_value(&self) -> Option<&str> {
56 self.hint.as_deref()
57 }
58
59 pub const fn is_retryable(&self) -> bool {
60 self.retryable
61 }
62
63 pub fn builder(&self) -> ErrorBuilder {
68 json_error(&self.code, &self.message)
69 .hint_if_some(self.hint.as_deref())
70 .retryable_if(self.retryable)
71 }
72
73 pub fn event(&self) -> Result<Event, BuildError> {
75 self.builder().build()
76 }
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
81pub enum ErrorCatalogError {
82 DuplicateCode(String),
83 UnknownCode(String),
84 InvalidSpec { code: String, source: BuildError },
85}
86
87impl fmt::Display for ErrorCatalogError {
88 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89 match self {
90 Self::DuplicateCode(code) => write!(formatter, "duplicate error code {code:?}"),
91 Self::UnknownCode(code) => write!(formatter, "unknown error code {code:?}"),
92 Self::InvalidSpec { code, source } => {
93 write!(formatter, "invalid error spec {code:?}: {source}")
94 }
95 }
96 }
97}
98
99impl std::error::Error for ErrorCatalogError {
100 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
101 match self {
102 Self::InvalidSpec { source, .. } => Some(source),
103 Self::DuplicateCode(_) | Self::UnknownCode(_) => None,
104 }
105 }
106}
107
108#[derive(Clone, Debug, Default)]
110pub struct ErrorCatalog {
111 specs: BTreeMap<String, ErrorSpec>,
112}
113
114impl ErrorCatalog {
115 pub fn new() -> Self {
116 Self::default()
117 }
118
119 pub fn from_specs<I>(specs: I) -> Result<Self, ErrorCatalogError>
121 where
122 I: IntoIterator<Item = ErrorSpec>,
123 {
124 let mut catalog = Self::new();
125 for spec in specs {
126 catalog.insert(spec)?;
127 }
128 Ok(catalog)
129 }
130
131 pub fn insert(&mut self, spec: ErrorSpec) -> Result<(), ErrorCatalogError> {
133 let code = spec.code.clone();
134 if self.specs.contains_key(&code) {
135 return Err(ErrorCatalogError::DuplicateCode(code));
136 }
137 spec.event()
138 .map_err(|source| ErrorCatalogError::InvalidSpec {
139 code: code.clone(),
140 source,
141 })?;
142 self.specs.insert(code, spec);
143 Ok(())
144 }
145
146 pub fn get(&self, code: &str) -> Option<&ErrorSpec> {
147 self.specs.get(code)
148 }
149
150 pub fn builder(&self, code: &str) -> Result<ErrorBuilder, ErrorCatalogError> {
152 self.get(code)
153 .map(ErrorSpec::builder)
154 .ok_or_else(|| ErrorCatalogError::UnknownCode(code.to_string()))
155 }
156
157 pub fn event(&self, code: &str) -> Result<Event, ErrorCatalogError> {
159 self.builder(code)?
160 .build()
161 .map_err(|source| ErrorCatalogError::InvalidSpec {
162 code: code.to_string(),
163 source,
164 })
165 }
166
167 pub fn len(&self) -> usize {
168 self.specs.len()
169 }
170
171 pub fn is_empty(&self) -> bool {
172 self.specs.is_empty()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::{ErrorCatalog, ErrorCatalogError, ErrorSpec};
179 use crate::validate_protocol_event;
180
181 #[test]
182 fn catalog_builds_only_declared_public_fields() {
183 let catalog = ErrorCatalog::from_specs([ErrorSpec::new(
184 "config_load_failed",
185 "Failed to load configuration",
186 )
187 .hint("inspect the configuration and retry")
188 .retryable(false)])
189 .unwrap_or_else(|error| panic!("{error}"));
190
191 let event = catalog
192 .event("config_load_failed")
193 .unwrap_or_else(|error| panic!("{error}"));
194 validate_protocol_event(event.as_value(), true).unwrap_or_else(|error| panic!("{error}"));
195 assert_eq!(
196 event.as_value()["error"]["message"],
197 "Failed to load configuration"
198 );
199 assert!(!event.to_string().contains("database diagnostic"));
200 }
201
202 #[test]
203 fn catalog_rejects_invalid_duplicate_and_unknown_codes() {
204 let invalid = ErrorCatalog::from_specs([ErrorSpec::new("", "message")]);
205 assert!(matches!(
206 invalid,
207 Err(ErrorCatalogError::InvalidSpec { .. })
208 ));
209
210 let duplicate = ErrorCatalog::from_specs([
211 ErrorSpec::new("same", "first"),
212 ErrorSpec::new("same", "second"),
213 ]);
214 assert!(matches!(
215 duplicate,
216 Err(ErrorCatalogError::DuplicateCode(code)) if code == "same"
217 ));
218
219 let empty = ErrorCatalog::new();
220 assert!(matches!(
221 empty.event("missing"),
222 Err(ErrorCatalogError::UnknownCode(code)) if code == "missing"
223 ));
224 }
225}