1use std::collections::BTreeMap;
4use std::error::Error;
5use std::fmt;
6
7use serde::de::Error as _;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum DiagnosticCategory {
14 InvalidContract,
16 UnsupportedCapability,
18 ResourceLimit,
20 Cancelled,
22 Integrity,
24}
25
26impl DiagnosticCategory {
27 pub const fn as_str(self) -> &'static str {
29 match self {
30 Self::InvalidContract => "invalid_contract",
31 Self::UnsupportedCapability => "unsupported_capability",
32 Self::ResourceLimit => "resource_limit",
33 Self::Cancelled => "cancelled",
34 Self::Integrity => "integrity",
35 }
36 }
37}
38
39impl fmt::Display for DiagnosticCategory {
40 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41 formatter.write_str(self.as_str())
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct DiagnosticCodeError;
48
49impl fmt::Display for DiagnosticCodeError {
50 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51 formatter.write_str("diagnostic code must be 1-128 lowercase snake-case bytes")
52 }
53}
54
55impl Error for DiagnosticCodeError {}
56
57#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub struct DiagnosticCode(String);
60
61impl DiagnosticCode {
62 pub fn new(value: impl Into<String>) -> Result<Self, DiagnosticCodeError> {
64 let value = value.into();
65 let mut bytes = value.bytes();
66 let valid = value.len() <= 128
67 && bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
68 && bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_');
69 if valid {
70 Ok(Self(value))
71 } else {
72 Err(DiagnosticCodeError)
73 }
74 }
75
76 pub fn as_str(&self) -> &str {
78 &self.0
79 }
80}
81
82impl fmt::Display for DiagnosticCode {
83 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84 formatter.write_str(self.as_str())
85 }
86}
87
88impl Serialize for DiagnosticCode {
89 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
90 where
91 S: Serializer,
92 {
93 serializer.serialize_str(self.as_str())
94 }
95}
96
97impl<'de> Deserialize<'de> for DiagnosticCode {
98 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
99 where
100 D: Deserializer<'de>,
101 {
102 Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
108#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
109pub enum DiagnosticPathSegment {
110 Field(String),
112 Index(u64),
114 Identifier(String),
116}
117
118#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
120#[serde(transparent)]
121pub struct DiagnosticPath(Vec<DiagnosticPathSegment>);
122
123impl DiagnosticPath {
124 pub const fn new() -> Self {
126 Self(Vec::new())
127 }
128 pub fn from_segments(segments: impl IntoIterator<Item = DiagnosticPathSegment>) -> Self {
130 Self(segments.into_iter().collect())
131 }
132 pub fn segments(&self) -> &[DiagnosticPathSegment] {
134 &self.0
135 }
136 pub fn push(&mut self, segment: DiagnosticPathSegment) {
138 self.0.push(segment);
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
144pub enum DiagnosticDetailValue {
145 Text(String),
147 Long(i64),
149 Boolean(bool),
151 TextList(Vec<String>),
153}
154
155#[derive(Serialize, Deserialize)]
156#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
157enum DetailWire {
158 Text(String),
159 Long(String),
160 Boolean(bool),
161 TextList(Vec<String>),
162}
163
164impl Serialize for DiagnosticDetailValue {
165 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
166 where
167 S: Serializer,
168 {
169 let wire = match self {
170 Self::Text(value) => DetailWire::Text(value.clone()),
171 Self::Long(value) => DetailWire::Long(value.to_string()),
172 Self::Boolean(value) => DetailWire::Boolean(*value),
173 Self::TextList(value) => DetailWire::TextList(value.clone()),
174 };
175 wire.serialize(serializer)
176 }
177}
178
179impl<'de> Deserialize<'de> for DiagnosticDetailValue {
180 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
181 where
182 D: Deserializer<'de>,
183 {
184 match DetailWire::deserialize(deserializer)? {
185 DetailWire::Text(value) => Ok(Self::Text(value)),
186 DetailWire::Boolean(value) => Ok(Self::Boolean(value)),
187 DetailWire::TextList(value) => Ok(Self::TextList(value)),
188 DetailWire::Long(value) => {
189 let parsed = value.parse::<i64>().map_err(D::Error::custom)?;
190 if parsed.to_string() != value {
191 return Err(D::Error::custom("diagnostic long is not canonical"));
192 }
193 Ok(Self::Long(parsed))
194 }
195 }
196 }
197}
198
199impl From<&str> for DiagnosticDetailValue {
200 fn from(value: &str) -> Self {
201 Self::Text(value.to_owned())
202 }
203}
204impl From<String> for DiagnosticDetailValue {
205 fn from(value: String) -> Self {
206 Self::Text(value)
207 }
208}
209impl From<i64> for DiagnosticDetailValue {
210 fn from(value: i64) -> Self {
211 Self::Long(value)
212 }
213}
214impl From<bool> for DiagnosticDetailValue {
215 fn from(value: bool) -> Self {
216 Self::Boolean(value)
217 }
218}
219impl From<Vec<String>> for DiagnosticDetailValue {
220 fn from(value: Vec<String>) -> Self {
221 Self::TextList(value)
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227pub struct Diagnostic {
228 category: DiagnosticCategory,
229 code: DiagnosticCode,
230 message: String,
231 path: DiagnosticPath,
232 details: BTreeMap<String, DiagnosticDetailValue>,
233}
234
235impl Diagnostic {
236 pub fn new(
238 category: DiagnosticCategory,
239 code: DiagnosticCode,
240 message: impl Into<String>,
241 ) -> Self {
242 Self {
243 category,
244 code,
245 message: message.into(),
246 path: DiagnosticPath::new(),
247 details: BTreeMap::new(),
248 }
249 }
250
251 pub(crate) fn stable(
253 category: DiagnosticCategory,
254 code: &'static str,
255 message: &'static str,
256 ) -> Self {
257 Self::new(
258 category,
259 DiagnosticCode::new(code).expect("static diagnostic code is valid"),
260 message,
261 )
262 }
263
264 pub fn with_path(mut self, path: DiagnosticPath) -> Self {
266 self.path = path;
267 self
268 }
269 pub fn at(mut self, segment: DiagnosticPathSegment) -> Self {
271 self.path.push(segment);
272 self
273 }
274 pub fn with_detail(
276 mut self,
277 key: impl Into<String>,
278 value: impl Into<DiagnosticDetailValue>,
279 ) -> Self {
280 self.details.insert(key.into(), value.into());
281 self
282 }
283 pub const fn category(&self) -> DiagnosticCategory {
285 self.category
286 }
287 pub fn code(&self) -> &DiagnosticCode {
289 &self.code
290 }
291 pub fn message(&self) -> &str {
293 &self.message
294 }
295 pub fn path(&self) -> &DiagnosticPath {
297 &self.path
298 }
299 pub fn details(&self) -> &BTreeMap<String, DiagnosticDetailValue> {
301 &self.details
302 }
303}
304
305impl fmt::Display for Diagnostic {
306 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
307 write!(
308 formatter,
309 "{} [{}]: {}",
310 self.category, self.code, self.message
311 )
312 }
313}
314
315impl Error for Diagnostic {}