Skip to main content

rpc/
response.rs

1//! Response parsing: the inverse of [`super::envelope::format_response`].
2
3use 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
9/// Maximum nesting depth for [`expr_to_value`] to guard against stack overflow.
10const MAX_VALUE_DEPTH: usize = 64;
11
12/// Parse a wire response frame into a [`Response`].
13///
14/// Accepts `(:id N :value V)` or `(:id N :error (:code sym :message "..." :detail "..."))`.
15/// Rejects frames containing both `:value` and `:error` simultaneously.
16/// This is the inverse of [`super::envelope::format_response`].
17pub 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
113/// Convert a parsed [`Expr`] to a runtime [`Value`].
114///
115/// Handles all structurally representable forms; compile-only forms
116/// (lambdas, wasm locals, etc.) that cannot appear in wire output map to `Nil`.
117/// Keywords (`:foo`) map to `Value::Symbol(":foo")` to preserve plist key identity.
118///
119/// Depth is bounded to [`MAX_VALUE_DEPTH`]; call [`expr_to_value_depth`] directly
120/// when a depth budget is already in scope.
121pub fn expr_to_value(expr: Expr) -> Value {
122    expr_to_value_depth(expr, 0).unwrap_or(Value::Nil)
123}
124
125/// Fallible variant of [`expr_to_value`]: surfaces depth overflow as an
126/// [`EnvelopeError`] instead of silently substituting `Value::Nil`. Callers
127/// re-parsing untrusted printed lists (e.g. `cli-core::render::reparse_list`)
128/// use this so over-nested data is reported, not masked as an empty result.
129pub 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        // Build a deeply nested Cons chain exceeding MAX_VALUE_DEPTH
313        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        // expr_to_value must return without stack overflow, capping at Nil
318        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}