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}