1
//! Response parsing: the inverse of [`super::envelope::format_response`].
2

            
3
use nomiscript::{Expr, Pair, Reader, Value, vec_to_list};
4

            
5
use 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.
10
const 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.
20
2841
pub fn parse_response(wire: &str) -> Result<Response, EnvelopeError> {
21
2841
    let program = Reader::parse(wire).map_err(|e| EnvelopeError::Parse(e.to_string()))?;
22
2816
    let mut iter = program.exprs.into_iter();
23
2816
    let envelope = iter.next().ok_or(EnvelopeError::NotSingleExpr)?;
24
2740
    if iter.next().is_some() {
25
151
        return Err(EnvelopeError::NotSingleExpr);
26
2589
    }
27
2589
    let plist = match envelope {
28
2588
        Expr::List(items) => items,
29
1
        _ => return Err(EnvelopeError::NotPlist),
30
    };
31
2588
    let pairs = collect_plist(plist)?;
32
2588
    let id_expr = pairs
33
2588
        .iter()
34
2588
        .find(|(k, _)| k == "ID")
35
2588
        .map(|(_, v)| v.clone())
36
2588
        .ok_or(EnvelopeError::MissingKey(":id"))?;
37
2587
    let id = expr_to_request_id(&id_expr)?;
38
5174
    let has_value = pairs.iter().any(|(k, _)| k == "VALUE");
39
5174
    let has_error = pairs.iter().any(|(k, _)| k == "ERROR");
40
2587
    match (has_value, has_error) {
41
2
        (true, true) => Err(EnvelopeError::InvalidValue(
42
2
            ":value/:error",
43
2
            "frame must contain exactly one of :value or :error, not both".into(),
44
2
        )),
45
        (true, false) => {
46
1832
            let value_expr = pairs
47
1832
                .into_iter()
48
3664
                .find(|(k, _)| k == "VALUE")
49
1832
                .map(|(_, v)| v)
50
1832
                .ok_or(EnvelopeError::MissingKey(":value"))?;
51
1832
            expr_to_value_depth(value_expr, 0)
52
1832
                .map(|v| Response {
53
1832
                    id,
54
1832
                    payload: ResponsePayload::Value(v),
55
1832
                })
56
1832
                .map_err(|e| {
57
                    EnvelopeError::InvalidValue(":value", format!("value too deeply nested: {e}"))
58
                })
59
        }
60
        (false, true) => {
61
427
            let error_expr = pairs
62
427
                .into_iter()
63
854
                .find(|(k, _)| k == "ERROR")
64
427
                .map(|(_, v)| v)
65
427
                .ok_or(EnvelopeError::MissingKey(":error"))?;
66
427
            let payload = parse_error_payload(error_expr)?;
67
427
            Ok(Response { id, payload })
68
        }
69
326
        (false, false) => Err(EnvelopeError::MissingKey(":value or :error")),
70
    }
71
2841
}
72

            
73
427
fn parse_error_payload(expr: Expr) -> Result<ResponsePayload, EnvelopeError> {
74
427
    let items = match expr {
75
427
        Expr::List(items) => items,
76
        _ => return Err(EnvelopeError::NotPlist),
77
    };
78
427
    let pairs = collect_plist(items)?;
79
427
    let code_expr = pairs
80
427
        .iter()
81
427
        .find(|(k, _)| k == "CODE")
82
427
        .map(|(_, v)| v.clone())
83
427
        .ok_or(EnvelopeError::MissingKey(":code"))?;
84
427
    let code_str = match code_expr {
85
427
        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
427
    let message = pairs
95
427
        .iter()
96
854
        .find(|(k, _)| k == "MESSAGE")
97
427
        .and_then(|(_, v)| match v {
98
427
            Expr::String(s) => Some(s.clone()),
99
            _ => None,
100
427
        })
101
427
        .ok_or(EnvelopeError::MissingKey(":message"))?;
102
427
    let detail = pairs
103
427
        .iter()
104
880
        .find(|(k, _)| k == "DETAIL")
105
427
        .and_then(|(_, v)| match v {
106
26
            Expr::String(s) => Some(s.clone()),
107
            _ => None,
108
26
        });
109
427
    Ok(ResponsePayload::Error {
110
427
        code: ErrorCode::new(code_str),
111
427
        message,
112
427
        detail,
113
427
    })
114
427
}
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.
124
2
pub fn expr_to_value(expr: Expr) -> Value {
125
2
    expr_to_value_depth(expr, 0).unwrap_or(Value::Nil)
126
2
}
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.
132
2325
pub fn try_expr_to_value(expr: Expr) -> Result<Value, EnvelopeError> {
133
2325
    expr_to_value_depth(expr, 0)
134
2325
        .map_err(|e| EnvelopeError::InvalidValue(":value", format!("value too deeply nested: {e}")))
135
2325
}
136

            
137
33445
fn expr_to_value_depth(expr: Expr, depth: usize) -> Result<Value, &'static str> {
138
33445
    if depth > MAX_VALUE_DEPTH {
139
2
        return Err("nesting depth exceeded");
140
33443
    }
141
33443
    let v = match expr {
142
555
        Expr::Nil => Value::Nil,
143
77
        Expr::Bool(b) => Value::Bool(b),
144
1728
        Expr::Number(n) => Value::Number(n),
145
10976
        Expr::String(s) => Value::String(s),
146
25
        Expr::Symbol(s) => Value::Symbol(s),
147
13951
        Expr::Keyword(name) => Value::Symbol(format!(":{}", name.to_lowercase())),
148
25
        Expr::Bytes(b) => Value::Bytes(b),
149
5976
        Expr::List(items) => {
150
5976
            let converted: Result<Vec<Value>, _> = items
151
5976
                .into_iter()
152
29027
                .map(|e| expr_to_value_depth(e, depth + 1))
153
5976
                .collect();
154
5976
            vec_to_list(converted?)
155
        }
156
130
        Expr::Cons(car, cdr) => Pair::cons(
157
130
            expr_to_value_depth(*car, depth + 1)?,
158
128
            expr_to_value_depth(*cdr, depth + 1)?,
159
        ),
160
        Expr::RuntimeValue(v) => v,
161
        _ => Value::Nil,
162
    };
163
33313
    Ok(v)
164
33445
}
165

            
166
#[cfg(test)]
167
mod 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
8
    fn round_trip(resp: Response) {
176
8
        let wire = format_response(&resp);
177
8
        let parsed = parse_response(&wire).unwrap();
178
8
        assert_eq!(parsed, resp, "round-trip failed for wire: {wire:?}");
179
8
    }
