Skip to main content

laminar_connectors/fpml/
error.rs

1//! Typed, bounded document rejections.
2
3use std::fmt;
4
5/// The decoding stage that rejected a document. Used as a bounded metrics label.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum Stage {
8    /// Encoding, DTD, processing-instruction and size checks before libxml2 sees the bytes.
9    Prescan,
10    /// XML well-formedness and namespaces (libxml2 parser).
11    Parse,
12    /// XML Schema validation against the pinned closure.
13    Schema,
14    /// Root element, view namespace, and `fpmlVersion` checks.
15    Root,
16    /// `id` uniqueness, `href` resolution, and reference target types.
17    References,
18    /// Conversion into the profile's Arrow contract.
19    Projection,
20    /// The validator worker stopped or failed internally.
21    Worker,
22}
23
24impl Stage {
25    /// Stable lower-case label.
26    #[must_use]
27    pub fn label(self) -> &'static str {
28        match self {
29            Self::Prescan => "prescan",
30            Self::Parse => "parse",
31            Self::Schema => "schema",
32            Self::Root => "root",
33            Self::References => "references",
34            Self::Projection => "projection",
35            Self::Worker => "worker",
36        }
37    }
38}
39
40impl fmt::Display for Stage {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        f.write_str(self.label())
43    }
44}
45
46/// Longest rejection detail kept; diagnostics never carry whole documents.
47const MAX_DETAIL: usize = 1024;
48
49/// Why one document was rejected. The detail is bounded and never contains raw document text
50/// beyond schema-vocabulary names and line numbers.
51#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
52#[error("FpML document rejected at {stage}: {detail}")]
53pub struct Rejection {
54    /// Stage that rejected the document.
55    pub stage: Stage,
56    /// Bounded human-readable reason.
57    pub detail: String,
58}
59
60impl Rejection {
61    pub(crate) fn new(stage: Stage, detail: impl Into<String>) -> Self {
62        let mut detail = detail.into();
63        if detail.len() > MAX_DETAIL {
64            let mut end = MAX_DETAIL;
65            while !detail.is_char_boundary(end) {
66                end -= 1;
67            }
68            detail.truncate(end);
69            detail.push('…');
70        }
71        Self { stage, detail }
72    }
73}