Lines
94.65 %
Functions
89.06 %
Branches
100 %
//! Response parsing: the inverse of [`super::envelope::format_response`].
use nomiscript::{Expr, Pair, Reader, Value, vec_to_list};
use super::envelope::{
EnvelopeError, ErrorCode, Response, ResponsePayload, collect_plist, expr_to_request_id,
};
/// Maximum nesting depth for [`expr_to_value`] to guard against stack overflow.
const MAX_VALUE_DEPTH: usize = 64;
/// Parse a wire response frame into a [`Response`].
///
/// Accepts `(:id N :value V)` or `(:id N :error (:code sym :message "..." :detail "..."))`.
/// Rejects frames containing both `:value` and `:error` simultaneously.
/// This is the inverse of [`super::envelope::format_response`].
/// Note: `#f` and `NIL` are reader-equivalent in nomiscript; both decode to
/// `Value::Nil`. `Bool(false)` does not survive a round-trip through the wire.
pub fn parse_response(wire: &str) -> Result<Response, EnvelopeError> {
let program = Reader::parse(wire).map_err(|e| EnvelopeError::Parse(e.to_string()))?;
let mut iter = program.exprs.into_iter();
let envelope = iter.next().ok_or(EnvelopeError::NotSingleExpr)?;
if iter.next().is_some() {
return Err(EnvelopeError::NotSingleExpr);
}
let plist = match envelope {
Expr::List(items) => items,
_ => return Err(EnvelopeError::NotPlist),
let pairs = collect_plist(plist)?;
let id_expr = pairs
.iter()
.find(|(k, _)| k == "ID")
.map(|(_, v)| v.clone())
.ok_or(EnvelopeError::MissingKey(":id"))?;
let id = expr_to_request_id(&id_expr)?;
let has_value = pairs.iter().any(|(k, _)| k == "VALUE");
let has_error = pairs.iter().any(|(k, _)| k == "ERROR");
match (has_value, has_error) {
(true, true) => Err(EnvelopeError::InvalidValue(
":value/:error",
"frame must contain exactly one of :value or :error, not both".into(),
)),
(true, false) => {
let value_expr = pairs
.into_iter()
.find(|(k, _)| k == "VALUE")
.map(|(_, v)| v)
.ok_or(EnvelopeError::MissingKey(":value"))?;
expr_to_value_depth(value_expr, 0)
.map(|v| Response {
id,
payload: ResponsePayload::Value(v),
})
.map_err(|e| {
EnvelopeError::InvalidValue(":value", format!("value too deeply nested: {e}"))
(false, true) => {
let error_expr = pairs
.find(|(k, _)| k == "ERROR")
.ok_or(EnvelopeError::MissingKey(":error"))?;
let payload = parse_error_payload(error_expr)?;
Ok(Response { id, payload })
(false, false) => Err(EnvelopeError::MissingKey(":value or :error")),
fn parse_error_payload(expr: Expr) -> Result<ResponsePayload, EnvelopeError> {
let items = match expr {
let pairs = collect_plist(items)?;
let code_expr = pairs
.find(|(k, _)| k == "CODE")
.ok_or(EnvelopeError::MissingKey(":code"))?;
let code_str = match code_expr {
Expr::Symbol(s) => s.to_lowercase(),
Expr::Keyword(s) => s.to_lowercase(),
other => {
return Err(EnvelopeError::InvalidValue(
":code",
format!("expected symbol, got {other:?}"),
));
let message = pairs
.find(|(k, _)| k == "MESSAGE")
.and_then(|(_, v)| match v {
Expr::String(s) => Some(s.clone()),
_ => None,
.ok_or(EnvelopeError::MissingKey(":message"))?;
let detail = pairs
.find(|(k, _)| k == "DETAIL")
});
Ok(ResponsePayload::Error {
code: ErrorCode::new(code_str),
message,
detail,
/// Convert a parsed [`Expr`] to a runtime [`Value`].
/// Handles all structurally representable forms; compile-only forms
/// (lambdas, wasm locals, etc.) that cannot appear in wire output map to `Nil`.
/// Keywords (`:foo`) map to `Value::Symbol(":foo")` to preserve plist key identity.
/// Depth is bounded to [`MAX_VALUE_DEPTH`]; call [`expr_to_value_depth`] directly
/// when a depth budget is already in scope.
pub fn expr_to_value(expr: Expr) -> Value {
expr_to_value_depth(expr, 0).unwrap_or(Value::Nil)
/// Fallible variant of [`expr_to_value`]: surfaces depth overflow as an
/// [`EnvelopeError`] instead of silently substituting `Value::Nil`. Callers
/// re-parsing untrusted printed lists (e.g. `cli-core::render::reparse_list`)
/// use this so over-nested data is reported, not masked as an empty result.
pub fn try_expr_to_value(expr: Expr) -> Result<Value, EnvelopeError> {
expr_to_value_depth(expr, 0)
.map_err(|e| EnvelopeError::InvalidValue(":value", format!("value too deeply nested: {e}")))
fn expr_to_value_depth(expr: Expr, depth: usize) -> Result<Value, &'static str> {
if depth > MAX_VALUE_DEPTH {
return Err("nesting depth exceeded");
let v = match expr {
Expr::Nil => Value::Nil,
Expr::Bool(b) => Value::Bool(b),
Expr::Number(n) => Value::Number(n),
Expr::String(s) => Value::String(s),
Expr::Symbol(s) => Value::Symbol(s),
Expr::Keyword(name) => Value::Symbol(format!(":{}", name.to_lowercase())),
Expr::Bytes(b) => Value::Bytes(b),
Expr::List(items) => {
let converted: Result<Vec<Value>, _> = items
.map(|e| expr_to_value_depth(e, depth + 1))
.collect();
vec_to_list(converted?)
Expr::Cons(car, cdr) => Pair::cons(
expr_to_value_depth(*car, depth + 1)?,
expr_to_value_depth(*cdr, depth + 1)?,
),
Expr::RuntimeValue(v) => v,
_ => Value::Nil,
Ok(v)
#[cfg(test)]
mod tests {
use nomiscript::{Fraction, Pair, Value};
use super::super::envelope::{
ErrorCode, RequestId, Response, ResponsePayload, format_response,
use super::*;
fn round_trip(resp: Response) {
let wire = format_response(&resp);
let parsed = parse_response(&wire).unwrap();
assert_eq!(parsed, resp, "round-trip failed for wire: {wire:?}");
#[test]
fn round_trip_value_number() {
round_trip(Response {
id: RequestId::Int(1),
payload: ResponsePayload::Value(Value::Number(Fraction::from_integer(42))),
fn round_trip_value_bool_true() {
id: RequestId::Int(2),
payload: ResponsePayload::Value(Value::Bool(true)),
fn round_trip_value_nil() {
id: RequestId::Int(4),
payload: ResponsePayload::Value(Value::Nil),
fn bool_false_round_trips_as_nil() {
// In nomiscript #f and NIL are reader-equivalent: format_value(Bool(false)) → "#f"
// and Reader::parse("#f") → Expr::Nil. Both are indistinguishable on the wire;
// parse_response normalises both to Value::Nil.
let resp = Response {
id: RequestId::Int(99),
payload: ResponsePayload::Value(Value::Bool(false)),
assert_eq!(parsed.payload, ResponsePayload::Value(Value::Nil));
fn round_trip_value_string() {
id: RequestId::Int(5),
payload: ResponsePayload::Value(Value::String("hello world".into())),
fn round_trip_value_pair() {
let list = Pair::cons(
Value::Number(Fraction::from_integer(10)),
Pair::cons(Value::Number(Fraction::from_integer(20)), Value::Nil),
);
id: RequestId::Int(6),
payload: ResponsePayload::Value(list),
fn round_trip_error_without_detail() {
id: RequestId::Int(7),
payload: ResponsePayload::Error {
code: ErrorCode::new(ErrorCode::ARGS),
message: "bad args".into(),
detail: None,
},
fn round_trip_error_with_detail() {
id: RequestId::Int(8),
code: ErrorCode::new(ErrorCode::DB),
message: "query failed".into(),
detail: Some("SqlxError(...)".into()),
fn round_trip_string_id() {
id: RequestId::String("req-abc".into()),
fn parse_response_rejects_empty_input() {
let err = parse_response("").unwrap_err();
assert!(matches!(err, EnvelopeError::NotSingleExpr));
fn parse_response_rejects_non_list() {
let err = parse_response("42").unwrap_err();
assert!(matches!(err, EnvelopeError::NotPlist));
fn parse_response_rejects_missing_id() {
let err = parse_response("(:value 1)").unwrap_err();
assert!(matches!(err, EnvelopeError::MissingKey(":id")));
fn parse_response_rejects_missing_value_and_error() {
let err = parse_response("(:id 1)").unwrap_err();
assert!(matches!(err, EnvelopeError::MissingKey(":value or :error")));
fn parse_response_rejects_multiple_top_level() {
let err = parse_response("(:id 1 :value 1) (:id 2 :value 2)").unwrap_err();
fn parse_response_rejects_both_value_and_error_value_first() {
let err =
parse_response(r#"(:id 1 :value 42 :error (:code args :message "bad"))"#).unwrap_err();
assert!(matches!(
err,
EnvelopeError::InvalidValue(":value/:error", _)
fn parse_response_rejects_both_value_and_error_error_first() {
parse_response(r#"(:id 1 :error (:code args :message "bad") :value 42)"#).unwrap_err();
fn expr_to_value_keyword_maps_to_symbol_with_colon() {
let expr = Expr::Keyword("COMMODITY-ID".into());
assert_eq!(expr_to_value(expr), Value::Symbol(":commodity-id".into()));
fn expr_to_value_deeply_nested_returns_nil_not_abort() {
// Build a deeply nested Cons chain exceeding MAX_VALUE_DEPTH
let mut expr = Expr::Nil;
for _ in 0..MAX_VALUE_DEPTH + 10 {
expr = Expr::Cons(Box::new(Expr::Nil), Box::new(expr));
// expr_to_value must return without stack overflow, capping at Nil
let _ = expr_to_value(expr);
fn expr_to_value_depth_errors_on_deep_input() {
let result = expr_to_value_depth(expr, 0);
assert!(result.is_err(), "expected depth error, got: {result:?}");