1use nomiscript::{Expr, Pair, Reader, Value, vec_to_list};
4
5use super::envelope::{
6 EnvelopeError, ErrorCode, Response, ResponsePayload, collect_plist, expr_to_request_id,
7};
8
9const MAX_VALUE_DEPTH: usize = 64;
11
12pub fn parse_response(wire: &str) -> Result<Response, EnvelopeError> {
21 let program = Reader::parse(wire).map_err(|e| EnvelopeError::Parse(e.to_string()))?;
22 let mut iter = program.exprs.into_iter();
23 let envelope = iter.next().ok_or(EnvelopeError::NotSingleExpr)?;
24 if iter.next().is_some() {
25 return Err(EnvelopeError::NotSingleExpr);
26 }
27 let plist = match envelope {
28 Expr::List(items) => items,
29 _ => return Err(EnvelopeError::NotPlist),
30 };
31 let pairs = collect_plist(plist)?;
32 let id_expr = pairs
33 .iter()
34 .find(|(k, _)| k == "ID")
35 .map(|(_, v)| v.clone())
36 .ok_or(EnvelopeError::MissingKey(":id"))?;
37 let id = expr_to_request_id(&id_expr)?;
38 let has_value = pairs.iter().any(|(k, _)| k == "VALUE");
39 let has_error = pairs.iter().any(|(k, _)| k == "ERROR");
40 match (has_value, has_error) {
41 (true, true) => Err(EnvelopeError::InvalidValue(
42 ":value/:error",
43 "frame must contain exactly one of :value or :error, not both".into(),
44 )),
45 (true, false) => {
46 let value_expr = pairs
47 .into_iter()
48 .find(|(k, _)| k == "VALUE")
49 .map(|(_, v)| v)
50 .ok_or(EnvelopeError::MissingKey(":value"))?;
51 expr_to_value_depth(value_expr, 0)
52 .map(|v| Response {
53 id,
54 payload: ResponsePayload::Value(v),
55 })
56 .map_err(|e| {
57 EnvelopeError::InvalidValue(":value", format!("value too deeply nested: {e}"))
58 })
59 }
60 (false, true) => {
61 let error_expr = pairs
62 .into_iter()
63 .find(|(k, _)| k == "ERROR")
64 .map(|(_, v)| v)
65 .ok_or(EnvelopeError::MissingKey(":error"))?;
66 let payload = parse_error_payload(error_expr)?;
67 Ok(Response { id, payload })
68 }
69 (false, false) => Err(EnvelopeError::MissingKey(":value or :error")),
70 }
71}
72
73fn parse_error_payload(expr: Expr) -> Result<ResponsePayload, EnvelopeError> {
74 let items = match expr {
75 Expr::List(items) => items,
76 _ => return Err(EnvelopeError::NotPlist),
77 };
78 let pairs = collect_plist(items)?;
79 let code_expr = pairs
80 .iter()
81 .find(|(k, _)| k == "CODE")
82 .map(|(_, v)| v.clone())
83 .ok_or(EnvelopeError::MissingKey(":code"))?;
84 let code_str = match code_expr {
85 Expr::Symbol(s) => s.to_lowercase(),
86 Expr::Keyword(s) => s.to_lowercase(),
87 other => {
88 return Err(EnvelopeError::InvalidValue(
89 ":code",
90 format!("expected symbol, got {other:?}"),
91 ));
92 }
93 };
94 let message = pairs
95 .iter()
96 .find(|(k, _)| k == "MESSAGE")
97 .and_then(|(_, v)| match v {
98 Expr::String(s) => Some(s.clone()),
99 _ => None,
100 })
101 .ok_or(EnvelopeError::MissingKey(":message"))?;
102 let detail = pairs
103 .iter()
104 .find(|(k, _)| k == "DETAIL")
105 .and_then(|(_, v)| match v {
106 Expr::String(s) => Some(s.clone()),
107 _ => None,
108 });
109 Ok(ResponsePayload::Error {
110 code: ErrorCode::new(code_str),
111 message,
112 detail,
113 })
114}
115
116pub fn expr_to_value(expr: Expr) -> Value {
125 expr_to_value_depth(expr, 0).unwrap_or(Value::Nil)
126}
127
128pub fn try_expr_to_value(expr: Expr) -> Result<Value, EnvelopeError> {
133 expr_to_value_depth(expr, 0)
134 .map_err(|e| EnvelopeError::InvalidValue(":value", format!("value too deeply nested: {e}")))
135}
136
137fn expr_to_value_depth(expr: Expr, depth: usize) -> Result<Value, &'static str> {
138 if depth > MAX_VALUE_DEPTH {
139 return Err("nesting depth exceeded");
140 }
141 let v = match expr {
142 Expr::Nil => Value::Nil,
143 Expr::Bool(b) => Value::Bool(b),
144 Expr::Number(n) => Value::Number(n),
145 Expr::String(s) => Value::String(s),
146 Expr::Symbol(s) => Value::Symbol(s),
147 Expr::Keyword(name) => Value::Symbol(format!(":{}", name.to_lowercase())),
148 Expr::Bytes(b) => Value::Bytes(b),
149 Expr::List(items) => {
150 let converted: Result<Vec<Value>, _> = items
151 .into_iter()
152 .map(|e| expr_to_value_depth(e, depth + 1))
153 .collect();
154 vec_to_list(converted?)
155 }
156 Expr::Cons(car, cdr) => Pair::cons(
157 expr_to_value_depth(*car, depth + 1)?,
158 expr_to_value_depth(*cdr, depth + 1)?,
159 ),
160 Expr::RuntimeValue(v) => v,
161 _ => Value::Nil,
162 };
163 Ok(v)
164}
165
166#[cfg(test)]
167mod tests {
168 use nomiscript::{Fraction, Pair, Value};
169
170 use super::super::envelope::{
171 ErrorCode, RequestId, Response, ResponsePayload, format_response,
172 };
173 use super::*;
174
175 fn round_trip(resp: Response) {
176 let wire = format_response(&resp);
177 let parsed = parse_response(&wire).unwrap();
178 assert_eq!(parsed, resp, "round-trip failed for wire: {wire:?}");
179 }
180
181 #[test]
182 fn round_trip_value_number() {
183 round_trip(Response {
184 id: RequestId::Int(1),
185 payload: ResponsePayload::Value(Value::Number(Fraction::from_integer(42))),
186 });
187 }
188
189 #[test]
190 fn round_trip_value_bool_true() {
191 round_trip(Response {
192 id: RequestId::Int(2),
193 payload: ResponsePayload::Value(Value::Bool(true)),
194 });
195 }
196
197 #[test]
198 fn round_trip_value_nil() {
199 round_trip(Response {
200 id: RequestId::Int(4),
201 payload: ResponsePayload::Value(Value::Nil),
202 });
203 }
204
205 #[test]
206 fn bool_false_round_trips_as_nil() {
207 let resp = Response {
211 id: RequestId::Int(99),
212 payload: ResponsePayload::Value(Value::Bool(false)),
213 };
214 let wire = format_response(&resp);
215 let parsed = parse_response(&wire).unwrap();
216 assert_eq!(parsed.payload, ResponsePayload::Value(Value::Nil));
217 }
218
219 #[test]
220 fn round_trip_value_string() {
221 round_trip(Response {
222 id: RequestId::Int(5),
223 payload: ResponsePayload::Value(Value::String("hello world".into())),
224 });
225 }
226
227 #[test]
228 fn round_trip_value_pair() {
229 let list = Pair::cons(
230 Value::Number(Fraction::from_integer(10)),
231 Pair::cons(Value::Number(Fraction::from_integer(20)), Value::Nil),
232 );
233 round_trip(Response {
234 id: RequestId::Int(6),
235 payload: ResponsePayload::Value(list),
236 });
237 }
238
239 #[test]
240 fn round_trip_error_without_detail() {
241 round_trip(Response {
242 id: RequestId::Int(7),
243 payload: ResponsePayload::Error {
244 code: ErrorCode::new(ErrorCode::ARGS),
245 message: "bad args".into(),
246 detail: None,
247 },
248 });
249 }
250
251 #[test]
252 fn round_trip_error_with_detail() {
253 round_trip(Response {
254 id: RequestId::Int(8),
255 payload: ResponsePayload::Error {
256 code: ErrorCode::new(ErrorCode::DB),
257 message: "query failed".into(),
258 detail: Some("SqlxError(...)".into()),
259 },
260 });
261 }
262
263 #[test]
264 fn round_trip_string_id() {
265 round_trip(Response {
266 id: RequestId::String("req-abc".into()),
267 payload: ResponsePayload::Value(Value::Bool(true)),
268 });
269 }
270
271 #[test]
272 fn parse_response_rejects_empty_input() {
273 let err = parse_response("").unwrap_err();
274 assert!(matches!(err, EnvelopeError::NotSingleExpr));
275 }
276
277 #[test]
278 fn parse_response_rejects_non_list() {
279 let err = parse_response("42").unwrap_err();
280 assert!(matches!(err, EnvelopeError::NotPlist));
281 }
282
283 #[test]
284 fn parse_response_rejects_missing_id() {
285 let err = parse_response("(:value 1)").unwrap_err();
286 assert!(matches!(err, EnvelopeError::MissingKey(":id")));
287 }
288
289 #[test]
290 fn parse_response_rejects_missing_value_and_error() {
291 let err = parse_response("(:id 1)").unwrap_err();
292 assert!(matches!(err, EnvelopeError::MissingKey(":value or :error")));
293 }
294
295 #[test]
296 fn parse_response_rejects_multiple_top_level() {
297 let err = parse_response("(:id 1 :value 1) (:id 2 :value 2)").unwrap_err();
298 assert!(matches!(err, EnvelopeError::NotSingleExpr));
299 }
300
301 #[test]
302 fn parse_response_rejects_both_value_and_error_value_first() {
303 let err =
304 parse_response(r#"(:id 1 :value 42 :error (:code args :message "bad"))"#).unwrap_err();
305 assert!(matches!(
306 err,
307 EnvelopeError::InvalidValue(":value/:error", _)
308 ));
309 }
310
311 #[test]
312 fn parse_response_rejects_both_value_and_error_error_first() {
313 let err =
314 parse_response(r#"(:id 1 :error (:code args :message "bad") :value 42)"#).unwrap_err();
315 assert!(matches!(
316 err,
317 EnvelopeError::InvalidValue(":value/:error", _)
318 ));
319 }
320
321 #[test]
322 fn expr_to_value_keyword_maps_to_symbol_with_colon() {
323 let expr = Expr::Keyword("COMMODITY-ID".into());
324 assert_eq!(expr_to_value(expr), Value::Symbol(":commodity-id".into()));
325 }
326
327 #[test]
328 fn expr_to_value_deeply_nested_returns_nil_not_abort() {
329 let mut expr = Expr::Nil;
331 for _ in 0..MAX_VALUE_DEPTH + 10 {
332 expr = Expr::Cons(Box::new(Expr::Nil), Box::new(expr));
333 }
334 let _ = expr_to_value(expr);
336 }
337
338 #[test]
339 fn expr_to_value_depth_errors_on_deep_input() {
340 let mut expr = Expr::Nil;
341 for _ in 0..MAX_VALUE_DEPTH + 10 {
342 expr = Expr::Cons(Box::new(Expr::Nil), Box::new(expr));
343 }
344 let result = expr_to_value_depth(expr, 0);
345 assert!(result.is_err(), "expected depth error, got: {result:?}");
346 }
347}