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