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`].
17///
18/// Note: `#f` and `NIL` are reader-equivalent in nomiscript; both decode to
19/// `Value::Nil`. `Bool(false)` does not survive a round-trip through the wire.
20pub 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
116/// Convert a parsed [`Expr`] to a runtime [`Value`].
117///
118/// Handles all structurally representable forms; compile-only forms
119/// (lambdas, wasm locals, etc.) that cannot appear in wire output map to `Nil`.
120/// Keywords (`:foo`) map to `Value::Symbol(":foo")` to preserve plist key identity.
121///
122/// Depth is bounded to [`MAX_VALUE_DEPTH`]; call [`expr_to_value_depth`] directly
123/// when a depth budget is already in scope.
124pub fn expr_to_value(expr: Expr) -> Value {
125    expr_to_value_depth(expr, 0).unwrap_or(Value::Nil)
126}
127
128/// Fallible variant of [`expr_to_value`]: surfaces depth overflow as an
129/// [`EnvelopeError`] instead of silently substituting `Value::Nil`. Callers
130/// re-parsing untrusted printed lists (e.g. `cli-core::render::reparse_list`)
131/// use this so over-nested data is reported, not masked as an empty result.
132pub 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        // In nomiscript #f and NIL are reader-equivalent: format_value(Bool(false)) → "#f"
208        // and Reader::parse("#f") → Expr::Nil. Both are indistinguishable on the wire;
209        // parse_response normalises both to Value::Nil.
210        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        // Build a deeply nested Cons chain exceeding MAX_VALUE_DEPTH
330        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        // expr_to_value must return without stack overflow, capping at Nil
335        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}