Engine: a parameter error carries its key and problem (EngineError::Param)

A form can show the problem beside the field it is about. The message
still names the key, as before, for agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 20:58:45 -04:00
1 parent 8df4ea040c
commit 9dcbb9c9a6
14 files changed
+60 -49

No files matched your search

+10 -13
View File
@@ -145,10 +145,7 @@ pub static ALL: &[CommandSpec] = &[
/// A required label parameter (1..=u32::MAX; the model checks its own range).
pub(crate) fn label(p: &Value, key: &str) -> Result<u32> {
p.get(key)
.and_then(Value::as_u64)
.and_then(|v| u32::try_from(v).ok())
.ok_or_else(|| EngineError::Params(format!("`{key}` must be a positive whole number")))
p.get(key).and_then(Value::as_u64).and_then(|v| u32::try_from(v).ok()).ok_or_else(|| EngineError::param(key, "must be a positive whole number"))
}
/// The first node and element labels after the model's largest, for `nodes` new nodes and
@@ -166,7 +163,7 @@ pub(crate) fn fresh_labels(model: &fieldcraft_model::Model, nodes: usize, elemen
/// A required `[x, y, z]` parameter.
pub(crate) fn xyz(p: &Value, key: &str) -> Result<[f64; 3]> {
p.get(key).and_then(xyz_value).ok_or_else(|| EngineError::Params(format!("`{key}` must be [x, y, z]")))
p.get(key).and_then(xyz_value).ok_or_else(|| EngineError::param(key, "must be [x, y, z]"))
}
/// A JSON `[x, y, z]`.
@@ -179,7 +176,7 @@ fn xyz_value(v: &Value) -> Option<[f64; 3]> {
/// A required list of `[x, y, z]` points, at most `max_len` long.
pub(crate) fn points(p: &Value, key: &str, max_len: usize) -> Result<Vec<[f64; 3]>> {
let bad = |what: String| EngineError::Params(format!("`{key}` must be a list of at most {max_len} [x, y, z]{what}"));
let bad = |what: String| EngineError::param(key, format!("must be a list of at most {max_len} [x, y, z]{what}"));
let raw = p.get(key).and_then(Value::as_array).filter(|a| a.len() <= max_len).ok_or_else(|| bad(String::new()))?;
raw.iter().enumerate().map(|(i, v)| xyz_value(v).ok_or_else(|| bad(format!(" (entry {i} is not)")))).collect()
}
@@ -190,11 +187,11 @@ pub(crate) fn labels(p: &Value, key: &str, max_len: usize) -> Result<Vec<u32>> {
.get(key)
.and_then(Value::as_array)
.filter(|a| a.len() <= max_len)
.ok_or_else(|| EngineError::Params(format!("`{key}` must be a list of at most {max_len} labels")))?;
.ok_or_else(|| EngineError::param(key, format!("must be a list of at most {max_len} labels")))?;
raw.iter()
.map(|v| v.as_u64().and_then(|n| u32::try_from(n).ok()).filter(|n| (1..=MAX_LABEL).contains(n)))
.collect::<Option<Vec<u32>>>()
.ok_or_else(|| EngineError::Params(format!("`{key}` must be labels in 1..={MAX_LABEL}")))
.ok_or_else(|| EngineError::param(key, format!("must be labels in 1..={MAX_LABEL}")))
}
/// A required path parameter, as given.
@@ -202,7 +199,7 @@ fn path_param<'a>(p: &'a Value, key: &str) -> Result<&'a str> {
p.get(key)
.and_then(Value::as_str)
.filter(|s| !s.trim().is_empty() && s.len() <= fieldcraft_io::MAX_PATH_LEN && !s.contains('\0'))
.ok_or_else(|| EngineError::Params(format!("`{key}` must be a file path")))
.ok_or_else(|| EngineError::param(key, "must be a file path"))
}
/// A required path parameter naming a file (or folder) the command reads or writes: the path
@@ -217,23 +214,23 @@ pub(crate) fn file_param(s: &Session, p: &Value, key: &str, access: Access) -> R
pub(crate) fn optional_number(p: &Value, key: &str, default: f64) -> Result<f64> {
match p.get(key) {
None => Ok(default),
Some(v) => v.as_f64().ok_or_else(|| EngineError::Params(format!("`{key}` must be a number"))),
Some(v) => v.as_f64().ok_or_else(|| EngineError::param(key, "must be a number")),
}
}
/// The `name` parameter.
pub(crate) fn name(p: &Value) -> Result<&str> {
p.get("name").and_then(Value::as_str).ok_or_else(|| EngineError::Params("`name` must be a name".into()))
p.get("name").and_then(Value::as_str).ok_or_else(|| EngineError::param("name", "must be a name"))
}
/// An optional parameter read as `T`'s JSON form; `None` when it is absent.
pub(crate) fn opt<'a, T: Deserialize<'a>>(p: &'a Value, key: &str) -> Result<Option<T>> {
p.get(key).map(|v| T::deserialize(v).map_err(|e| EngineError::Params(format!("`{key}`: {e}")))).transpose()
p.get(key).map(|v| T::deserialize(v).map_err(|e| EngineError::param(key, format!("is not valid: {e}")))).transpose()
}
/// A required parameter read as `T`'s JSON form.
pub(crate) fn req<'a, T: Deserialize<'a>>(p: &'a Value, key: &str) -> Result<T> {
opt(p, key)?.ok_or_else(|| EngineError::Params(format!("`{key}` is required")))
opt(p, key)?.ok_or_else(|| EngineError::param(key, "is required"))
}
/// Entries listed in a report: the first ones, and how many there are.