Skip to main content

cufflink_types/host/
ext.rs

1//! `ext_call`.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7#[serde(deny_unknown_fields)]
8pub struct CallRequest {
9    pub extension: String,
10    pub payload: Value,
11    /// How long to wait for the reply; the platform's default when unset,
12    /// and never past what is left of the handler's own budget.
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub timeout_ms: Option<u64>,
15}
16
17/// `{"ok": true, "value": …}` or `{"ok": false, "error": {…}}`.
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19#[serde(try_from = "ResponseWire", into = "ResponseWire")]
20pub enum CallResponse {
21    Ok(Value),
22    Error(CallErrorEnvelope),
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct CallErrorEnvelope {
28    pub kind: CallErrorKind,
29    /// The extension's own code, carried by [`CallErrorKind::Failed`].
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub code: Option<String>,
32    pub message: String,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum CallErrorKind {
38    /// The service's manifest does not list the extension.
39    NotDeclared,
40    /// No extension of the name is registered for the tenant.
41    NotFound,
42    /// The registration does not list the calling service.
43    Denied,
44    /// Nothing serves the extension, or what does answered nonsense.
45    Unavailable,
46    Timeout,
47    TooLarge,
48    /// The extension answered with an error of its own.
49    Failed,
50    /// The request is malformed.
51    Invalid,
52}
53
54impl CallErrorKind {
55    pub fn as_str(self) -> &'static str {
56        match self {
57            CallErrorKind::NotDeclared => "not_declared",
58            CallErrorKind::NotFound => "not_found",
59            CallErrorKind::Denied => "denied",
60            CallErrorKind::Unavailable => "unavailable",
61            CallErrorKind::Timeout => "timeout",
62            CallErrorKind::TooLarge => "too_large",
63            CallErrorKind::Failed => "failed",
64            CallErrorKind::Invalid => "invalid",
65        }
66    }
67}
68
69#[derive(Serialize, Deserialize)]
70#[serde(deny_unknown_fields)]
71struct ResponseWire {
72    ok: bool,
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    value: Option<Value>,
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    error: Option<CallErrorEnvelope>,
77}
78
79impl TryFrom<ResponseWire> for CallResponse {
80    type Error = &'static str;
81
82    fn try_from(wire: ResponseWire) -> Result<Self, &'static str> {
83        match wire {
84            ResponseWire {
85                ok: true,
86                value,
87                error: None,
88            } => Ok(CallResponse::Ok(value.unwrap_or_default())),
89            ResponseWire {
90                ok: false,
91                value: None,
92                error: Some(error),
93            } => Ok(CallResponse::Error(error)),
94            ResponseWire { ok: true, .. } => Err("a response that is ok carries no error"),
95            ResponseWire { ok: false, .. } => {
96                Err("a response that is not ok carries an error and no value")
97            }
98        }
99    }
100}
101
102impl From<CallResponse> for ResponseWire {
103    fn from(response: CallResponse) -> Self {
104        match response {
105            CallResponse::Ok(value) => ResponseWire {
106                ok: true,
107                value: Some(value),
108                error: None,
109            },
110            CallResponse::Error(error) => ResponseWire {
111                ok: false,
112                value: None,
113                error: Some(error),
114            },
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::host::tests::{refused, round_trip};
123    use serde_json::json;
124
125    const KINDS: [CallErrorKind; 8] = [
126        CallErrorKind::NotDeclared,
127        CallErrorKind::NotFound,
128        CallErrorKind::Denied,
129        CallErrorKind::Unavailable,
130        CallErrorKind::Timeout,
131        CallErrorKind::TooLarge,
132        CallErrorKind::Failed,
133        CallErrorKind::Invalid,
134    ];
135
136    #[test]
137    fn a_request_carries_only_what_it_sets() {
138        round_trip(
139            &CallRequest {
140                extension: "google.token".into(),
141                payload: json!({"scope": "analytics.readonly"}),
142                timeout_ms: None,
143            },
144            json!({"extension": "google.token", "payload": {"scope": "analytics.readonly"}}),
145        );
146        round_trip(
147            &CallRequest {
148                extension: "pdf.render".into(),
149                payload: Value::Null,
150                timeout_ms: Some(5_000),
151            },
152            json!({"extension": "pdf.render", "payload": null, "timeout_ms": 5000}),
153        );
154    }
155
156    #[test]
157    fn a_response_says_whether_it_is_ok() {
158        round_trip(
159            &CallResponse::Ok(json!({"token": "ya29"})),
160            json!({"ok": true, "value": {"token": "ya29"}}),
161        );
162        round_trip(
163            &CallResponse::Ok(Value::Null),
164            json!({"ok": true, "value": null}),
165        );
166        assert_eq!(
167            serde_json::from_value::<CallResponse>(json!({"ok": true})).expect("parse"),
168            CallResponse::Ok(Value::Null)
169        );
170        round_trip(
171            &CallResponse::Error(CallErrorEnvelope {
172                kind: CallErrorKind::Failed,
173                code: Some("quota".into()),
174                message: "daily quota exhausted".into(),
175            }),
176            json!({
177                "ok": false,
178                "error": {"kind": "failed", "code": "quota", "message": "daily quota exhausted"}
179            }),
180        );
181    }
182
183    #[test]
184    fn errors_name_their_kind() {
185        for kind in KINDS {
186            round_trip(
187                &CallResponse::Error(CallErrorEnvelope {
188                    kind,
189                    code: None,
190                    message: "why".into(),
191                }),
192                json!({"ok": false, "error": {"kind": kind.as_str(), "message": "why"}}),
193            );
194        }
195    }
196
197    #[test]
198    fn contradictions_and_unknown_kinds_are_refused() {
199        let error = json!({"kind": "denied", "message": "m"});
200        refused::<CallResponse>(json!({"ok": true, "error": error}));
201        refused::<CallResponse>(json!({"ok": true, "value": 1, "error": error}));
202        refused::<CallResponse>(json!({"ok": false}));
203        refused::<CallResponse>(json!({"ok": false, "value": 1, "error": error}));
204        refused::<CallResponse>(json!({"value": 1}));
205        refused::<CallResponse>(json!({"ok": "true", "value": 1}));
206        refused::<CallResponse>(json!({"ok": true, "value": 1, "trace": "x"}));
207        refused::<CallResponse>(
208            json!({"ok": false, "error": {"kind": "exploded", "message": "m"}}),
209        );
210        refused::<CallResponse>(
211            json!({"ok": false, "error": {"kind": "denied", "message": "m", "extra": 1}}),
212        );
213        refused::<CallRequest>(json!({"extension": "a"}));
214        refused::<CallRequest>(json!({"payload": 1}));
215        refused::<CallRequest>(json!({"extension": "a", "payload": 1, "retries": 2}));
216        refused::<CallRequest>(json!({"extension": "a", "payload": 1, "timeout_ms": -1}));
217    }
218}