Skip to main content

sui_graphql/
error.rs

1//! Error types for the GraphQL client.
2
3use serde::Deserialize;
4use std::collections::HashMap;
5use std::fmt;
6use thiserror::Error;
7
8/// Error type for GraphQL client operations.
9#[derive(Debug, Error)]
10#[non_exhaustive]
11pub enum Error {
12    /// HTTP or network error.
13    #[error("Request error: {0}")]
14    Request(#[from] reqwest::Error),
15
16    /// Invalid URL.
17    #[error("Invalid URL: {0}")]
18    InvalidUrl(#[from] url::ParseError),
19
20    /// Failed to serialize data (e.g., BCS encoding).
21    #[error("Serialization error: {0}")]
22    Serialization(String),
23
24    /// Failed to deserialize or decode response data.
25    #[error("Deserialization error: {0}")]
26    Deserialization(String),
27
28    /// Missing expected data in response.
29    #[error("Missing expected data: {0}")]
30    MissingData(&'static str),
31}
32
33impl From<base64ct::Error> for Error {
34    fn from(err: base64ct::Error) -> Self {
35        Self::Deserialization(format!("base64 decode: {err}"))
36    }
37}
38
39impl From<bcs::Error> for Error {
40    fn from(err: bcs::Error) -> Self {
41        Self::Deserialization(format!("bcs decode: {err}"))
42    }
43}
44
45impl From<sui_sdk_types::TypeParseError> for Error {
46    fn from(err: sui_sdk_types::TypeParseError) -> Self {
47        Self::Deserialization(format!("type parse: {err}"))
48    }
49}
50
51impl From<std::num::ParseIntError> for Error {
52    fn from(err: std::num::ParseIntError) -> Self {
53        Self::Deserialization(format!("integer parse: {err}"))
54    }
55}
56
57impl From<sui_sdk_types::DigestParseError> for Error {
58    fn from(err: sui_sdk_types::DigestParseError) -> Self {
59        Self::Deserialization(format!("digest parse: {err}"))
60    }
61}
62
63// =============================================================================
64// GraphQL Response Types (per GraphQL spec)
65// https://spec.graphql.org/October2021/#sec-Errors
66// =============================================================================
67
68/// A single GraphQL error.
69#[derive(Debug, Deserialize)]
70pub struct GraphQLError {
71    message: String,
72    locations: Option<Vec<Location>>,
73    path: Option<Vec<PathFragment>>,
74    extensions: Option<HashMap<String, serde_json::Value>>,
75}
76
77impl fmt::Display for GraphQLError {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{}", self.message)
80    }
81}
82
83impl GraphQLError {
84    /// A description of the error.
85    pub fn message(&self) -> &str {
86        &self.message
87    }
88
89    /// Locations in the query where the error occurred.
90    pub fn locations(&self) -> Option<&[Location]> {
91        self.locations.as_deref()
92    }
93
94    /// Path to the field that caused the error (e.g., `["user", "name"]`).
95    pub fn path(&self) -> Option<&[PathFragment]> {
96        self.path.as_deref()
97    }
98
99    /// Additional error metadata (e.g., error code).
100    pub fn extensions(&self) -> Option<&HashMap<String, serde_json::Value>> {
101        self.extensions.as_ref()
102    }
103
104    /// Attempt to extract an error code from the extensions, if present.
105    pub fn code(&self) -> Option<&str> {
106        self.extensions.as_ref()?.get("code")?.as_str()
107    }
108}
109
110/// A segment in an error path - either a field name or array index.
111#[derive(Debug, Deserialize)]
112#[serde(untagged)]
113pub enum PathFragment {
114    /// Field name in the response.
115    Key(String),
116    /// Array index in the response.
117    Index(i32),
118}
119
120/// Location in the GraphQL query where an error occurred.
121#[derive(Debug, Deserialize)]
122pub struct Location {
123    /// Line number (1-indexed).
124    pub line: i32,
125    /// Column number (1-indexed).
126    pub column: i32,
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_error_display() {
135        // Use a relative URL (no scheme) to trigger a builder error
136        let err = Error::Request(
137            reqwest::Client::new()
138                .get("not a valid url")
139                .build()
140                .unwrap_err(),
141        );
142        assert!(err.to_string().contains("Request error"));
143    }
144
145    #[test]
146    fn test_graphql_error_display() {
147        let err: GraphQLError =
148            serde_json::from_value(serde_json::json!({"message": "Field not found"})).unwrap();
149        assert_eq!(err.to_string(), "Field not found");
150    }
151
152    #[test]
153    fn test_graphql_error_code() {
154        let err: GraphQLError = serde_json::from_value(serde_json::json!({
155            "message": "Unknown field \"foo\" on type \"Query\".",
156            "extensions": {"code": "GRAPHQL_VALIDATION_FAILED"}
157        }))
158        .unwrap();
159        assert_eq!(err.code(), Some("GRAPHQL_VALIDATION_FAILED"));
160    }
161
162    #[test]
163    fn test_graphql_error_no_code() {
164        let err: GraphQLError =
165            serde_json::from_value(serde_json::json!({"message": "Error"})).unwrap();
166        assert_eq!(err.code(), None);
167    }
168
169    #[test]
170    fn test_path_fragment_deserialization() {
171        let json = r#"["object", 0, "field"]"#;
172        let path: Vec<PathFragment> = serde_json::from_str(json).unwrap();
173
174        assert!(matches!(&path[0], PathFragment::Key(k) if k == "object"));
175        assert!(matches!(&path[1], PathFragment::Index(0)));
176        assert!(matches!(&path[2], PathFragment::Key(k) if k == "field"));
177    }
178}