68 lines
2.3 KiB
Rust
68 lines
2.3 KiB
Rust
//! YAML frontmatter parsing helpers shared by every kind.
|
|
|
|
use lint_common::RecordLintError;
|
|
use serde::de::DeserializeOwned;
|
|
|
|
use crate::error::LintError;
|
|
|
|
/// Strict YAML deserialization that maps serde errors into the linter's
|
|
/// `MissingField` / `InvalidField` / `MalformedFrontmatter` taxonomy
|
|
/// when possible.
|
|
pub fn deserialize_strict<F: DeserializeOwned>(yaml: &str) -> Result<F, LintError> {
|
|
serde_yaml::from_str::<F>(yaml).map_err(map_serde_error)
|
|
}
|
|
|
|
fn map_serde_error(err: serde_yaml::Error) -> LintError {
|
|
let msg = err.to_string();
|
|
|
|
// `missing field \`X\`` is the exact pattern serde uses for missing
|
|
// required fields. Hoist into the typed variant so the LLM sees a
|
|
// crisp message it can act on.
|
|
if let Some(field) = parse_missing_field(&msg) {
|
|
return LintError::MissingField(field);
|
|
}
|
|
if let Some((field, message)) = parse_invalid_status(&msg) {
|
|
if field == "status" {
|
|
return LintError::InvalidStatus(message);
|
|
}
|
|
return LintError::InvalidField { field, message };
|
|
}
|
|
LintError::Record(RecordLintError::MalformedFrontmatter(msg))
|
|
}
|
|
|
|
fn parse_missing_field(msg: &str) -> Option<&'static str> {
|
|
let needle = "missing field `";
|
|
let start = msg.find(needle)? + needle.len();
|
|
let end = msg[start..].find('`')? + start;
|
|
let field_name = &msg[start..end];
|
|
static FIELDS: &[&str] = &[
|
|
"created_at",
|
|
"updated_at",
|
|
"sources",
|
|
"status",
|
|
"kind",
|
|
"description",
|
|
"model_invokation",
|
|
"user_invocable",
|
|
"last_sources",
|
|
"requires",
|
|
];
|
|
FIELDS.iter().copied().find(|n| *n == field_name)
|
|
}
|
|
|
|
fn parse_invalid_status(msg: &str) -> Option<(&'static str, String)> {
|
|
// serde renders enum failures as: "unknown variant `Foo`, expected one of ..."
|
|
// We can't reliably attribute it to a specific field from the message
|
|
// alone, so we conservatively label it as `status` only when the
|
|
// message mentions one of the DecisionStatus variants in the
|
|
// expected set.
|
|
if msg.contains("unknown variant") && msg.contains("`open`") {
|
|
let needle = "unknown variant `";
|
|
let start = msg.find(needle)? + needle.len();
|
|
let end = msg[start..].find('`')? + start;
|
|
let bad = msg[start..end].to_string();
|
|
return Some(("status", bad));
|
|
}
|
|
None
|
|
}
|