180

            
181
    #[test]
182
1
    fn round_trip_value_number() {
183
1
        round_trip(Response {
184
1
            id: RequestId::Int(1),
185
1
            payload: ResponsePayload::Value(Value::Number(Fraction::from_integer(42))),
186
1
        });
187
1
    }
188

            
189
    #[test]
190
1
    fn round_trip_value_bool_true() {
191
1
        round_trip(Response {
192
1
            id: RequestId::Int(2),
193
1
            payload: ResponsePayload::Value(Value::Bool(true)),
194
1
        });
195
1
    }
196

            
197
    #[test]
198
1
    fn round_trip_value_nil() {
199
1
        round_trip(Response {
200
1
            id: RequestId::Int(4),
201
1
            payload: ResponsePayload::Value(Value::Nil),
202
1
        });
203
1
    }
204

            
205
    #[test]
206
1
    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
1
        let resp = Response {
211
1
            id: RequestId::Int(99),
212
1
            payload: ResponsePayload::Value(Value::Bool(false)),
213
1
        };
214
1
        let wire = format_response(&resp);
215
1
        let parsed = parse_response(&wire).unwrap();
216
1
        assert_eq!(parsed.payload, ResponsePayload::Value(Value::Nil));
217
1
    }
218

            
219
    #[test]
220
1
    fn round_trip_value_string() {
221
1
        round_trip(Response {
222
1
            id: RequestId::Int(5),
223
1
            payload: ResponsePayload::Value(Value::String("hello world".into())),
224
1
        });
225
1
    }
226

            
227
    #[test]
228
1
    fn round_trip_value_pair() {
229
1
        let list = Pair::cons(
230
1
            Value::Number(Fraction::from_integer(10)),
231
1
            Pair::cons(Value::Number(Fraction::from_integer(20)), Value::Nil),
232
        );
233
1
        round_trip(Response {
234
1
            id: RequestId::Int(6),
235
1
            payload: ResponsePayload::Value(list),
236
1
        });
237
1
    }
238

            
239
    #[test]
240
1
    fn round_trip_error_without_detail() {
241
1
        round_trip(Response {
242
1
            id: RequestId::Int(7),
243
1
            payload: ResponsePayload::Error {
244
1
                code: ErrorCode::new(ErrorCode::ARGS),
245
1
                message: "bad args".into(),
246
1
                detail: None,
247
1
            },
248
1
        });
249
1
    }
