forked from 1b-agent/fieldcraft-upstream
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:
1 parent
8df4ea040c
commit
9dcbb9c9a6
14 files changed
+60
-49
No files matched your search
@@ -109,7 +109,7 @@ fn block_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let region = match (planar, &p.face) {
|
||||
(true, face) => GeometryRef::face(p.body.clone(), face.clone().unwrap_or_else(|| "area".into()))?,
|
||||
(false, None) => GeometryRef::body(p.body.clone())?,
|
||||
(false, Some(_)) => return Err(EngineError::Params("`face` names the region of 2D elements; a solid block fills its body".into())),
|
||||
(false, Some(_)) => return Err(EngineError::param("face", "names the region of 2D elements; a solid block fills its body")),
|
||||
};
|
||||
if planar && !p.faces.is_empty() {
|
||||
return Err(EngineError::Params("a 2D patch has no faces to name: name its edges".into()));
|
||||
|
||||
@@ -81,14 +81,14 @@ fn cells(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(v) => match v.as_u64() {
|
||||
Some(1) => false,
|
||||
Some(2) => true,
|
||||
_ => return Err(EngineError::Params("`order` must be 1 or 2".into())),
|
||||
_ => return Err(EngineError::param("order", "must be 1 or 2")),
|
||||
},
|
||||
};
|
||||
let default = p.get("default").map(Method::parse).transpose()?.unwrap_or(Method::Auto);
|
||||
let methods: BTreeMap<String, Method> = match p.get("methods") {
|
||||
None => BTreeMap::new(),
|
||||
Some(Value::Object(m)) => m.iter().map(|(k, v)| Ok((k.clone(), Method::parse(v)?))).collect::<Result<_>>()?,
|
||||
Some(_) => return Err(EngineError::Params("`methods` must map cell names to methods".into())),
|
||||
Some(_) => return Err(EngineError::param("methods", "must map cell names to methods")),
|
||||
};
|
||||
let design = s.design.clone().ok_or_else(|| EngineError::Params("open a design first".into()))?;
|
||||
let names: Vec<String> = match p.get("bodies") {
|
||||
|
||||
@@ -34,7 +34,7 @@ fn contact(p: &Value, surfaces: ContactSurfaces) -> Result<Contact> {
|
||||
|
||||
/// A required name-valued parameter.
|
||||
fn text<'a>(p: &'a Value, key: &str) -> Result<&'a str> {
|
||||
p.get(key).and_then(Value::as_str).ok_or_else(|| EngineError::Params(format!("`{key}` must be a set name")))
|
||||
p.get(key).and_then(Value::as_str).ok_or_else(|| EngineError::param(key, "must be a set name"))
|
||||
}
|
||||
|
||||
pub const PAIR: CommandSpec = CommandSpec {
|
||||
|
||||
@@ -126,7 +126,7 @@ fn plane_datum(by: PlaneBy) -> Result<Datum> {
|
||||
Ok(match by {
|
||||
PlaneBy::Offset { plane, offset } => {
|
||||
if !offset.is_finite() {
|
||||
return Err(EngineError::Params("`offset` must be a finite number".into()));
|
||||
return Err(EngineError::param("offset", "must be a finite number"));
|
||||
}
|
||||
let normal = match plane {
|
||||
Principal::Yz => [1.0, 0.0, 0.0],
|
||||
|
||||
@@ -1046,7 +1046,11 @@ mod tests {
|
||||
fill_required(&s, form, &mut p);
|
||||
}
|
||||
let missing: Vec<String> = problems(form, &p).into_iter().filter(|p| p.message == "required").map(|p| p.path).collect();
|
||||
match s.execute(c.id, &p) {
|
||||
let r = s.execute(c.id, &p).map_err(|e| match e {
|
||||
EngineError::Param { .. } => EngineError::Params(e.to_string()),
|
||||
e => e,
|
||||
});
|
||||
match r {
|
||||
// State, not parameters: the meshers' default is the open design's bodies, the
|
||||
// bar has no explicit step; activation and probes need one of several fields.
|
||||
Err(EngineError::Params(e)) if STATE.iter().any(|m| e.contains(m)) => {}
|
||||
|
||||
@@ -151,14 +151,14 @@ fn bodies(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn body<'a>(s: &'a Session, p: &Value) -> Result<&'a Shape> {
|
||||
let name = p.get("body").and_then(Value::as_str).ok_or_else(|| EngineError::Params("`body` must be a body name".into()))?;
|
||||
let name = p.get("body").and_then(Value::as_str).ok_or_else(|| EngineError::param("body", "must be a body name"))?;
|
||||
s.design.as_deref().and_then(|d| d.body(name)).ok_or_else(|| EngineError::Params(format!("no body `{name}` (open a design first)")))
|
||||
}
|
||||
|
||||
/// The body named by `body`, tessellated within `tolerance`.
|
||||
pub(crate) fn body_mesh<'a>(s: &'a Session, p: &Value) -> Result<(&'a Shape, SurfaceMesh)> {
|
||||
let shape = body(s, p)?;
|
||||
let tolerance = p.get("tolerance").and_then(Value::as_f64).ok_or_else(|| EngineError::Params("`tolerance` must be a number (mm)".into()))?;
|
||||
let tolerance = p.get("tolerance").and_then(Value::as_f64).ok_or_else(|| EngineError::param("tolerance", "must be a number (mm)"))?;
|
||||
Ok((shape, shape.tessellate(tolerance)?))
|
||||
}
|
||||
|
||||
|
||||
@@ -53,11 +53,9 @@ fn analytic(s: FaceSurface) -> Surface {
|
||||
pub(super) fn number(p: &Value, key: &str) -> Result<Option<f64>> {
|
||||
match p.get(key) {
|
||||
None => Ok(None),
|
||||
Some(v) => v
|
||||
.as_f64()
|
||||
.filter(|x| x.is_finite() && *x > 0.0)
|
||||
.map(Some)
|
||||
.ok_or_else(|| EngineError::Params(format!("`{key}` must be a positive number (mm)"))),
|
||||
Some(v) => {
|
||||
v.as_f64().filter(|x| x.is_finite() && *x > 0.0).map(Some).ok_or_else(|| EngineError::param(key, "must be a positive number (mm)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +78,7 @@ pub(super) fn inputs<'a>(s: &'a Session, p: &Value) -> Result<Vec<Input<'a>>> {
|
||||
let tolerance = number(p, "tolerance")?;
|
||||
match (p.get("body"), p.get("surface")) {
|
||||
(Some(b), None) => {
|
||||
let name = b.as_str().ok_or_else(|| EngineError::Params("`body` must be a body name".into()))?;
|
||||
let name = b.as_str().ok_or_else(|| EngineError::param("body", "must be a body name"))?;
|
||||
let shape = s
|
||||
.design
|
||||
.as_deref()
|
||||
@@ -153,14 +151,14 @@ fn hex_blocks(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let shapes: Vec<&Shape> = match p.get("bodies") {
|
||||
None => design.bodies.iter().collect(),
|
||||
Some(v) => {
|
||||
let names = v.as_array().ok_or_else(|| EngineError::Params("`bodies` must be a list of body names".into()))?;
|
||||
let names = v.as_array().ok_or_else(|| EngineError::param("bodies", "must be a list of body names"))?;
|
||||
// Bounded before the duplicate check, which is quadratic in the list.
|
||||
if names.len() > MAX_CELLS {
|
||||
return Err(EngineError::Params(format!("`bodies` lists {} bodies; at most {MAX_CELLS} are meshed together", names.len())));
|
||||
}
|
||||
let mut out: Vec<&Shape> = Vec::with_capacity(names.len());
|
||||
for n in names {
|
||||
let name = n.as_str().ok_or_else(|| EngineError::Params("`bodies` must be a list of body names".into()))?;
|
||||
let name = n.as_str().ok_or_else(|| EngineError::param("bodies", "must be a list of body names"))?;
|
||||
let shape = design.body(name).ok_or_else(|| EngineError::Params(format!("no body `{name}`")))?;
|
||||
if out.iter().any(|b| b.name == shape.name) {
|
||||
return Err(EngineError::Params(format!("body `{name}` is listed twice")));
|
||||
@@ -261,7 +259,7 @@ fn order(p: &Value) -> Result<bool> {
|
||||
Some(v) => match v.as_u64() {
|
||||
Some(1) => Ok(false),
|
||||
Some(2) => Ok(true),
|
||||
_ => Err(EngineError::Params("`order` must be 1 (hex8) or 2 (hex20)".into())),
|
||||
_ => Err(EngineError::param("order", "must be 1 (hex8) or 2 (hex20)")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,9 +126,9 @@ pub(super) fn optional_count(p: &Value, key: &str, default: u64, range: std::ops
|
||||
Some(v) => v
|
||||
.as_u64()
|
||||
.filter(|n| range.contains(n))
|
||||
.ok_or_else(|| EngineError::Params(format!("`{key}` must be a whole number in {}..={}", range.start(), range.end())))?,
|
||||
.ok_or_else(|| EngineError::param(key, format!("must be a whole number in {}..={}", range.start(), range.end())))?,
|
||||
};
|
||||
usize::try_from(v).map_err(|_| EngineError::Params(format!("`{key}` is too large")))
|
||||
usize::try_from(v).map_err(|_| EngineError::param(key, "is too large"))
|
||||
}
|
||||
|
||||
/// The boundary options of `mesh.surface.check`, `mesh.tet.generate` and the hex meshers.
|
||||
@@ -144,7 +144,7 @@ pub(super) fn surface_options(p: &Value, mesh: &SurfaceMesh) -> Result<SurfaceOp
|
||||
/// An optional number in `range`.
|
||||
fn number_in(p: &Value, key: &str, default: f64, range: std::ops::RangeInclusive<f64>) -> Result<f64> {
|
||||
let v = optional_number(p, key, default)?;
|
||||
if range.contains(&v) { Ok(v) } else { Err(EngineError::Params(format!("`{key}` must be a number in {}..={}", range.start(), range.end()))) }
|
||||
if range.contains(&v) { Ok(v) } else { Err(EngineError::param(key, format!("must be a number in {}..={}", range.start(), range.end()))) }
|
||||
}
|
||||
|
||||
/// The quality options of `mesh.tet.generate`, and whether the quality stage runs at all.
|
||||
@@ -155,7 +155,7 @@ pub(super) fn quality_options(p: &Value, max_tets: u64) -> Result<(QualityOption
|
||||
Some(v) => Some(
|
||||
v.as_f64()
|
||||
.filter(|h| *h > 0.0 && h.is_finite() && *h <= fieldcraft_model::MAX_COORD)
|
||||
.ok_or_else(|| EngineError::Params("`size` must be a positive number (mm)".into()))?,
|
||||
.ok_or_else(|| EngineError::param("size", "must be a positive number (mm)"))?,
|
||||
),
|
||||
};
|
||||
let order = match p.get("order") {
|
||||
@@ -163,7 +163,7 @@ pub(super) fn quality_options(p: &Value, max_tets: u64) -> Result<(QualityOption
|
||||
Some(v) => match v.as_str() {
|
||||
Some("linear") => Order::Linear,
|
||||
Some("quadratic") => Order::Quadratic,
|
||||
_ => return Err(EngineError::Params("`order` must be \"linear\" or \"quadratic\"".into())),
|
||||
_ => return Err(EngineError::param("order", "must be \"linear\" or \"quadratic\"")),
|
||||
},
|
||||
};
|
||||
let target = number_in(p, "quality_target", 40.0, 5.0..=60.0)?;
|
||||
@@ -389,14 +389,14 @@ fn planar(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.get("size")
|
||||
.and_then(Value::as_f64)
|
||||
.filter(|v| *v > 0.0 && v.is_finite())
|
||||
.ok_or_else(|| EngineError::Params("`size` must be a positive number (the element size)".into()))?;
|
||||
.ok_or_else(|| EngineError::param("size", "must be a positive number (the element size)"))?;
|
||||
let kind = match p.get("kind") {
|
||||
None => ElementKind::Quad4,
|
||||
Some(k) => k
|
||||
.as_str()
|
||||
.and_then(ElementKind::from_name)
|
||||
.filter(|k| k.dimension() == 2)
|
||||
.ok_or_else(|| EngineError::Params("`kind` must be quad4, quad8, tri3 or tri6".into()))?,
|
||||
.ok_or_else(|| EngineError::param("kind", "must be quad4, quad8, tri3 or tri6"))?,
|
||||
};
|
||||
let opts = fieldcraft_mesh::quad::QuadOptions {
|
||||
max_elements: optional_count(p, "max_elements", 1_000_000, 1..=PLANAR_MAX_ELEMENTS)?,
|
||||
@@ -418,7 +418,7 @@ fn planar(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let name = match p.get("name") {
|
||||
None => "Plane".to_string(),
|
||||
Some(n) => n.as_str().map(str::to_string).ok_or_else(|| EngineError::Params("`name` must be a body name".into()))?,
|
||||
Some(n) => n.as_str().map(str::to_string).ok_or_else(|| EngineError::param("name", "must be a body name"))?,
|
||||
};
|
||||
GeometryRef::body(name.clone())?;
|
||||
let mut id = 0u32;
|
||||
@@ -438,7 +438,7 @@ fn planar(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(loops, name, "area".to_string(), names)
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
let face_name = p.get("face").and_then(Value::as_str).ok_or_else(|| EngineError::Params("`face` must be a face name".into()))?;
|
||||
let face_name = p.get("face").and_then(Value::as_str).ok_or_else(|| EngineError::param("face", "must be a face name"))?;
|
||||
let (shape, mesh) = super::geometry::body_mesh(s, p)?;
|
||||
let face = shape.face_index(face_name).ok_or_else(|| EngineError::Params(format!("body `{}` has no face `{face_name}`", shape.name)))?;
|
||||
let b = face_boundary(&mesh.positions, &mesh.triangles, &mesh.tri_face, face, mesh.weld_distance)?;
|
||||
@@ -528,7 +528,7 @@ fn number(v: f64) -> Value {
|
||||
fn quality_report(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let bins = match p.get("bins") {
|
||||
None => 10,
|
||||
Some(v) => v.as_u64().and_then(|b| usize::try_from(b).ok()).ok_or_else(|| EngineError::Params("`bins` must be a whole number".into()))?,
|
||||
Some(v) => v.as_u64().and_then(|b| usize::try_from(b).ok()).ok_or_else(|| EngineError::param("bins", "must be a whole number"))?,
|
||||
};
|
||||
let selection = match p.get("elements") {
|
||||
None => None,
|
||||
|
||||
@@ -43,7 +43,7 @@ fn plane(s: &Session, p: &Value) -> Result<([f64; 3], [f64; 3])> {
|
||||
.and_then(|d| d.plane())
|
||||
.ok_or_else(|| EngineError::Params(format!("no datum plane `{}`", name.chars().take(64).collect::<String>()))),
|
||||
Some(v @ Value::Object(_)) => Ok((super::xyz(v, "point")?, super::xyz(v, "normal")?)),
|
||||
_ => Err(EngineError::Params("`plane` must be a datum plane's name or {\"point\", \"normal\"}".into())),
|
||||
_ => Err(EngineError::param("plane", "must be a datum plane's name or {\"point\", \"normal\"}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -173,7 +173,7 @@ fn deformed(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn contour(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let step = step_for(s, p, Want::Any)?;
|
||||
let name = opt::<String>(p, "field")?.ok_or_else(|| EngineError::Params("`field` is required (a field id, or \"none\")".into()))?;
|
||||
let name = opt::<String>(p, "field")?.ok_or_else(|| EngineError::param("field", "is required (a field id, or \"none\")"))?;
|
||||
let field = match name.as_str() {
|
||||
"none" => None,
|
||||
id => Some(Field::parse(id).ok_or_else(|| {
|
||||
@@ -255,7 +255,7 @@ fn frame(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(None, Some(t)) if t.is_finite() => {
|
||||
times.iter().enumerate().min_by(|a, b| (a.1 - t).abs().total_cmp(&(b.1 - t).abs())).map_or(last, |(i, _)| i)
|
||||
}
|
||||
(None, Some(_)) => return Err(EngineError::Params("`time` must be a number".into())),
|
||||
(None, Some(_)) => return Err(EngineError::param("time", "must be a number")),
|
||||
(None, None) => last,
|
||||
};
|
||||
let v = view_of(s, &step)?;
|
||||
@@ -272,7 +272,7 @@ fn reactions(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let step = step_for(s, p, Want::Any)?;
|
||||
let about = if p.get("about").is_some() { xyz(p, "about")? } else { [0.0; 3] };
|
||||
if !about.iter().all(|c| c.is_finite()) {
|
||||
return Err(EngineError::Params("`about` must be a finite [x, y, z]".into()));
|
||||
return Err(EngineError::param("about", "must be a finite [x, y, z]"));
|
||||
}
|
||||
let Some((result, current)) = s.latest_result(&step) else { return Err(EngineError::Params(solved_list(s))) };
|
||||
let Kind::Static(r) = result.kind() else {
|
||||
@@ -287,7 +287,7 @@ fn reactions(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.filter(|a| a.len() <= MAX_SETS)
|
||||
.ok_or_else(|| EngineError::Params(format!("`sets` must be a list of at most {MAX_SETS} set names")))?;
|
||||
list.iter()
|
||||
.map(|n| n.as_str().map(str::to_string).ok_or_else(|| EngineError::Params("`sets` must be set names".into())))
|
||||
.map(|n| n.as_str().map(str::to_string).ok_or_else(|| EngineError::param("sets", "must be set names")))
|
||||
.collect::<Result<_>>()?
|
||||
}
|
||||
None => {
|
||||
|
||||
@@ -134,7 +134,7 @@ fn cap<T: Copy + TryInto<u64> + TryFrom<u64> + std::fmt::Display>(p: &Value, key
|
||||
.as_u64()
|
||||
.filter(|n| (1..=most).contains(n))
|
||||
.and_then(|n| T::try_from(n).ok())
|
||||
.ok_or_else(|| EngineError::Params(format!("`{key}` must be a whole number from 1 to {default}"))),
|
||||
.ok_or_else(|| EngineError::param(key, format!("must be a whole number from 1 to {default}"))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,7 +390,7 @@ fn history(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let h = &r.history;
|
||||
let samples = h.time.len();
|
||||
let every = match opt::<u64>(p, "every")? {
|
||||
Some(0) => return Err(EngineError::Params("`every` must be at least 1".into())),
|
||||
Some(0) => return Err(EngineError::param("every", "must be at least 1")),
|
||||
Some(k) => usize::try_from(k).unwrap_or(usize::MAX),
|
||||
None => samples.div_ceil(MAX_HISTORY_SAMPLES).max(1),
|
||||
};
|
||||
@@ -541,7 +541,7 @@ fn probe_fluid(r: &FluidResult, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn probe(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let step = opt::<String>(p, "step")?.ok_or_else(|| EngineError::Params("`step` is required".into()))?;
|
||||
let step = opt::<String>(p, "step")?.ok_or_else(|| EngineError::param("step", "is required"))?;
|
||||
let result = s.result(&step).ok_or_else(|| {
|
||||
EngineError::Params(if s.latest_result(&step).is_some() {
|
||||
format!("the result of step `{step}` is stale (the model changed since it was solved); solve it again")
|
||||
|
||||
@@ -38,7 +38,7 @@ struct ElementEdge {
|
||||
/// An optional list parameter of at most [`MAX_ELEMENTS`] entries.
|
||||
fn list<'a, T: serde::Deserialize<'a>>(p: &'a Value, key: &str) -> Result<Vec<T>> {
|
||||
if p.get(key).and_then(Value::as_array).is_some_and(|a| a.len() > MAX_ELEMENTS) {
|
||||
return Err(EngineError::Params(format!("`{key}` must be a list of at most {MAX_ELEMENTS} entries")));
|
||||
return Err(EngineError::param(key, format!("must be a list of at most {MAX_ELEMENTS} entries")));
|
||||
}
|
||||
Ok(opt(p, key)?.unwrap_or_default())
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ pub enum EngineError {
|
||||
UnknownCommand(String),
|
||||
#[error("{0}")]
|
||||
Params(String),
|
||||
/// One parameter is missing or wrong: its key (a JSON path, `elastic/young`) and what is
|
||||
/// wrong with it. The message names the key (agents need it); a form shows the problem
|
||||
/// beside the field, by its label.
|
||||
#[error("`{key}` {problem}")]
|
||||
Param { key: String, problem: String },
|
||||
#[error(transparent)]
|
||||
Model(#[from] fieldcraft_model::Error),
|
||||
#[error(transparent)]
|
||||
@@ -65,6 +70,13 @@ pub enum EngineError {
|
||||
Internal { command: String, message: String },
|
||||
}
|
||||
|
||||
impl EngineError {
|
||||
/// A problem with parameter `key`.
|
||||
pub fn param(key: impl Into<String>, problem: impl Into<String>) -> Self {
|
||||
Self::Param { key: key.into(), problem: problem.into() }
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, EngineError>;
|
||||
|
||||
/// The result of a solved step, from the implicit, the explicit or the lattice Boltzmann solver.
|
||||
|
||||
Reference in new issue
Block a user