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