pub(crate) fn compile_schema(schema: &serde_json::Value) -> Result<jsonschema::Validator, String> {
jsonschema::validator_for(schema)
.map_err(|err| format!("task(): invalid responseSchema: {err}"))
}
pub(crate) fn decode_reply(
text: &str,
validator: &jsonschema::Validator,
) -> Result<serde_json::Value, String> {
let candidate = strip_code_fence(text);
let parsed: serde_json::Value = serde_json::from_str(candidate).map_err(|err| {
format!("task(): responseSchema was set but the reply is not valid JSON: {err}")
})?;
let errors = validator
.iter_errors(&parsed)
.map(|err| err.to_string())
.collect::<Vec<_>>();
if !errors.is_empty() {
return Err(format!(
"task(): reply failed responseSchema validation: {}",
errors.join("; ")
));
}
Ok(parsed)
}
fn strip_code_fence(text: &str) -> &str {
let trimmed = text.trim();
let Some(rest) = trimmed.strip_prefix("```") else {
return trimmed;
};
let Some(body) = rest.strip_suffix("```") else {
return trimmed;
};
match body.split_once('\n') {
Some((first_line, tail)) if !first_line.trim().is_empty() => tail.trim(),
_ => body.trim(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn validator() -> jsonschema::Validator {
compile_schema(&json!({
"type": "object",
"properties": { "refuted": { "type": "boolean" } },
"required": ["refuted"],
}))
.expect("schema compiles")
}
#[test]
fn decodes_plain_json() {
let value = decode_reply(r#"{"refuted": true}"#, &validator()).unwrap();
assert_eq!(value, json!({"refuted": true}));
}
#[test]
fn decodes_fenced_json() {
let text = "```json\n{\"refuted\": false}\n```";
let value = decode_reply(text, &validator()).unwrap();
assert_eq!(value, json!({"refuted": false}));
}
#[test]
fn rejects_non_json() {
let err = decode_reply("definitely not json", &validator()).unwrap_err();
assert!(err.contains("not valid JSON"), "{err}");
}
#[test]
fn rejects_schema_violation() {
let err = decode_reply(r#"{"refuted": "yes"}"#, &validator()).unwrap_err();
assert!(err.contains("responseSchema validation"), "{err}");
}
#[test]
fn rejects_invalid_schema_before_spawn() {
let err = compile_schema(&json!({"type": "not-a-type"})).unwrap_err();
assert!(err.contains("invalid responseSchema"), "{err}");
}
}