use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MalformedSchema {
RootNotObject,
RequiredNotStringArray,
}
impl MalformedSchema {
pub(crate) fn describe(&self) -> &'static str {
match self {
Self::RootNotObject => "root is not a JSON object",
Self::RequiredNotStringArray => "`required` is not an array of strings",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RequiredDeclaration {
Nothing,
Fields(Vec<String>),
Malformed(MalformedSchema),
}
pub(crate) fn required_declaration(schema: &Value) -> RequiredDeclaration {
let Some(root) = schema.as_object() else {
return RequiredDeclaration::Malformed(MalformedSchema::RootNotObject);
};
let Some(required) = root.get("required") else {
return RequiredDeclaration::Nothing;
};
let Some(entries) = required.as_array() else {
return RequiredDeclaration::Malformed(MalformedSchema::RequiredNotStringArray);
};
let mut fields: Vec<String> = Vec::with_capacity(entries.len());
for entry in entries {
let Some(name) = entry.as_str() else {
return RequiredDeclaration::Malformed(MalformedSchema::RequiredNotStringArray);
};
if !fields.iter().any(|seen| seen == name) {
fields.push(name.to_string());
}
}
if fields.is_empty() {
RequiredDeclaration::Nothing
} else {
RequiredDeclaration::Fields(fields)
}
}
pub(crate) fn missing_fields(required: &[String], input: &Value) -> Vec<String> {
let object = input.as_object();
required
.iter()
.filter(|name| !object.is_some_and(|object| object.contains_key(name.as_str())))
.cloned()
.collect()
}
pub(crate) fn refusal_text(tool: &str, missing: &[String], required: &[String]) -> String {
let noun = if missing.len() == 1 {
"field"
} else {
"fields"
};
format!(
"{tool}: missing required {noun} {missing}\n\n\
This call did not run. {tool}'s input_schema requires: {required}. Call it again with \
every one of them present. Only the presence of required fields is checked before a \
call runs — not their types, allowed values or nested contents.",
missing = quoted_list(missing),
required = quoted_list(required),
)
}
fn quoted_list(names: &[String]) -> String {
names
.iter()
.map(|name| format!("\"{name}\""))
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn names(list: &[&str]) -> Vec<String> {
list.iter().map(|name| name.to_string()).collect()
}
#[test]
fn required_fields_root_not_an_object_is_malformed() {
for schema in [json!([]), json!("object"), json!(null), json!(7)] {
assert_eq!(
required_declaration(&schema),
RequiredDeclaration::Malformed(MalformedSchema::RootNotObject),
"{schema}"
);
}
}
#[test]
fn required_fields_absent_or_empty_required_is_nothing() {
assert_eq!(
required_declaration(&json!({"type": "object", "properties": {}})),
RequiredDeclaration::Nothing
);
assert_eq!(
required_declaration(&json!({"type": "object", "required": []})),
RequiredDeclaration::Nothing
);
}
#[test]
fn required_fields_required_not_an_array_is_malformed() {
for required in [json!("operation"), json!({"operation": true}), json!(null)] {
assert_eq!(
required_declaration(&json!({ "required": required })),
RequiredDeclaration::Malformed(MalformedSchema::RequiredNotStringArray),
"{required}"
);
}
}
#[test]
fn required_fields_a_non_string_element_makes_the_whole_list_malformed() {
assert_eq!(
required_declaration(&json!({"required": ["operation", 3, "dest_path"]})),
RequiredDeclaration::Malformed(MalformedSchema::RequiredNotStringArray)
);
}
#[test]
fn required_fields_keeps_declared_order_and_drops_duplicates() {
assert_eq!(
required_declaration(&json!({
"required": ["operation", "dest_path", "operation", "content", "dest_path"]
})),
RequiredDeclaration::Fields(names(&["operation", "dest_path", "content"]))
);
}
#[test]
fn required_fields_presence_is_any_key_whatever_its_value() {
let required = names(&["operation", "dest_path", "content"]);
let input = json!({"operation": null, "dest_path": "", "content": {"nested": 1}});
assert!(missing_fields(&required, &input).is_empty());
}
#[test]
fn required_fields_missing_names_keep_required_order() {
let required = names(&["operation", "dest_path", "content"]);
let input = json!({"content": "hello", "extra": true});
assert_eq!(
missing_fields(&required, &input),
names(&["operation", "dest_path"])
);
}
#[test]
fn required_fields_a_non_object_input_misses_every_name() {
let required = names(&["operation", "dest_path"]);
for input in [
json!(null),
json!("operation"),
json!(["operation"]),
json!(1),
] {
assert_eq!(missing_fields(&required, &input), required, "{input}");
}
}
#[test]
fn required_fields_malformed_descriptions() {
assert_eq!(
MalformedSchema::RootNotObject.describe(),
"root is not a JSON object"
);
assert_eq!(
MalformedSchema::RequiredNotStringArray.describe(),
"`required` is not an array of strings"
);
}
#[test]
fn required_fields_refusal_text_for_one_missing_field() {
assert_eq!(
refusal_text(
"murmur-tool-editor",
&names(&["operation"]),
&names(&["operation", "dest_path"]),
),
"murmur-tool-editor: missing required field \"operation\"\n\n\
This call did not run. murmur-tool-editor's input_schema requires: \"operation\", \
\"dest_path\". Call it again with every one of them present. Only the presence of \
required fields is checked before a call runs — not their types, allowed values or \
nested contents."
);
}
#[test]
fn required_fields_refusal_text_for_several_missing_fields() {
assert_eq!(
refusal_text(
"murmur-tool-editor",
&names(&["operation", "dest_path"]),
&names(&["operation", "dest_path"]),
),
"murmur-tool-editor: missing required fields \"operation\", \"dest_path\"\n\n\
This call did not run. murmur-tool-editor's input_schema requires: \"operation\", \
\"dest_path\". Call it again with every one of them present. Only the presence of \
required fields is checked before a call runs — not their types, allowed values or \
nested contents."
);
}
}