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

            
70
496
fn parse_error_payload(expr: Expr) -> Result<ResponsePayload, EnvelopeError> {
71
496
    let items = match expr {
72
496
        Expr::List(items) => items,
73
        _ => return Err(EnvelopeError::NotPlist),
74
    };
75
496
    let pairs = collect_plist(items)?;
76
496
    let code_expr = pairs
77
496
        .iter()
78
496
        .find(|(k, _)| k == "CODE")
79
496
        .map(|(_, v)| v.clone())
80
496
        .ok_or(EnvelopeError::MissingKey(":code"))?;
81
496
    let code_str = match code_expr {
82
496
        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
496
    let message = pairs
92
496
        .iter()
93
992
        .find(|(k, _)| k == "MESSAGE")
94
496
        .and_then(|(_, v)| match v {
95
496
            Expr::String(s) => Some(s.clone()),
96
            _ => None,
97
496
        })
98
496
        .ok_or(EnvelopeError::MissingKey(":message"))?;
99
496
    let detail = pairs
100
496
        .iter()
101
1071
        .find(|(k, _)| k == "DETAIL")
102
496
        .and_then(|(_, v)| match v {
103
79
            Expr::String(s) => Some(s.clone()),
104
            _ => None,
105
79
        });
106
496
    Ok(ResponsePayload::Error {
107
496
        code: ErrorCode::new(code_str),
108
496
        message,
109
496
        detail,
110
496
    })
111
496
}
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.
121
2
pub fn expr_to_value(expr: Expr) -> Value {
122
2
    expr_to_value_depth(expr, 0).unwrap_or(Value::Nil)
123
2
}
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.
129
2730
pub fn try_expr_to_value(expr: Expr) -> Result<Value, EnvelopeError> {
130
2730
    expr_to_value_depth(expr, 0)
131
2730
        .map_err(|e| EnvelopeError::InvalidValue(":value", format!("value too deeply nested: {e}")))
132
2730
}
133

            
134
37137
fn expr_to_value_depth(expr: Expr, depth: usize) -> Result<Value, &'static str> {
135
37137
    if depth > MAX_VALUE_DEPTH {
136
2
        return Err("nesting depth exceeded");
137
37135
    }
138
37135
    let v = match expr {
139
1429
        Expr::Nil => Value::Nil,
140
132
        Expr::T => Value::T,
141
1953
        Expr::Number(n) => Value::Number(n),
142
12039
        Expr::String(s) => Value::String(s),
143
26
        Expr::Symbol(s) => Value::Symbol(s),
144
15159
        Expr::Keyword(name) => Value::Symbol(format!(":{}", name.to_lowercase())),
145
52
        Expr::Bytes(b) => Value::Bytes(b),
146
6215
        Expr::List(items) => {
147
6215
            let converted: Result<Vec<Value>, _> = items
148
6215
                .into_iter()
149
31488
                .map(|e| expr_to_value_depth(e, depth + 1))
150
6215
                .collect();
151
6215
            vec_to_list(converted?)
152
        }
153
130
        Expr::Cons(car, cdr) => Pair::cons(
154
130
            expr_to_value_depth(*car, depth + 1)?,
155
128
            expr_to_value_depth(*cdr, depth + 1)?,
156
        ),
157
        Expr::RuntimeValue(v) => v,
158
        _ => Value::Nil,
159
    };
160
37005
    Ok(v)
161
37137
}
162

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

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

            
186
    #[test]
187
1
    fn round_trip_value_t() {
188
1
        round_trip(Response {
189
1
            id: RequestId::Int(2),
190
1
            payload: ResponsePayload::Value(Value::T),
191
1
        });
192
1
    }
193

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

            
202
    #[test]
203
1
    fn round_trip_value_string() {
204
1
        round_trip(Response {
205
1
            id: RequestId::Int(5),
206
1
            payload: ResponsePayload::Value(Value::String("hello world".into())),
207
1
        });
208
1
    }
209

            
210
    #[test]
211
1
    fn round_trip_value_pair() {
212
1
        let list = Pair::cons(
213
1
            Value::Number(Fraction::from_integer(10)),
214
1
            Pair::cons(Value::Number(Fraction::from_integer(20)), Value::Nil),
215
        );
216
1
        round_trip(Response {
217
1
            id: RequestId::Int(6),
218
1
            payload: ResponsePayload::Value(list),
219
1
        });
220
1
    }
221

            
222
    #[test]
