hive_router_plan_executor/execution/
error.rs

1use strum::IntoStaticStr;
2
3use crate::{
4    headers::errors::HeaderRuleRuntimeError, projection::error::ProjectionError,
5    response::graphql_error::GraphQLError,
6};
7
8#[derive(thiserror::Error, Debug, Clone, IntoStaticStr)]
9pub enum PlanExecutionErrorKind {
10    #[error("Projection failure: {0}")]
11    #[strum(serialize = "PROJECTION_FAILURE")]
12    ProjectionFailure(#[from] ProjectionError),
13
14    #[error(transparent)]
15    #[strum(serialize = "HEADER_PROPAGATION_FAILURE")]
16    HeaderPropagation(#[from] HeaderRuleRuntimeError),
17}
18
19/// The central error type for all query plan execution failures.
20///
21/// This struct combines a specific `PlanExecutionErrorKind` with a
22/// `PlanExecutionErrorContext` that holds shared, dynamic information
23/// like the subgraph name or affected GraphQL path.
24#[derive(thiserror::Error, Debug, Clone)]
25#[error("{kind}")]
26pub struct PlanExecutionError {
27    #[source]
28    kind: PlanExecutionErrorKind,
29    context: PlanExecutionErrorContext,
30}
31
32#[derive(Debug, Clone)]
33pub struct PlanExecutionErrorContext {
34    subgraph_name: Option<String>,
35    affected_path: Option<String>,
36}
37
38pub struct LazyPlanContext<SN, AP> {
39    pub subgraph_name: SN,
40    pub affected_path: AP,
41}
42
43impl PlanExecutionError {
44    pub(crate) fn new<SN, AP>(
45        kind: PlanExecutionErrorKind,
46        lazy_context: LazyPlanContext<SN, AP>,
47    ) -> Self
48    where
49        SN: FnOnce() -> Option<String>,
50        AP: FnOnce() -> Option<String>,
51    {
52        Self {
53            kind,
54            context: PlanExecutionErrorContext {
55                subgraph_name: (lazy_context.subgraph_name)(),
56                affected_path: (lazy_context.affected_path)(),
57            },
58        }
59    }
60
61    pub fn error_code(&self) -> &'static str {
62        (&self.kind).into()
63    }
64
65    pub fn subgraph_name(&self) -> &Option<String> {
66        &self.context.subgraph_name
67    }
68
69    pub fn affected_path(&self) -> &Option<String> {
70        &self.context.affected_path
71    }
72}
73
74impl From<PlanExecutionError> for GraphQLError {
75    fn from(val: PlanExecutionError) -> Self {
76        let mut error = GraphQLError::from_message_and_code(val.to_string(), val.error_code());
77
78        // We destructure the context to take ownership of the Option<String> values.
79        // Then we move owned Strings directly into builder methods.
80        // This way we avoid cloning through Into<String> in those methods.
81
82        if let Some(subgraph_name) = val.context.subgraph_name {
83            error = error.add_subgraph_name(subgraph_name);
84        }
85        if let Some(affected_path) = val.context.affected_path {
86            error = error.add_affected_path(affected_path);
87        }
88        error
89    }
90}
91
92/// An extension trait for `Result` types that can be converted into a `PlanExecutionError`.
93///
94/// This trait provides a lazy, performant way to add contextual information to
95/// an error, only performing work (like cloning strings) if the `Result` is an `Err`.
96pub trait IntoPlanExecutionError<T> {
97    fn with_plan_context<SN, AP>(
98        self,
99        context: LazyPlanContext<SN, AP>,
100    ) -> Result<T, PlanExecutionError>
101    where
102        SN: FnOnce() -> Option<String>,
103        AP: FnOnce() -> Option<String>;
104}
105
106impl<T> IntoPlanExecutionError<T> for Result<T, ProjectionError> {
107    fn with_plan_context<SN, AP>(
108        self,
109        context: LazyPlanContext<SN, AP>,
110    ) -> Result<T, PlanExecutionError>
111    where
112        SN: FnOnce() -> Option<String>,
113        AP: FnOnce() -> Option<String>,
114    {
115        self.map_err(|source| {
116            let kind = PlanExecutionErrorKind::ProjectionFailure(source);
117            PlanExecutionError::new(kind, context)
118        })
119    }
120}
121
122impl<T> IntoPlanExecutionError<T> for Result<T, HeaderRuleRuntimeError> {
123    fn with_plan_context<SN, AP>(
124        self,
125        context: LazyPlanContext<SN, AP>,
126    ) -> Result<T, PlanExecutionError>
127    where
128        SN: FnOnce() -> Option<String>,
129        AP: FnOnce() -> Option<String>,
130    {
131        self.map_err(|source| {
132            let kind = PlanExecutionErrorKind::HeaderPropagation(source);
133            PlanExecutionError::new(kind, context)
134        })
135    }
136}