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>
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!(