1use std::error::Error;
4
5use crate::GfError;
6
7impl GfError {
8 #[must_use]
14 pub fn from_plan_error(error: impl Error + 'static) -> Self {
15 Self::recover_source(&error, false).unwrap_or_else(|| Self::Plan(error.to_string()))
16 }
17
18 #[must_use]
25 pub fn from_execution_error(error: impl Error + 'static) -> Self {
26 Self::recover_source(&error, true).unwrap_or_else(|| Self::Execution(error.to_string()))
27 }
28
29 fn recover_source(error: &(dyn Error + 'static), execution: bool) -> Option<Self> {
30 let mut source = Some(error);
31 while let Some(error) = source {
32 if let Some(original) = error.downcast_ref::<Self>() {
33 return Some(original.clone());
34 }
35 if let Some(original) = error.downcast_ref::<crate::LoweringError>() {
36 return Some(if execution {
37 Self::LoweringExecution(original.clone())
38 } else {
39 Self::Lowering(original.clone())
40 });
41 }
42 if let Some(original) = error.downcast_ref::<crate::AlgorithmError>() {
43 return Some(Self::Algorithm(original.clone()));
44 }
45 source = error.source();
46 }
47 None
48 }
49}
50
51impl From<crate::ParseError> for GfError {
52 fn from(error: crate::ParseError) -> Self {
53 if let crate::ParseErrorKind::UnsupportedFeature(feature) = error.kind {
54 return Self::NotImplemented(feature.description());
55 }
56 Self::Parse {
57 msg: error.message.clone(),
58 span: error.span,
59 diagnostic: Some(Box::new(error)),
60 }
61 }
62}
63
64impl From<crate::LoweringError> for GfError {
65 fn from(error: crate::LoweringError) -> Self {
66 Self::Lowering(error)
67 }
68}
69
70impl From<crate::AlgorithmError> for GfError {
71 fn from(error: crate::AlgorithmError) -> Self {
72 Self::Algorithm(error)
73 }
74}
75
76impl GfError {
77 #[must_use]
79 pub fn from_parse_display(error: crate::ParseError) -> Self {
80 if let crate::ParseErrorKind::UnsupportedFeature(feature) = error.kind {
81 return Self::NotImplemented(feature.description());
82 }
83 Self::Parse {
84 msg: error.to_string(),
85 span: error.span,
86 diagnostic: Some(Box::new(error)),
87 }
88 }
89
90 #[must_use]
92 pub fn from_bind_plan_errors(errors: &[crate::BindError]) -> Self {
93 if errors
94 .iter()
95 .all(|error| matches!(error.kind, crate::BindErrorKind::UnsupportedFeature(_)))
96 && let Some(crate::BindError {
97 kind: crate::BindErrorKind::UnsupportedFeature(feature),
98 ..
99 }) = errors.first()
100 {
101 return Self::NotImplemented(feature.description());
102 }
103 Self::BindPlan {
104 msg: format!(
105 "bind errors: {}",
106 errors
107 .iter()
108 .map(|e| e.message.as_str())
109 .collect::<Vec<_>>()
110 .join("; ")
111 ),
112 diagnostics: errors.to_vec(),
113 }
114 }
115
116 #[must_use]
118 pub fn from_bind_errors(errors: &[crate::BindError]) -> Self {
119 if errors
120 .iter()
121 .all(|error| matches!(error.kind, crate::BindErrorKind::UnsupportedFeature(_)))
122 && let Some(crate::BindError {
123 kind: crate::BindErrorKind::UnsupportedFeature(feature),
124 ..
125 }) = errors.first()
126 {
127 return Self::NotImplemented(feature.description());
128 }
129 if let Some(error) = errors
131 .iter()
132 .filter(|error| {
133 error.kind == crate::BindErrorKind::InvalidArgument
134 && error.message.starts_with("typed UUID parameter `$")
135 })
136 .min_by_key(|error| (error.span.start, error.message.as_str()))
137 {
138 return Self::BindValidation {
139 msg: error.message.clone(),
140 diagnostics: errors.to_vec(),
141 };
142 }
143 Self::Bind {
144 msg: errors
145 .iter()
146 .map(|e| e.message.as_str())
147 .collect::<Vec<_>>()
148 .join("; "),
149 span: errors.first().map_or(crate::Span::default(), |e| e.span),
150 diagnostics: errors.to_vec(),
151 }
152 }
153}
154
155#[cfg(test)]
156mod stage_tests {
157 use super::*;
158 use crate::{BindError, BindErrorKind, LoweringError, ParseError, ParseErrorKind, Span};
159
160 #[test]
161 fn parser_kinds_payloads_and_messages_survive_both_presentations() {
162 let kinds = [
163 ParseErrorKind::UnexpectedChar,
164 ParseErrorKind::UnexpectedToken {
165 found: "token".into(),
166 expected: vec!["expression".into()],
167 },
168 ParseErrorKind::UnterminatedString,
169 ParseErrorKind::UnterminatedBlockComment,
170 ParseErrorKind::InvalidNumericLiteral,
171 ParseErrorKind::InvalidParameter,
172 ParseErrorKind::UnexpectedEof {
173 expected: vec!["expression".into()],
174 },
175 ];
176 for kind in kinds {
177 let original = ParseError::new(kind, Span::new(7, 13), "detailed source diagnostic");
178 for display in [false, true] {
179 let error = if display {
180 GfError::from_parse_display(original.clone())
181 } else {
182 original.clone().into()
183 };
184 assert_eq!(error.code(), "GF_PARSE");
185 let GfError::Parse {
186 diagnostic,
187 msg,
188 span,
189 } = error
190 else {
191 panic!("parser variant lost")
192 };
193 assert_eq!(*diagnostic.expect("typed parser diagnostic"), original);
194 assert_eq!(span, original.span);
195 assert_eq!(
196 msg,
197 if display {
198 original.to_string()
199 } else {
200 original.message.clone()
201 }
202 );
203 }
204 }
205 }
206
207 #[test]
208 fn every_binder_kind_and_span_survives_in_reported_order() {
209 use BindErrorKind as K;
210 let kinds = [
211 K::UnknownLabel,
212 K::UnknownRelationType,
213 K::UnknownProperty,
214 K::UndeclaredVariable,
215 K::DuplicateVariable,
216 K::AmbiguousProperty,
217 K::UnsupportedClause,
218 K::InvalidDeleteTarget,
219 K::VariableKindConflict,
220 K::VariableAlreadyBound,
221 K::InvalidArgument,
222 K::AmbiguousComposedSymbol,
223 K::CompositionConflict,
224 ];
225 let original: Vec<_> = kinds
226 .into_iter()
227 .enumerate()
228 .map(|(i, kind)| {
229 BindError::new(kind, Span::new(i * 2, i * 2 + 1), format!("diagnostic {i}"))
230 })
231 .collect();
232 let error = GfError::from_bind_errors(&original);
233 assert_eq!(error.code(), "GF_PARSE");
234 let GfError::Bind {
235 diagnostics,
236 msg,
237 span,
238 } = error
239 else {
240 panic!("binder variant lost")
241 };
242 assert_eq!(diagnostics, original);
243 assert_eq!(span, original[0].span);
244 assert_eq!(
245 msg,
246 original
247 .iter()
248 .map(|e| e.message.as_str())
249 .collect::<Vec<_>>()
250 .join("; ")
251 );
252 }
253
254 #[test]
255 fn uuid_validation_keeps_all_binder_diagnostics() {
256 let original = vec![
257 BindError::new(
258 BindErrorKind::UnknownLabel,
259 Span::new(0, 2),
260 "unknown label",
261 ),
262 BindError::new(
263 BindErrorKind::InvalidArgument,
264 Span::new(4, 9),
265 "typed UUID parameter `$id` is only supported as a direct node_uuid or edge_uuid identity equality predicate",
266 ),
267 ];
268 let error = GfError::from_bind_errors(&original);
269 assert_eq!(error.code(), "GF_VALIDATION");
270 let GfError::BindValidation { diagnostics, msg } = error else {
271 panic!("validation classification lost")
272 };
273 assert_eq!(diagnostics, original);
274 assert_eq!(msg, original[1].message);
275 }
276
277 #[test]
278 fn lowering_kind_and_runtime_domain_are_preserved() {
279 for original in [
280 LoweringError::UnknownFunction("fn".into()),
281 LoweringError::UnsupportedExpr("shape".into()),
282 LoweringError::UnboundVar(42),
283 LoweringError::InvalidType("predicate".into()),
284 ] {
285 let expected = if matches!(original, LoweringError::InvalidType(_)) {
286 "GF_VALIDATION"
287 } else {
288 "GF_PLAN"
289 };
290 let planning = GfError::from_plan_error(original.clone());
291 assert_eq!(planning.code(), expected);
292 assert!(matches!(planning, GfError::Lowering(ref e) if e == &original));
293 let execution = GfError::from_execution_error(original.clone());
294 assert_eq!(execution.code(), "GF_EXECUTION");
295 assert!(matches!(execution, GfError::LoweringExecution(ref e) if e == &original));
296 }
297 }
298}