rullama_reasoning/
schema_validator.rs1use std::future::Future;
16
17use anyhow::{Result, anyhow};
18use jsonschema::Validator;
19use serde_json::Value;
20
21#[derive(Clone)]
23pub struct SchemaValidator {
24 validator: std::sync::Arc<Validator>,
25}
26
27impl SchemaValidator {
28 pub fn new(schema: &Value) -> Result<Self> {
31 let validator =
32 jsonschema::validator_for(schema).map_err(|e| anyhow!("invalid JSON Schema: {e}"))?;
33 Ok(Self {
34 validator: std::sync::Arc::new(validator),
35 })
36 }
37
38 pub fn validate(&self, value: &Value) -> std::result::Result<(), Vec<String>> {
42 let errors: Vec<String> = self
43 .validator
44 .iter_errors(value)
45 .map(|e| format!("{}: {e}", e.instance_path))
46 .collect();
47 if errors.is_empty() {
48 Ok(())
49 } else {
50 Err(errors)
51 }
52 }
53}
54
55pub async fn retry_until_valid<F, Fut>(
64 validator: &SchemaValidator,
65 max_attempts: usize,
66 mut producer: F,
67) -> Result<Value>
68where
69 F: FnMut(Option<Vec<String>>) -> Fut,
70 Fut: Future<Output = Result<Value>>,
71{
72 let mut last_errors: Option<Vec<String>> = None;
73 for attempt in 0..max_attempts.max(1) {
74 let value = producer(last_errors.clone()).await?;
75 match validator.validate(&value) {
76 Ok(()) => return Ok(value),
77 Err(errors) => {
78 tracing::warn!(
79 attempt = attempt + 1,
80 max = max_attempts,
81 "schema validation failed: {} violation(s)",
82 errors.len()
83 );
84 last_errors = Some(errors);
85 }
86 }
87 }
88 Err(anyhow!(
89 "schema validation failed after {} attempts: {}",
90 max_attempts,
91 last_errors.unwrap_or_default().join("; ")
92 ))
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use serde_json::json;
99
100 fn person_schema() -> Value {
101 json!({
102 "type": "object",
103 "properties": {
104 "name": { "type": "string" },
105 "age": { "type": "integer", "minimum": 0 }
106 },
107 "required": ["name", "age"]
108 })
109 }
110
111 #[test]
112 fn valid_value_passes() {
113 let v = SchemaValidator::new(&person_schema()).unwrap();
114 assert!(v.validate(&json!({"name": "Ada", "age": 36})).is_ok());
115 }
116
117 #[test]
118 fn missing_required_field_fails() {
119 let v = SchemaValidator::new(&person_schema()).unwrap();
120 let result = v.validate(&json!({"name": "Ada"}));
121 assert!(result.is_err());
122 assert!(result.unwrap_err().iter().any(|e| e.contains("age")));
123 }
124
125 #[test]
126 fn wrong_type_fails() {
127 let v = SchemaValidator::new(&person_schema()).unwrap();
128 let result = v.validate(&json!({"name": "Ada", "age": "old"}));
129 assert!(result.is_err());
130 }
131
132 #[test]
133 fn invalid_schema_returns_error() {
134 let result = SchemaValidator::new(&json!({"type": 42}));
136 assert!(result.is_err());
137 }
138
139 #[tokio::test]
140 async fn retry_converges_on_valid() {
141 let schema = SchemaValidator::new(&person_schema()).unwrap();
142 let mut call = 0;
143 let result = retry_until_valid(&schema, 3, |errors| {
144 call += 1;
145 let value = if errors.is_none() {
146 json!({"name": "Ada"}) } else {
148 json!({"name": "Ada", "age": 36})
149 };
150 async move { Ok(value) }
151 })
152 .await
153 .unwrap();
154 assert_eq!(result, json!({"name": "Ada", "age": 36}));
155 assert_eq!(call, 2);
156 }
157
158 #[tokio::test]
159 async fn retry_exhausts_then_errors() {
160 let schema = SchemaValidator::new(&person_schema()).unwrap();
161 let result = retry_until_valid(&schema, 2, |_errors| async {
162 Ok(json!({"name": "Ada"})) })
164 .await;
165 assert!(result.is_err());
166 }
167}