Skip to main content

gestalt/
invoke_support.rs

1// Code generated by sdkgen. DO NOT EDIT.
2
3use serde_json::Value;
4
5use crate::rpc_support::{GestaltError, gestalt_error_code, http_status_to_gestalt_code};
6
7/// Decoded app invocation payload failure: an HTTP-error status, an error
8/// envelope, or an undecodable result body.
9///
10/// Envelope `error.code` on the wire maps to [`InvokeResultError::reason`].
11#[derive(Clone, Debug, thiserror::Error)]
12#[error("{message}")]
13pub struct InvokeResultError {
14    /// The invoked app.
15    pub app: String,
16    /// The invoked operation.
17    pub operation: String,
18    /// The HTTP status when the result carried one.
19    pub status: Option<u16>,
20    /// The error envelope's code when present.
21    pub reason: Option<String>,
22    /// The failure message.
23    pub message: String,
24    /// The decoded result body when it parsed as JSON.
25    pub body: Option<Box<Value>>,
26    /// The raw result body bytes.
27    pub raw_body: Vec<u8>,
28}
29
30impl InvokeResultError {
31    /// Returns the canonical Gestalt gRPC error code for this payload failure.
32    pub fn gestalt_code(&self) -> i32 {
33        if let Some(status) = self.status {
34            return http_status_to_gestalt_code(status as i32);
35        }
36        if self.reason.as_deref() == Some("graphql_errors")
37            || self.message == "app invoke response is not valid JSON"
38            || self.message == "operation result body is not valid JSON"
39        {
40            return gestalt_error_code::INTERNAL;
41        }
42        gestalt_error_code::UNKNOWN
43    }
44}
45
46/// Failure of a `json_result` method: the transport failed, or the decoded
47/// result carried a payload failure.
48#[derive(Debug, thiserror::Error)]
49pub enum InvokeError {
50    /// The transport failed before a result decoded.
51    #[error(transparent)]
52    Transport(#[from] GestaltError),
53    /// The decoded result carried a payload failure.
54    #[error(transparent)]
55    Result(#[from] Box<InvokeResultError>),
56}
57
58/// Decodes one app operation result with the standard JSON envelope
59/// semantics: success envelopes return their data, error envelopes and
60/// HTTP-error statuses return [`InvokeResultError`], and any other JSON body
61/// passes through unchanged.
62pub fn decode_app_result(
63    app: &str,
64    operation: &str,
65    status: i32,
66    body: &[u8],
67) -> Result<Value, Box<InvokeResultError>> {
68    let parsed = parse_json_result_body(body);
69    if !is_success(status) {
70        return Err(http_status_error(app, operation, status, body, parsed));
71    }
72    let parsed = match parsed {
73        Ok(parsed) => parsed,
74        Err(_) => {
75            return Err(Box::new(InvokeResultError {
76                app: app.to_string(),
77                operation: operation.to_string(),
78                status: None,
79                reason: None,
80                message: "app invoke response is not valid JSON".to_string(),
81                body: None,
82                raw_body: body.to_vec(),
83            }));
84        }
85    };
86    if let Some(status_value) = parsed.get("status").and_then(Value::as_str) {
87        if status_value == "error" {
88            let mut error = InvokeResultError {
89                app: app.to_string(),
90                operation: operation.to_string(),
91                status: None,
92                reason: None,
93                message: "app invoke failed".to_string(),
94                body: None,
95                raw_body: body.to_vec(),
96            };
97            apply_invoke_error_fields(&mut error, &parsed);
98            error.body = Some(Box::new(parsed));
99            return Err(Box::new(error));
100        }
101        if status_value == "success" {
102            if let Some(data) = parsed.get("data") {
103                return Ok(data.clone());
104            }
105        }
106    }
107    Ok(parsed)
108}
109
110/// Decodes one GraphQL invocation result like [`decode_app_result`] and
111/// additionally fails when the response carries a non-empty GraphQL `errors`
112/// array.
113pub fn decode_graphql_result(
114    app: &str,
115    status: i32,
116    body: &[u8],
117) -> Result<Value, Box<InvokeResultError>> {
118    let decoded = decode_app_result(app, "graphql", status, body)?;
119    if let Ok(raw) = parse_json_result_body(body) {
120        graphql_errors(app, body, &raw)?;
121    }
122    graphql_errors(app, body, &decoded)?;
123    Ok(decoded)
124}
125
126/// Reports whether an HTTP status code is a success (200-299), mirroring
127/// reqwest's `StatusCode::is_success`.
128pub fn is_success(status: i32) -> bool {
129    (200..=299).contains(&status)
130}
131
132/// Returns the payload failure a non-success HTTP status decodes to,
133/// mirroring reqwest's `Response::error_for_status`; success statuses return
134/// Ok. The error carries exactly what [`decode_app_result`] would attach for
135/// the same status and body.
136pub fn error_for_status(
137    app: &str,
138    operation: &str,
139    status: i32,
140    body: &[u8],
141) -> Result<(), Box<InvokeResultError>> {
142    if is_success(status) {
143        return Ok(());
144    }
145    Err(http_status_error(
146        app,
147        operation,
148        status,
149        body,
150        parse_json_result_body(body),
151    ))
152}
153
154/// Builds the payload failure for an HTTP-error status: the raw body always
155/// rides along, and a JSON body additionally carries its parsed form and
156/// error envelope fields.
157fn http_status_error(
158    app: &str,
159    operation: &str,
160    status: i32,
161    body: &[u8],
162    parsed: Result<Value, serde_json::Error>,
163) -> Box<InvokeResultError> {
164    let mut error = InvokeResultError {
165        app: app.to_string(),
166        operation: operation.to_string(),
167        status: u16::try_from(status).ok(),
168        reason: None,
169        message: format!("app invoke failed with status {status}"),
170        body: None,
171        raw_body: body.to_vec(),
172    };
173    if let Ok(parsed) = parsed {
174        apply_invoke_error_fields(&mut error, &parsed);
175        error.body = Some(Box::new(parsed));
176    }
177    Box::new(error)
178}
179
180fn parse_json_result_body(body: &[u8]) -> Result<Value, serde_json::Error> {
181    if body.iter().all(u8::is_ascii_whitespace) {
182        return Ok(serde_json::json!({}));
183    }
184    serde_json::from_slice(body)
185}
186
187fn graphql_errors(app: &str, raw_body: &[u8], value: &Value) -> Result<(), Box<InvokeResultError>> {
188    let Some(errors) = value.get("errors").and_then(Value::as_array) else {
189        return Ok(());
190    };
191    if errors.is_empty() {
192        return Ok(());
193    }
194    let message = errors
195        .first()
196        .and_then(|first| first.get("message"))
197        .and_then(Value::as_str)
198        .filter(|text| !text.trim().is_empty())
199        .unwrap_or("GraphQL returned errors")
200        .to_string();
201    Err(Box::new(InvokeResultError {
202        app: app.to_string(),
203        operation: "graphql".to_string(),
204        status: None,
205        reason: Some("graphql_errors".to_string()),
206        message,
207        body: Some(Box::new(value.clone())),
208        raw_body: raw_body.to_vec(),
209    }))
210}
211
212fn apply_invoke_error_fields(error: &mut InvokeResultError, parsed: &Value) {
213    let nested = parsed.get("error");
214    let nested_message = nested
215        .and_then(|value| value.get("message"))
216        .and_then(Value::as_str)
217        .filter(|text| !text.trim().is_empty());
218    let nested_reason = nested
219        .and_then(|value| value.get("code"))
220        .and_then(Value::as_str)
221        .filter(|text| !text.trim().is_empty());
222    let top_message = parsed
223        .get("message")
224        .and_then(Value::as_str)
225        .filter(|text| !text.trim().is_empty());
226    let top_reason = parsed
227        .get("code")
228        .and_then(Value::as_str)
229        .filter(|text| !text.trim().is_empty());
230    if let Some(message) = nested_message.or(top_message) {
231        error.message = message.to_string();
232    }
233    if let Some(reason) = nested_reason.or(top_reason) {
234        error.reason = Some(reason.to_string());
235    }
236}