Skip to main content

omena_query/
sdk_error.rs

1use crate::{
2    OmenaErrorClassV0, OmenaErrorContextV0, OmenaErrorEvidenceReferenceV0,
3    OmenaErrorRecoverabilityV0, OmenaErrorSeverityV0, OmenaErrorV0,
4};
5use omena_evidence_graph::{EvidenceGraphV0, EvidenceNodeKeyV0};
6use std::fmt;
7
8pub type OmenaError = OmenaErrorV0;
9
10impl OmenaErrorV0 {
11    pub fn new(
12        class: OmenaErrorClassV0,
13        message: impl Into<String>,
14        context: OmenaErrorContextV0,
15    ) -> Self {
16        Self {
17            class,
18            message: message.into(),
19            context,
20        }
21    }
22
23    pub fn unknown(message: impl Into<String>, code: impl Into<String>) -> Self {
24        Self::new(
25            OmenaErrorClassV0::Unknown,
26            message,
27            OmenaErrorContextV0 {
28                code: code.into(),
29                severity: OmenaErrorSeverityV0::Error,
30                recoverability: OmenaErrorRecoverabilityV0::UserAction,
31                evidence: Vec::new(),
32            },
33        )
34    }
35
36    pub fn with_evidence_graph_nodes(
37        mut self,
38        graph: &EvidenceGraphV0,
39        keys: impl IntoIterator<Item = EvidenceNodeKeyV0>,
40    ) -> Result<Self, OmenaErrorEvidenceBindingErrorV0> {
41        let mut keys = keys.into_iter().collect::<Vec<_>>();
42        keys.sort();
43        keys.dedup();
44        for key in &keys {
45            if !graph.nodes.iter().any(|node| &node.key == key) {
46                return Err(OmenaErrorEvidenceBindingErrorV0::MissingNode(key.clone()));
47            }
48        }
49        self.context.evidence = keys
50            .into_iter()
51            .map(|key| OmenaErrorEvidenceReferenceV0 {
52                query_identity: key.query_identity,
53                input_identity: key.input_identity,
54            })
55            .collect();
56        Ok(self)
57    }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum OmenaErrorEvidenceBindingErrorV0 {
62    MissingNode(EvidenceNodeKeyV0),
63}
64
65impl fmt::Display for OmenaErrorEvidenceBindingErrorV0 {
66    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            Self::MissingNode(key) => write!(
69                formatter,
70                "evidence graph does not contain query '{}' input '{}'",
71                key.query_identity, key.input_identity
72            ),
73        }
74    }
75}
76
77impl std::error::Error for OmenaErrorEvidenceBindingErrorV0 {}
78
79impl fmt::Display for OmenaErrorV0 {
80    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81        write!(formatter, "{}: {}", self.context.code, self.message)
82    }
83}
84
85impl std::error::Error for OmenaErrorV0 {}
86
87pub fn omena_error_from_boundary_encoding(
88    kind: &str,
89    message: impl Into<String>,
90    operation: &str,
91) -> OmenaError {
92    let (class, recoverability) = match kind {
93        "parse-error" => (
94            OmenaErrorClassV0::Input,
95            OmenaErrorRecoverabilityV0::UserAction,
96        ),
97        "serialize-error" => (
98            OmenaErrorClassV0::Internal,
99            OmenaErrorRecoverabilityV0::Retry,
100        ),
101        "unsupported-mode" => (
102            OmenaErrorClassV0::Unsupported,
103            OmenaErrorRecoverabilityV0::UserAction,
104        ),
105        _ => (
106            OmenaErrorClassV0::Unknown,
107            OmenaErrorRecoverabilityV0::UserAction,
108        ),
109    };
110    OmenaErrorV0::new(
111        class,
112        message,
113        OmenaErrorContextV0 {
114            code: format!("boundary.{operation}.{kind}"),
115            severity: OmenaErrorSeverityV0::Error,
116            recoverability,
117            evidence: Vec::new(),
118        },
119    )
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use omena_evidence_graph::{
126        EvidenceDemandEdgeV0, EvidenceNodeSeedV0, GuaranteeKindV0,
127        build_evidence_graph_from_edges_v0,
128    };
129
130    #[test]
131    fn boundary_error_encodings_map_to_unified_classes() {
132        assert_eq!(
133            omena_error_from_boundary_encoding("parse-error", "bad input", "build").class,
134            OmenaErrorClassV0::Input
135        );
136        assert_eq!(
137            omena_error_from_boundary_encoding("serialize-error", "encoding", "check").class,
138            OmenaErrorClassV0::Internal
139        );
140        assert_eq!(
141            omena_error_from_boundary_encoding("unsupported-mode", "mode", "build").class,
142            OmenaErrorClassV0::Unsupported
143        );
144        assert_eq!(
145            omena_error_from_boundary_encoding("future-kind", "unknown", "query").class,
146            OmenaErrorClassV0::Unknown
147        );
148    }
149
150    #[test]
151    fn evidence_binding_requires_a_graph_node_and_is_absent_by_default() -> Result<(), String> {
152        let key = EvidenceNodeKeyV0::new("sdkDiagnostics", "src/card.css");
153        let graph = build_evidence_graph_from_edges_v0(
154            [EvidenceNodeSeedV0::new(
155                key.clone(),
156                vec!["fixture".to_string()],
157                GuaranteeKindV0::for_label_less_family(),
158            )],
159            [EvidenceDemandEdgeV0::new(
160                "sdkError",
161                key.clone(),
162                "supportsErrorContext",
163            )],
164        )
165        .map_err(|error| format!("evidence graph fixture must be valid: {error:?}"))?;
166        let error = OmenaError::unknown("analysis failed", "analysis.failed");
167        let default_json = serde_json::to_value(&error)
168            .map_err(|error| format!("default error must serialize: {error}"))?;
169        assert!(default_json["context"].get("evidence").is_none());
170
171        let bound = error
172            .with_evidence_graph_nodes(&graph, [key.clone()])
173            .map_err(|error| format!("known evidence node must bind: {error}"))?;
174        let bound_json = serde_json::to_value(bound)
175            .map_err(|error| format!("bound error must serialize: {error}"))?;
176        assert_eq!(
177            bound_json["context"]["evidence"][0],
178            serde_json::json!({
179                "queryIdentity": "sdkDiagnostics",
180                "inputIdentity": "src/card.css",
181            }),
182        );
183
184        let missing = OmenaError::unknown("analysis failed", "analysis.failed")
185            .with_evidence_graph_nodes(
186                &graph,
187                [EvidenceNodeKeyV0::new("sdkDiagnostics", "src/missing.css")],
188            );
189        assert!(matches!(
190            missing,
191            Err(OmenaErrorEvidenceBindingErrorV0::MissingNode(_))
192        ));
193        Ok(())
194    }
195}