223
1
    fn round_trip_error_without_detail() {
224
1
        round_trip(Response {
225
1
            id: RequestId::Int(7),
226
1
            payload: ResponsePayload::Error {
227
1
                code: ErrorCode::new(ErrorCode::ARGS),
228
1
                message: "bad args".into(),
229
1
                detail: None,
230
1
            },
231
1
        });
232
1
    }
233

            
234
    #[test]
235
1
    fn round_trip_error_with_detail() {
236
1
        round_trip(Response {
237
1
            id: RequestId::Int(8),
238
1
            payload: ResponsePayload::Error {
239
1
                code: ErrorCode::new(ErrorCode::DB),
240
1
                message: "query failed".into(),
241
1
                detail: Some("SqlxError(...)".into()),
242
1
            },
243
1
        });
244
1
    }
245

            
246
    #[test]
247
1
    fn round_trip_string_id() {
248
1
        round_trip(Response {
249
1
            id: RequestId::String("req-abc".into()),
250
1
            payload: ResponsePayload::Value(Value::T),
251
1
        });
252
1
    }
253

            
254
    #[test]
255
1
    fn parse_response_rejects_empty_input() {
256
1
        let err = parse_response("").unwrap_err();
257
1
        assert!(matches!(err, EnvelopeError::NotSingleExpr));
258
1
    }
259

            
260
    #[test]
261
1
    fn parse_response_rejects_non_list() {
262
1
        let err = parse_response("42").unwrap_err();
263
1
        assert!(matches!(err, EnvelopeError::NotPlist));
264
1
    }
265

            
266
    #[test]
267
1
    fn parse_response_rejects_missing_id() {
268
1
        let err = parse_response("(:value 1)").unwrap_err();
269
1
        assert!(matches!(err, EnvelopeError::MissingKey(":id")));
270
1
    }
271

            
272
    #[test]
273
1
    fn parse_response_rejects_missing_value_and_error() {
274
1
        let err = parse_response("(:id 1)").unwrap_err();
275
1
        assert!(matches!(err, EnvelopeError::MissingKey(":value or :error")));
276
1
    }
277

            
278
    #[test]
279
1
    fn parse_response_rejects_multiple_top_level() {
280
1
        let err = parse_response("(:id 1 :value 1) (:id 2 :value 2)").unwrap_err();
281
1
        assert!(matches!(err, EnvelopeError::NotSingleExpr));
282
1
    }
283

            
284
    #[test]
285
1
    fn parse_response_rejects_both_value_and_error_value_first() {
286
1
        let err =
287
1
            parse_response(r#"(:id 1 :value 42 :error (:code args :message "bad"))"#).unwrap_err();
288
1
        assert!(matches!(
289
1
            err,
290
1
            EnvelopeError::InvalidValue(":value/:error", _)
291
        ));
292
1
    }
293

            
294
    #[test]
295
1
    fn parse_response_rejects_both_value_and_error_error_first() {
296
1
        let err =
297
1
            parse_response(r#"(:id 1 :error (:code args :message "bad") :value 42)"#).unwrap_err();
298
1
        assert!(matches!(
299
1
            err,
300
1
            EnvelopeError::InvalidValue(":value/:error", _)
301
        ));
302
1
    }
303

            
304
    #[test]
305
1
    fn expr_to_value_keyword_maps_to_symbol_with_colon() {
306
1
        let expr = Expr::Keyword("COMMODITY-ID".into());
307
1
        assert_eq!(expr_to_value(expr), Value::Symbol(":commodity-id".into()));
308
1
    }
309

            
310
    #[test]
311
1
    fn expr_to_value_deeply_nested_returns_nil_not_abort() {
312
        // Build a deeply nested Cons chain exceeding MAX_VALUE_DEPTH
313
1
        let mut expr = Expr::Nil;
314
74
        for _ in 0..MAX_VALUE_DEPTH + 10 {
315
74
            expr = Expr::Cons(Box::new(Expr::Nil), Box::new(expr));
316
74
        }
317
        // expr_to_value must return without stack overflow, capping at Nil
318
1
        let _ = expr_to_value(expr);
319
1
    }
320

            
321
    #[test]
322
1
    fn expr_to_value_depth_errors_on_deep_input() {
323
1
        let mut expr = Expr::Nil;
324
74
        for _ in 0..MAX_VALUE_DEPTH + 10 {
325
74
            expr = Expr::Cons(Box::new(Expr::Nil), Box::new(expr));
326
74
        }
327
1
        let result = expr_to_value_depth(expr, 0);
328
1
        assert!(result.is_err(), "expected depth error, got: {result:?}");
329
1
    }
330
}