engine: refuse unknown command parameters, suggest a valid name, document heat loads and thermal materials

Every command with a schema now refuses a top-level key it does not take,
naming the key and the accepted ones (#316). A bad definition name's error
suggests the nearest valid one (#320). The load and material params docs list
the heat loads and the thermal block.

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-15 09:14:34 -04:00
1 parent 35ee88aa18
commit 7dd4043afe
4 files changed
+107 -4

No files matched your search

+35 -1
View File
@@ -294,10 +294,30 @@ pub fn check_name(what: &str, name: &str) -> Result<()> {
Ok(())
} else {
let shown: String = name.chars().take(MAX_NAME_LEN).collect();
Err(Error::Invalid(format!("{what} name `{shown}` must be 1 to {MAX_NAME_LEN} characters: a letter, then letters, digits, `_` or `-`")))
let rule = format!("{what} name `{shown}` must be 1 to {MAX_NAME_LEN} characters: a letter, then letters, digits, `_` or `-`");
Err(Error::Invalid(match suggest_name(name) {
Some(fixed) => format!("{rule} (try `{fixed}`)"),
None => rule,
}))
}
}
/// The valid name closest to `name`: spaces and other characters become `_`, a leading
/// non-letter gets an `N` before it, and it is cut to [`MAX_NAME_LEN`]. `None` when nothing of it
/// is usable (empty, or no letter or digit at all).
pub fn suggest_name(name: &str) -> Option<String> {
let trimmed = name.trim();
if !trimmed.chars().any(|c| c.is_ascii_alphanumeric()) {
return None;
}
let mut fixed: String = trimmed.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '_' }).collect();
if !fixed.starts_with(|c: char| c.is_ascii_alphabetic()) {
fixed.insert(0, 'N');
}
fixed.truncate(MAX_NAME_LEN);
Some(fixed)
}
/// Check that a value is a finite number, naming it in the error.
pub(crate) fn finite(what: &str, v: f64) -> Result<f64> {
if v.is_finite() { Ok(v) } else { Err(Error::Invalid(format!("{what} must be a finite number, got {v}"))) }
@@ -412,3 +432,17 @@ impl Model {
]
}
}
#[cfg(test)]
mod name_tests {
use super::*;
#[test]
fn a_bad_name_suggests_a_fixed_one() {
let e = check_name("load", "Hot air").unwrap_err().to_string();
assert!(e.contains("try `Hot_air`"), "{e}");
assert_eq!(suggest_name("1st load"), Some("N1st_load".into()));
assert_eq!(suggest_name(" "), None);
assert!(check_name("load", &suggest_name(&"x ".repeat(80)).unwrap()).is_ok());
}
}