config: name the key path in setup errors
Assisted-by: Claude Opus 5.5 via Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
change ztyppkvpoozmoxuonlntpztnxpwtvwvm
commit 61710bafa02c1927b379941ddc5eadf775e88fc1
author Alpha Chen <alpha@kejadlen.dev>
date
parent kmtrxvqk
diff --git a/Cargo.lock b/Cargo.lock
index 701ea1b..1f2fc6b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -417,6 +417,12 @@ version = "1.70.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
 
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
 [[package]]
 name = "lazy_static"
 version = "1.5.0"
@@ -645,6 +651,7 @@ dependencies = [
  "mlua",
  "predicates",
  "serde",
+ "serde_path_to_error",
  "tempfile",
  "thiserror",
  "tokio",
@@ -861,6 +868,17 @@ dependencies = [
  "syn 3.0.6",
 ]
 
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
 [[package]]
 name = "sharded-slab"
 version = "0.1.7"
diff --git a/Cargo.toml b/Cargo.toml
index a6d7734..d1bd347 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,7 @@ fs-err = "*"
 miette = { version = "*", features = ["fancy"] }
 mlua = { version = "*", features = ["lua54", "vendored", "send", "serde"] }
 serde = { version = "*", features = ["derive"] }
+serde_path_to_error = "*"
 thiserror = "*"
 tokio = { version = "*", features = ["full"] }
 tracing = "*"
diff --git a/src/parallax.rs b/src/parallax.rs
index 3fc065c..b70a77e 100644
--- a/src/parallax.rs
+++ b/src/parallax.rs
@@ -13,9 +13,9 @@ use mlua::ErrorContext as _;
 use mlua::FromLua;
 use mlua::Function;
 use mlua::Lua;
-use mlua::LuaSerdeExt as _;
 use mlua::Table;
 use mlua::Value;
+use mlua::serde::Deserializer;
 
 use crate::Error;
 use crate::spec::Setup;
@@ -41,11 +41,20 @@ impl FromLua for Config {
         let spec = shallow_copy(lua, &table)?;
         let server = pop_function(&spec, "server", lua)?;
         let flow = pop_function(&spec, "flow", lua)?;
-        let setup: Setup = lua
-            .from_value(Value::Table(spec))
-            .map_err(|error| match error {
-                mlua::Error::DeserializeError(message) => config_error(lua, message),
-                other => other,
+        // The whole call usually sits on one line, so the key path is
+        // what locates a mistake inside the table.
+        let setup: Setup = serde_path_to_error::deserialize(Deserializer::new(Value::Table(spec)))
+            .map_err(|error| {
+                let path = error.path().to_string();
+                match error.into_inner() {
+                    mlua::Error::DeserializeError(message) if path == "." => {
+                        config_error(lua, message)
+                    }
+                    mlua::Error::DeserializeError(message) => {
+                        config_error(lua, format!("{path}: {message}"))
+                    }
+                    other => other,
+                }
             })?;
         Ok(Config {
             server,
diff --git a/tests/cli.rs b/tests/cli.rs
index 8bded2b..8131d68 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -108,6 +108,22 @@ fn unknown_field_errors_point_at_the_parallax_call() {
         );
 }
 
+#[test]
+fn nested_errors_name_the_key_path() {
+    let (_dir, path) = config_file(concat!(
+        "(local parallax (require :parallax))\n",
+        "(parallax {:variants {:desktop {:viewport [1280 900] :shceme :dark}}\n",
+        "           :server (fn [] {:cmd [\"true\"]}) :flow (fn [] nil)})\n",
+    ));
+    cargo_bin_cmd!("parallax")
+        .arg("list")
+        .arg("--config")
+        .arg(&path)
+        .assert()
+        .failure()
+        .stderr(predicate::str::contains("variants.desktop.shceme:"));
+}
+
 #[test]
 fn leaves_the_config_table_intact() {
     let (_dir, path) = config_file(concat!(