1use 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 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub timeout_ms: Option<u64>,
15}
16
17#[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 #[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 NotDeclared,
40 NotFound,
42 Denied,
44 Unavailable,
46 Timeout,
47 TooLarge,
48 Failed,
50 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}