Engine: EngineError::Param carries the schema Problem at its field's path

The ~40 parameter checks give a ProblemKind instead of text; Session::execute looks the problem
up in the command's form (a group's missing field becomes that field's own path). Replies still
name keys; forms phrase the same problem by label. A test refuses every field of every form and
checks each refusal names a field of that form.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 22:42:19 -04:00
1 parent de8b7b6321
commit 65fb1112ab
16 files changed
+172 -87

No files matched your search

+8 -7
View File
@@ -33,7 +33,8 @@ pub struct Context<'a> {
pub touched: &'a BTreeSet<String>,
/// OK was tried: every problem shows.
pub attempted: bool,
/// The command refused a parameter: its path and the problem, shown beside the field.
/// The command refused a parameter: its path and the problem as the form phrases it, shown
/// beside the field.
pub refused: Option<(&'a str, &'a str)>,
/// What is missing or wrong in the parameters ([`messages`]).
pub problems: &'a [(String, String)],
@@ -146,9 +147,9 @@ pub fn show(ui: &mut Ui, t: &Tokens, cx: &Context, fields: &'static [Field], par
impl Form<'_> {
/// The problem with the field at `path`, under it: the command's refusal at once, the
/// form's own once the field was edited or OK was tried.
fn problem_note(&self, ui: &mut Ui, path: &str, label: &str) {
fn problem_note(&self, ui: &mut Ui, path: &str) {
let message = match self.cx.refused {
Some((key, problem)) if key == path => Some(format!("{label}: {problem}")),
Some((key, message)) if key == path => Some(message.to_string()),
_ => self.problems.get(path).filter(|_| self.cx.attempted || self.cx.touched.contains(path)).cloned(),
};
if let Some(message) = message {
@@ -190,7 +191,7 @@ impl Form<'_> {
*obj = schema::switch_variant(f, common, obj, &t);
self.touch(&path);
}
self.problem_note(ui, &path, f.label);
self.problem_note(ui, &path);
if let Some(v) = obj.get(tag).and_then(Value::as_str).and_then(|t| f.kind.variant(t)) {
self.fields(ui, v.fields, common, obj, prefix, depth + 1);
}
@@ -234,7 +235,7 @@ impl Form<'_> {
}
}
}
self.problem_note(ui, &path, f.label);
self.problem_note(ui, &path);
}
/// A one-row field; returns its new value (`Null` removes it).
@@ -504,7 +505,7 @@ impl Form<'_> {
*child = schema::switch_variant(f, &[], child, &t);
self.touch(&tag_path);
}
self.problem_note(ui, &tag_path, row.label);
self.problem_note(ui, &tag_path);
if let Some(v) = child.get(tag).and_then(Value::as_str).and_then(|t| f.kind.variant(t)) {
self.fields(ui, v.fields, v.fields, child, path, depth + 1);
}
@@ -830,7 +831,7 @@ mod tests {
assert!(draw(&none, true, None).contains("Material: choose a material"));
let touched: BTreeSet<String> = ["name".to_string()].into();
assert!(draw(&touched, false, None).contains("Material: choose a material"));
let refused = draw(&none, false, Some(("name", "names no material")));
let refused = draw(&none, false, Some(("name", "Material: names no material")));
assert!(refused.contains("Material: names no material") && !refused.contains("choose a material"), "{refused}");
}
+13 -15
View File
@@ -164,7 +164,8 @@ pub struct ParamsDialog {
/// The parameters as JSON text.
pub text: String,
pub error: Option<String>,
/// The command refused one parameter: its path and the problem, shown beside its field.
/// The command refused one parameter: its path and the problem phrased for the form, shown
/// beside its field.
pub refused: Option<(String, String)>,
/// The paths of the fields edited: their problems show; the others wait for OK.
pub touched: std::collections::BTreeSet<String>,
@@ -179,7 +180,7 @@ pub struct ParamsDialog {
pub back: Option<Box<Back>>,
}
/// A command that failed: its message, and the parameter it refused (path, problem).
/// A command that failed: its message, and the parameter it refused (path, form message).
struct Failure {
message: String,
refused: Option<(String, String)>,
@@ -486,22 +487,20 @@ impl ModelApp {
/// Run a command with JSON parameters: what every panel, the control channel and scripts
/// do. The result or the error goes to the status bar.
pub fn run(&mut self, id: &str, params: Value) -> Result<Value, String> {
self.run_form(id, params, None).map_err(|f| f.message)
self.run_form(id, params, false).map_err(|f| f.message)
}
/// [`Self::run`] from a form with `fields`: a refused parameter is named by its field's label
/// (the engine's message names its key, which agents need).
fn run_form(&mut self, id: &str, params: Value, fields: Option<&'static [fieldcraft_engine::schema::Field]>) -> Result<Value, Failure> {
/// [`Self::run`], from a form when `form`: a refused parameter is then named by its field's
/// label (the engine's message names its key, which agents need).
fn run_form(&mut self, id: &str, params: Value, form: bool) -> Result<Value, Failure> {
self.ensure_idle().map_err(|message| Failure { message, refused: None })?;
let revision = self.session.revision();
let label = command_label(id);
let r = self.session.execute(id, &params).map_err(|e| match e {
EngineError::Param { key, problem } => {
let message = match fields.and_then(|f| fieldcraft_engine::schema::field_at(f, &key)) {
Some(f) => format!("{}: {problem}", f.label),
None => format!("`{key}` {problem}"),
};
Failure { message, refused: Some((key, problem)) }
EngineError::Param(p) => {
let message = if form { p.for_form() } else { p.for_agents() };
let shown = p.for_form();
Failure { refused: Some((p.path, shown)), message }
}
e => Failure { message: e.to_string(), refused: None },
});
@@ -677,8 +676,7 @@ impl ModelApp {
/// Run the open parameter form; it closes when the command succeeds.
pub fn run_dialog(&mut self) -> Result<Value, String> {
let (command, params) = self.dialog_params()?;
let fields = fieldcraft_engine::find_command(command).and_then(|c| c.form);
match self.run_form(command, params, fields) {
match self.run_form(command, params, true) {
Ok(v) => {
self.dialog_done();
Ok(v)
@@ -1597,7 +1595,7 @@ mod tests {
let e = app.run_dialog().unwrap_err();
assert_eq!(e, "Material: must be a name");
let d = app.dialog.as_ref().unwrap();
assert_eq!(d.refused, Some(("name".to_string(), "must be a name".to_string())));
assert_eq!(d.refused, Some(("name".to_string(), "Material: must be a name".to_string())));
assert!(app.status.as_ref().is_some_and(|s| s.text == "Plasticity: Material: must be a name"));
let raw = app.session.execute("model.material.plasticity", &json!({"name": 7})).unwrap_err();
assert_eq!(raw.to_string(), "`name` must be a name");