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