250

            
251
    #[test]
252
1
    fn round_trip_error_with_detail() {
253
1
        round_trip(Response {
254
1
            id: RequestId::Int(8),
255
1
            payload: ResponsePayload::Error {
256
1
                code: ErrorCode::new(ErrorCode::DB),
257
1
                message: "query failed".into(),
258
1
                detail: Some("SqlxError(...)".into()),
259
1
            },
260
1
        });
261
1
    }
262

            
263
    #[test]
264
1
    fn round_trip_string_id() {
265
1
        round_trip(Response {
266
1
            id: RequestId::String("req-abc".into()),
267
1
            payload: ResponsePayload::Value(Value::Bool(true)),
268
1
        });
269
1
    }
270

            
271
    #[test]
272
1
    fn parse_response_rejects_empty_input() {
273
1
        let err = parse_response("").unwrap_err();
274
1
        assert!(matches!(err, EnvelopeError::NotSingleExpr));
275
1
    }
276

            
277
    #[test]
278
1
    fn parse_response_rejects_non_list() {
279
1
        let err = parse_response("42").unwrap_err();
280
1
        assert!(matches!(err, EnvelopeError::NotPlist));
281
1
    }
282

            
283
    #[test]
284
1
    fn parse_response_rejects_missing_id() {
285
1
        let err = parse_response("(:value 1)").unwrap_err();
286
1
        assert!(matches!(err, EnvelopeError::MissingKey(":id")));
287
1
    }
288

            
289
    #[test]
290
1
    fn parse_response_rejects_missing_value_and_error() {
291
1
        let err = parse_response("(:id 1)").unwrap_err();
292
1
        assert!(matches!(err, EnvelopeError::MissingKey(":value or :error")));
293
1
    }
294

            
295
    #[test]
296
1
    fn parse_response_rejects_multiple_top_level() {
297
1
        let err = parse_response("(:id 1 :value 1) (:id 2 :value 2)").unwrap_err();
298
1
        assert!(matches!(err, EnvelopeError::NotSingleExpr));
299
1
    }
300

            
301
    #[test]
302
1
    fn parse_response_rejects_both_value_and_error_value_first() {
303
1
        let err =
304
1
            parse_response(r#"(:id 1 :value 42 :error (:code args :message "bad"))"#).unwrap_err();
305
1
        assert!(matches!(
306
1
            err,
307
1
            EnvelopeError::InvalidValue(":value/:error", _)
308
        ));
309
1
    }
310

            
311
    #[test]
312
1
    fn parse_response_rejects_both_value_and_error_error_first() {
313
1
        let err =
314
1
            parse_response(r#"(:id 1 :error (:code args :message "bad") :value 42)"#).unwrap_err();
315
1
        assert!(matches!(
316
1
            err,
317
1
            EnvelopeError::InvalidValue(":value/:error", _)
318
        ));
319
1
    }
320

            
321
    #[test]
322
1
    fn expr_to_value_keyword_maps_to_symbol_with_colon() {
323
1
        let expr = Expr::Keyword("COMMODITY-ID".into());
324
1
        assert_eq!(expr_to_value(expr), Value::Symbol(":commodity-id".into()));
325
1
    }
326

            
327
    #[test]
328
1
    fn expr_to_value_deeply_nested_returns_nil_not_abort() {
329
        // Build a deeply nested Cons chain exceeding MAX_VALUE_DEPTH
330
1
        let mut expr = Expr::Nil;
331
74
        for _ in 0..MAX_VALUE_DEPTH + 10 {
332
74
            expr = Expr::Cons(Box::new(Expr::Nil), Box::new(expr));
333
74
        }
334
        // expr_to_value must return without stack overflow, capping at Nil
335
1
        let _ = expr_to_value(expr);
336
1
    }
337

            
338
    #[test]
339
1
    fn expr_to_value_depth_errors_on_deep_input() {
340
1
        let mut expr = Expr::Nil;
341
74
        for _ in 0..MAX_VALUE_DEPTH + 10 {
342
74
            expr = Expr::Cons(Box::new(Expr::Nil), Box::new(expr));
343
74
        }
344
1
        let result = expr_to_value_depth(expr, 0);
345
1
        assert!(result.is_err(), "expected depth error, got: {result:?}");
346
1
    }
347
}