forked from 1b-agent/fieldcraft-upstream
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
de8b7b6321
commit
65fb1112ab
16 files changed
+172
-87
No files matched your search
@@ -23,6 +23,7 @@ pub(crate) mod solve;
|
||||
mod symmetry;
|
||||
mod tags;
|
||||
|
||||
use crate::schema::ProblemKind;
|
||||
use fieldcraft_model::MAX_LABEL;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
@@ -143,9 +144,18 @@ pub static ALL: &[CommandSpec] = &[
|
||||
results::CLEAR,
|
||||
];
|
||||
|
||||
/// The problem with required parameter `key`: [`ProblemKind::Required`] when it is left out,
|
||||
/// else `kind`.
|
||||
pub(crate) fn wrong(p: &Value, key: &str, kind: ProblemKind) -> EngineError {
|
||||
EngineError::param(key, if p.get(key).is_none_or(Value::is_null) { ProblemKind::Required } else { kind })
|
||||
}
|
||||
|
||||
/// 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::param(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(|| wrong(p, key, ProblemKind::NotAWholeNumber { min: 1, max: i64::from(u32::MAX) }))
|
||||
}
|
||||
|
||||
/// The first node and element labels after the model's largest, for `nodes` new nodes and
|
||||
@@ -163,7 +173,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::param(key, "must be [x, y, z]"))
|
||||
p.get(key).and_then(xyz_value).ok_or_else(|| wrong(p, key, ProblemKind::NotAVector { len: 3 }))
|
||||
}
|
||||
|
||||
/// A JSON `[x, y, z]`.
|
||||
@@ -176,9 +186,12 @@ 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::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()
|
||||
let raw = p.get(key).and_then(Value::as_array).filter(|a| a.len() <= max_len);
|
||||
let raw = raw.ok_or_else(|| wrong(p, key, ProblemKind::NotAList { max: max_len, what: "[x, y, z]" }))?;
|
||||
raw.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| xyz_value(v).ok_or_else(|| EngineError::invalid(key, format!("must be a list of [x, y, z] (entry {i} is not)"))))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A required list of labels (each in 1..=MAX_LABEL), at most `max_len` long.
|
||||
@@ -187,11 +200,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::param(key, format!("must be a list of at most {max_len} labels")))?;
|
||||
.ok_or_else(|| wrong(p, key, ProblemKind::NotAList { max: max_len, what: "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::param(key, format!("must be labels in 1..={MAX_LABEL}")))
|
||||
.ok_or_else(|| EngineError::invalid(key, format!("must be labels in 1..={MAX_LABEL}")))
|
||||
}
|
||||
|
||||
/// A required path parameter, as given.
|
||||
@@ -199,7 +212,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::param(key, "must be a file path"))
|
||||
.ok_or_else(|| wrong(p, key, ProblemKind::NotAPath))
|
||||
}
|
||||
|
||||
/// A required path parameter naming a file (or folder) the command reads or writes: the path
|
||||
@@ -214,23 +227,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::param(key, "must be a number")),
|
||||
Some(v) => v.as_f64().ok_or_else(|| EngineError::param(key, ProblemKind::NotANumber)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The `name` parameter.
|
||||
pub(crate) fn name(p: &Value) -> Result<&str> {
|
||||
p.get("name").and_then(Value::as_str).ok_or_else(|| EngineError::param("name", "must be a name"))
|
||||
p.get("name").and_then(Value::as_str).ok_or_else(|| wrong(p, "name", ProblemKind::NotAName))
|
||||
}
|
||||
|
||||
/// 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::param(key, format!("is not valid: {e}")))).transpose()
|
||||
p.get(key).map(|v| T::deserialize(v).map_err(|e| EngineError::invalid(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::param(key, "is required"))
|
||||
opt(p, key)?.ok_or_else(|| EngineError::param(key, ProblemKind::Required))
|
||||
}
|
||||
|
||||
/// Entries listed in a report: the first ones, and how many there are.
|
||||
|
||||
Reference in new issue
Block a user