1use nomiscript::{Expr, Fraction, Reader, Value, format_value};
2use thiserror::Error;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum RequestId {
6 Int(i64),
7 String(String),
8}
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct Request {
12 pub id: RequestId,
13 pub form: Expr,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ErrorCode(String);
25
26impl ErrorCode {
27 pub fn new(symbol: impl Into<String>) -> Self {
28 Self(symbol.into())
29 }
30
31 #[must_use]
32 pub fn as_symbol(&self) -> &str {
33 &self.0
34 }
35
36 pub const ARGS: &str = "args";
37 pub const CONFIG: &str = "config";
38 pub const DB: &str = "db";
39 pub const SERVER: &str = "server";
40 pub const FINANCE: &str = "finance";
41 pub const SCRIPT: &str = "script";
42 pub const PARSE: &str = "parse";
43 pub const COMPILE: &str = "compile";
44 pub const RUNTIME: &str = "runtime";
45 pub const AUTH: &str = "auth";
46 pub const INTERRUPTED: &str = "interrupted";
47 pub const NO_CONVERSION: &str = "no-conversion";
59}
60
61#[derive(Debug, Clone, PartialEq)]
62pub enum ResponsePayload {
63 Value(Value),
64 Error {
65 code: ErrorCode,
66 message: String,
67 detail: Option<String>,
68 },
69}
70
71#[derive(Debug, Clone, PartialEq)]
72pub struct Response {
73 pub id: RequestId,
74 pub payload: ResponsePayload,
75}
76
77#[derive(Debug, Error)]
78pub enum EnvelopeError {
79 #[error("frame parse error: {0}")]
80 Parse(String),
81 #[error("envelope must contain exactly one top-level expression")]
82 NotSingleExpr,
83 #[error("envelope must be a list of plist pairs")]
84 NotPlist,
85 #[error("missing required key {0}")]
86 MissingKey(&'static str),
87 #[error("invalid value for key {0}: {1}")]
88 InvalidValue(&'static str, String),
89}
90
91pub fn parse_request(frame: &str) -> Result<Request, EnvelopeError> {
92 let program = Reader::parse(frame).map_err(|e| EnvelopeError::Parse(e.to_string()))?;
93 let mut iter = program.exprs.into_iter();
94 let envelope = iter.next().ok_or(EnvelopeError::NotSingleExpr)?;
95 if iter.next().is_some() {
96 return Err(EnvelopeError::NotSingleExpr);
97 }
98 let plist = match envelope {
99 Expr::List(items) => items,
100 _ => return Err(EnvelopeError::NotPlist),
101 };
102 let pairs = collect_plist(plist)?;
103 let id_expr = pairs
104 .iter()
105 .find(|(k, _)| k == "ID")
106 .map(|(_, v)| v.clone())
107 .ok_or(EnvelopeError::MissingKey(":id"))?;
108 let form = pairs
109 .into_iter()
110 .find(|(k, _)| k == "FORM")
111 .map(|(_, v)| v)
112 .ok_or(EnvelopeError::MissingKey(":form"))?;
113 let id = expr_to_request_id(&id_expr)?;
114 Ok(Request { id, form })
115}
116
117pub fn format_response(response: &Response) -> String {
118 match &response.payload {
119 ResponsePayload::Value(value) => format!(
120 "(:id {} :value {})",
121 format_id(&response.id),
122 format_value(value)
123 ),
124 ResponsePayload::Error {
125 code,
126 message,
127 detail,
128 } => {
129 let mut error_body = format!(
130 "(:code {} :message {})",
131 code.as_symbol(),
132 format_value(&Value::String(message.clone()))
133 );
134 if let Some(detail) = detail {
135 error_body = format!(
136 "(:code {} :message {} :detail {})",
137 code.as_symbol(),
138 format_value(&Value::String(message.clone())),
139 format_value(&Value::String(detail.clone()))
140 );
141 }
142 format!("(:id {} :error {})", format_id(&response.id), error_body)
143 }
144 }
145}
146
147fn format_id(id: &RequestId) -> String {
148 match id {
149 RequestId::Int(n) => n.to_string(),
150 RequestId::String(s) => format_value(&Value::String(s.clone())),
151 }
152}
153
154fn collect_plist(items: Vec<Expr>) -> Result<Vec<(String, Expr)>, EnvelopeError> {
155 if !items.len().is_multiple_of(2) {
156 return Err(EnvelopeError::NotPlist);
157 }
158 let mut pairs = Vec::with_capacity(items.len() / 2);
159 let mut iter = items.into_iter();
160 while let Some(key) = iter.next() {
161 let key_name = match key {
162 Expr::Keyword(name) => name,
163 _ => return Err(EnvelopeError::NotPlist),
164 };
165 let value = iter.next().ok_or(EnvelopeError::NotPlist)?;
166 pairs.push((key_name, value));
167 }
168 Ok(pairs)
169}
170
171fn expr_to_request_id(expr: &Expr) -> Result<RequestId, EnvelopeError> {
172 match expr {
173 Expr::Number(n) => integer_value(n).map(RequestId::Int).ok_or_else(|| {
174 EnvelopeError::InvalidValue(":id", format!("expected integer, got {n}"))
175 }),
176 Expr::String(s) => Ok(RequestId::String(s.clone())),
177 other => Err(EnvelopeError::InvalidValue(
178 ":id",
179 format!("expected integer or string, got {other:?}"),
180 )),
181 }
182}
183
184fn integer_value(n: &Fraction) -> Option<i64> {
185 if *n.denom() == 1 {
186 Some(*n.numer())
187 } else {
188 None
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use nomiscript::Pair;
196
197 #[test]
198 fn parses_basic_request() {
199 let req = parse_request("(:id 42 :form (list-accounts))").unwrap();
200 assert_eq!(req.id, RequestId::Int(42));
201 assert_eq!(
202 req.form,
203 Expr::List(vec![Expr::Symbol("LIST-ACCOUNTS".into())])
204 );
205 }
206
207 #[test]
208 fn parses_string_id() {
209 let req = parse_request("(:id \"abc\" :form 1)").unwrap();
210 assert_eq!(req.id, RequestId::String("abc".to_string()));
211 }
212
213 #[test]
214 fn rejects_envelope_missing_id() {
215 let err = parse_request("(:form (foo))").unwrap_err();
216 assert!(matches!(err, EnvelopeError::MissingKey(":id")));
217 }
218
219 #[test]
220 fn rejects_envelope_missing_form() {
221 let err = parse_request("(:id 1)").unwrap_err();
222 assert!(matches!(err, EnvelopeError::MissingKey(":form")));
223 }
224
225 #[test]
226 fn rejects_envelope_with_invalid_id() {
227 let err = parse_request("(:id (a b) :form 1)").unwrap_err();
228 assert!(matches!(err, EnvelopeError::InvalidValue(":id", _)));
229 }
230
231 #[test]
232 fn rejects_envelope_with_fractional_id() {
233 let err = parse_request("(:id 3/4 :form 1)").unwrap_err();
234 assert!(matches!(err, EnvelopeError::InvalidValue(":id", _)));
235 }
236
237 #[test]
238 fn rejects_envelope_with_unbalanced_plist() {
239 let err = parse_request("(:id 1 :form)").unwrap_err();
240 assert!(matches!(err, EnvelopeError::NotPlist));
241 }
242
243 #[test]
244 fn rejects_non_list_envelope() {
245 let err = parse_request("42").unwrap_err();
246 assert!(matches!(err, EnvelopeError::NotPlist));
247 }
248
249 #[test]
250 fn rejects_multiple_envelopes_per_frame() {
251 let err = parse_request("(:id 1 :form 1) (:id 2 :form 2)").unwrap_err();
252 assert!(matches!(err, EnvelopeError::NotSingleExpr));
253 }
254
255 #[test]
256 fn formats_value_response() {
257 let resp = Response {
258 id: RequestId::Int(7),
259 payload: ResponsePayload::Value(Value::Number(Fraction::from_integer(99))),
260 };
261 assert_eq!(format_response(&resp), "(:id 7 :value 99)");
262 }
263
264 #[test]
265 fn formats_value_response_with_string_id() {
266 let resp = Response {
267 id: RequestId::String("req-1".into()),
268 payload: ResponsePayload::Value(Value::Bool(true)),
269 };
270 assert_eq!(format_response(&resp), "(:id \"req-1\" :value #t)");
271 }
272
273 #[test]
274 fn formats_error_response_without_detail() {
275 let resp = Response {
276 id: RequestId::Int(3),
277 payload: ResponsePayload::Error {
278 code: ErrorCode::new(ErrorCode::ARGS),
279 message: "missing :user-id".into(),
280 detail: None,
281 },
282 };
283 assert_eq!(
284 format_response(&resp),
285 "(:id 3 :error (:code args :message \"missing :user-id\"))"
286 );
287 }
288
289 #[test]
290 fn formats_error_response_with_detail() {
291 let resp = Response {
292 id: RequestId::Int(8),
293 payload: ResponsePayload::Error {
294 code: ErrorCode::new(ErrorCode::DB),
295 message: "query failed".into(),
296 detail: Some("SqlxError(...)".into()),
297 },
298 };
299 assert_eq!(
300 format_response(&resp),
301 "(:id 8 :error (:code db :message \"query failed\" :detail \"SqlxError(...)\"))"
302 );
303 }
304
305 #[test]
306 fn formats_error_for_every_well_known_code() {
307 for symbol in [
308 ErrorCode::ARGS,
309 ErrorCode::CONFIG,
310 ErrorCode::DB,
311 ErrorCode::SERVER,
312 ErrorCode::FINANCE,
313 ErrorCode::SCRIPT,
314 ErrorCode::PARSE,
315 ErrorCode::COMPILE,
316 ErrorCode::RUNTIME,
317 ErrorCode::AUTH,
318 ErrorCode::INTERRUPTED,
319 ErrorCode::NO_CONVERSION,
320 ] {
321 let code = ErrorCode::new(symbol);
322 let resp = Response {
323 id: RequestId::Int(0),
324 payload: ResponsePayload::Error {
325 code: code.clone(),
326 message: "x".into(),
327 detail: None,
328 },
329 };
330 let formatted = format_response(&resp);
331 assert!(
332 formatted.contains(code.as_symbol()),
333 "expected {symbol:?} in {formatted:?}",
334 );
335 }
336 }
337
338 #[test]
339 fn formats_script_raised_code_verbatim() {
340 let resp = Response {
341 id: RequestId::Int(4),
342 payload: ResponsePayload::Error {
343 code: ErrorCode::new("no-such-account"),
344 message: "id=42".into(),
345 detail: None,
346 },
347 };
348 assert_eq!(
349 format_response(&resp),
350 "(:id 4 :error (:code no-such-account :message \"id=42\"))"
351 );
352 }
353
354 #[test]
355 fn formats_value_response_with_bytes() {
356 let resp = Response {
357 id: RequestId::Int(1),
358 payload: ResponsePayload::Value(Value::Bytes(vec![0xCA, 0xFE])),
359 };
360 assert_eq!(format_response(&resp), "(:id 1 :value #u8(202 254))");
361 }
362
363 #[test]
364 fn formats_value_response_with_pair() {
365 let list = Pair::cons(
366 Value::Number(Fraction::from_integer(1)),
367 Pair::cons(Value::Number(Fraction::from_integer(2)), Value::Nil),
368 );
369 let resp = Response {
370 id: RequestId::Int(1),
371 payload: ResponsePayload::Value(list),
372 };
373 assert_eq!(format_response(&resp), "(:id 1 :value (1 2))");
374 }
375}