huginn door: report every lost key across the batch, accept only objects
The strict decode stopped at the first document with a lost key and let serde read a struct from a JSON array. It now names every lost path in the batch and refuses a non-object document or selection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
78773a2a13
commit
055b73e8f8
2 changed files with 26 additions and 9 deletions
|
|
@ -48,11 +48,15 @@ pub fn documents(text: &str) -> Result<Vec<PipelineDocument>, Trouble> {
|
|||
object @ Value::Object(_) => vec![object],
|
||||
_ => return Err(Trouble::invalid("the documents must be a JSON array of {kind, value} objects")),
|
||||
};
|
||||
items
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, item)| strict(item, &format!("documents[{index}]")))
|
||||
.collect()
|
||||
let mut typed = Vec::with_capacity(items.len());
|
||||
let mut lost = Vec::new();
|
||||
for (index, item) in items.into_iter().enumerate() {
|
||||
let (document, paths) = decode(item, &format!("documents[{index}]"))?;
|
||||
typed.push(document);
|
||||
lost.extend(paths);
|
||||
}
|
||||
refuse_lost(&lost)?;
|
||||
Ok(typed)
|
||||
}
|
||||
|
||||
pub fn selection(text: &str) -> Result<Selection, Trouble> {
|
||||
|
|
@ -60,7 +64,9 @@ pub fn selection(text: &str) -> Result<Selection, Trouble> {
|
|||
}
|
||||
|
||||
pub fn selection_value(value: Value) -> Result<Selection, Trouble> {
|
||||
strict(value, "selection")
|
||||
let (selection, lost) = decode(value, "selection")?;
|
||||
refuse_lost(&lost)?;
|
||||
Ok(selection)
|
||||
}
|
||||
|
||||
/// The kind a pipeline id names in its middle segment (`<root>:<kind>:<local>`).
|
||||
|
|
@ -80,13 +86,23 @@ fn parse(text: &str) -> Result<Value, Trouble> {
|
|||
serde_json::from_str(text).map_err(|error| Trouble::invalid(format!("not JSON: {error}")))
|
||||
}
|
||||
|
||||
fn strict<T: Serialize + DeserializeOwned>(input: Value, at: &str) -> Result<T, Trouble> {
|
||||
/// The typed value of one JSON object and the paths its decode lost. Serde
|
||||
/// would also read a struct from an array, positionally, so only an object
|
||||
/// is a document or a selection.
|
||||
fn decode<T: Serialize + DeserializeOwned>(input: Value, at: &str) -> Result<(T, Vec<String>), Trouble> {
|
||||
if !input.is_object() {
|
||||
return Err(Trouble::invalid(format!("{at} must be a JSON object")));
|
||||
}
|
||||
let typed: T = serde_json::from_value(input.clone()).map_err(|error| Trouble::invalid(format!("{at} does not decode: {error}")))?;
|
||||
let kept = serde_json::to_value(&typed).map_err(|error| Trouble::Internal(format!("{at} does not re-serialise: {error}")))?;
|
||||
let mut paths = Vec::new();
|
||||
dropped(&input, &kept, at, &mut paths);
|
||||
Ok((typed, paths))
|
||||
}
|
||||
|
||||
fn refuse_lost(paths: &[String]) -> Result<(), Trouble> {
|
||||
if paths.is_empty() {
|
||||
Ok(typed)
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Trouble::invalid(format!("keys the schema does not know (`huginn schema` prints it): {}", paths.join(", "))))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@ fn a_document_that_does_not_decode_is_invalid_input() {
|
|||
let cases = [
|
||||
(json!([{ "kind": "nonsense", "value": {} }]), "documents[0] does not decode"),
|
||||
(json!("text"), "JSON array"),
|
||||
(json!([[]]), "documents[0] must be a JSON object"),
|
||||
(json!([]), "1 to 64"),
|
||||
(json!((0..65).map(|_| campaign_json()).collect::<Vec<_>>()), "1 to 64"),
|
||||
];
|
||||
|
|
@ -832,7 +833,7 @@ fn a_resolved_document_shows_its_closing_record() {
|
|||
let resolution = json!({
|
||||
"kind": "resolution",
|
||||
"value": {
|
||||
"subject": { "kind": "question", "id": format!("{CAMPAIGN}:question:Q1") },
|
||||
"subject": { "kind": "Question", "id": format!("{CAMPAIGN}:question:Q1") },
|
||||
"sequence": 1,
|
||||
"outcome": { "Withdrawn": { "reason": "moot" } },
|
||||
"rationale": "Resolved.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue