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}