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    /// An integrity algorithm, digest, or canonicalization contract failed.
21    Integrity,
22}
23
24impl DiagnosticCategory {
25    /// Return the stable language-neutral category spelling.
26    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/// Error returned when a diagnostic code is not canonical snake case.
43#[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/// A validated stable machine-readable diagnostic code.
55#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
56pub struct DiagnosticCode(String);
57
58impl DiagnosticCode {
59    /// Validate and construct one code.
60    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    /// Return the canonical code spelling.
74    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/// One typed segment in a contract diagnostic path.
104#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
105#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
106pub enum DiagnosticPathSegment {
107    /// An object field.
108    Field(String),
109    /// A zero-based collection index.
110    Index(u64),
111    /// A typed identifier rendered for diagnostics.
112    Identifier(String),
113}
114
115/// A typed path into one canonical contract value.
116#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
117#[serde(transparent)]
118pub struct DiagnosticPath(Vec<DiagnosticPathSegment>);
119
120impl DiagnosticPath {
121    /// Construct an empty root path.
122    pub const fn new() -> Self {
123        Self(Vec::new())
124    }
125    /// Construct a path from typed segments.
126    pub fn from_segments(segments: impl IntoIterator<Item = DiagnosticPathSegment>) -> Self {
127        Self(segments.into_iter().collect())
128    }
129    /// Return the ordered segments.
130    pub fn segments(&self) -> &[DiagnosticPathSegment] {
131        &self.0
132    }
133    /// Append one segment.
134    pub fn push(&mut self, segment: DiagnosticPathSegment) {
135        self.0.push(segment);
136    }
137}
138
139/// A deterministic typed diagnostic detail value.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum DiagnosticDetailValue {
142    /// Textual context.
143    Text(String),
144    /// A signed integer encoded as a decimal string for binding safety.
145    Long(i64),
146    /// A boolean fact.
147    Boolean(bool),
148    /// An ordered list of text values.
149    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/// One stable structured contract failure.
223#[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    /// Construct a diagnostic from a validated code.
234    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    /// Construct an implementation-owned diagnostic with a static valid code.
249    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    /// Attach a complete typed path.
262    pub fn with_path(mut self, path: DiagnosticPath) -> Self {
263        self.path = path;
264        self
265    }
266    /// Append one path segment.
267    pub fn at(mut self, segment: DiagnosticPathSegment) -> Self {
268        self.path.push(segment);
269        self
270    }
271    /// Attach one deterministic detail.
272    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    /// Return the stable category.
281    pub const fn category(&self) -> DiagnosticCategory {
282        self.category
283    }
284    /// Return the stable code.
285    pub fn code(&self) -> &DiagnosticCode {
286        &self.code
287    }
288    /// Return the human-readable message.
289    pub fn message(&self) -> &str {
290        &self.message
291    }
292    /// Return the typed path.
293    pub fn path(&self) -> &DiagnosticPath {
294        &self.path
295    }
296    /// Return deterministic details.
297    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 {}