Skip to main content

type_bridge_contract/
diagnostic.rs

1//! Stable structured diagnostics shared by contract parsers and codecs.
2
3use std::collections::BTreeMap;
4use std::error::Error;
5use std::fmt;
6
7use serde::de::Error as _;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10/// Stable high-level contract failure categories.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum DiagnosticCategory {
14    /// Contract bytes or values are malformed or internally inconsistent.
15    InvalidContract,
16    /// A required open capability is not advertised.
17    UnsupportedCapability,
18    /// A canonical structural ceiling was exceeded.
19    ResourceLimit,
20    /// Cooperative cancellation interrupted execution.
21    Cancelled,
22    /// An integrity algorithm, digest, or canonicalization contract failed.
23    Integrity,
24}
25
26impl DiagnosticCategory {
27    /// Return the stable language-neutral category spelling.
28    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/// Error returned when a diagnostic code is not canonical snake case.
46#[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/// A validated stable machine-readable diagnostic code.
58#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub struct DiagnosticCode(String);
60
61impl DiagnosticCode {
62    /// Validate and construct one code.
63    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    /// Return the canonical code spelling.
77    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/// One typed segment in a contract diagnostic path.
107#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
108#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
109pub enum DiagnosticPathSegment {
110    /// An object field.
111    Field(String),
112    /// A zero-based collection index.
113    Index(u64),
114    /// A typed identifier rendered for diagnostics.
115    Identifier(String),
116}
117
118/// A typed path into one canonical contract value.
119#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
120#[serde(transparent)]
121pub struct DiagnosticPath(Vec<DiagnosticPathSegment>);
122
123impl DiagnosticPath {
124    /// Construct an empty root path.
125    pub const fn new() -> Self {
126        Self(Vec::new())
127    }
128    /// Construct a path from typed segments.
129    pub fn from_segments(segments: impl IntoIterator<Item = DiagnosticPathSegment>) -> Self {
130        Self(segments.into_iter().collect())
131    }
132    /// Return the ordered segments.
133    pub fn segments(&self) -> &[DiagnosticPathSegment] {
134        &self.0
135    }
136    /// Append one segment.
137    pub fn push(&mut self, segment: DiagnosticPathSegment) {
138        self.0.push(segment);
139    }
140}
141
142/// A deterministic typed diagnostic detail value.
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub enum DiagnosticDetailValue {
145    /// Textual context.
146    Text(String),
147    /// A signed integer encoded as a decimal string for binding safety.
148    Long(i64),
149    /// A boolean fact.
150    Boolean(bool),
151    /// An ordered list of text values.
152    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/// One stable structured contract failure.
226#[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    /// Construct a diagnostic from a validated code.
237    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    /// Construct an implementation-owned diagnostic with a static valid code.
252    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    /// Attach a complete typed path.
265    pub fn with_path(mut self, path: DiagnosticPath) -> Self {
266        self.path = path;
267        self
268    }
269    /// Append one path segment.
270    pub fn at(mut self, segment: DiagnosticPathSegment) -> Self {
271        self.path.push(segment);
272        self
273    }
274    /// Attach one deterministic detail.
275    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    /// Return the stable category.
284    pub const fn category(&self) -> DiagnosticCategory {
285        self.category
286    }
287    /// Return the stable code.
288    pub fn code(&self) -> &DiagnosticCode {
289        &self.code
290    }
291    /// Return the human-readable message.
292    pub fn message(&self) -> &str {
293        &self.message
294    }
295    /// Return the typed path.
296    pub fn path(&self) -> &DiagnosticPath {
297        &self.path
298    }
299    /// Return deterministic details.
300    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 {}