gent/runtime/
validation.rs

1//! Output validation for structured outputs
2
3use crate::interpreter::OutputSchema;
4use crate::parser::ast::FieldType;
5use serde_json::Value as JsonValue;
6
7/// Validate JSON output against a schema
8pub fn validate_output(json: &JsonValue, schema: &OutputSchema) -> Result<(), String> {
9    let obj = json.as_object().ok_or("Expected JSON object")?;
10
11    for field in &schema.fields {
12        let value = obj
13            .get(&field.name)
14            .ok_or_else(|| format!("missing required field: '{}'", field.name))?;
15
16        validate_field_type(value, &field.field_type, &field.name)?;
17    }
18
19    Ok(())
20}
21
22fn validate_field_type(value: &JsonValue, expected: &FieldType, path: &str) -> Result<(), String> {
23    match expected {
24        FieldType::String => {
25            if !value.is_string() {
26                return Err(format!(
27                    "'{}': expected string, got {}",
28                    path,
29                    json_type_name(value)
30                ));
31            }
32        }
33        FieldType::Number => {
34            if !value.is_number() {
35                return Err(format!(
36                    "'{}': expected number, got {}",
37                    path,
38                    json_type_name(value)
39                ));
40            }
41        }
42        FieldType::Boolean => {
43            if !value.is_boolean() {
44                return Err(format!(
45                    "'{}': expected boolean, got {}",
46                    path,
47                    json_type_name(value)
48                ));
49            }
50        }
51        FieldType::Array(inner) => {
52            let arr = value.as_array().ok_or_else(|| {
53                format!("'{}': expected array, got {}", path, json_type_name(value))
54            })?;
55            for (i, item) in arr.iter().enumerate() {
56                validate_field_type(item, inner, &format!("{}[{}]", path, i))?;
57            }
58        }
59        FieldType::Object(fields) => {
60            let obj = value.as_object().ok_or_else(|| {
61                format!("'{}': expected object, got {}", path, json_type_name(value))
62            })?;
63            for field in fields {
64                let field_value = obj
65                    .get(&field.name)
66                    .ok_or_else(|| format!("'{}.{}': missing required field", path, field.name))?;
67                validate_field_type(
68                    field_value,
69                    &field.field_type,
70                    &format!("{}.{}", path, field.name),
71                )?;
72            }
73        }
74        FieldType::Named(_) => {
75            // Named types should be resolved before validation
76            // For now, accept any object
77            if !value.is_object() {
78                return Err(format!(
79                    "'{}': expected object, got {}",
80                    path,
81                    json_type_name(value)
82                ));
83            }
84        }
85    }
86    Ok(())
87}
88
89fn json_type_name(value: &JsonValue) -> &'static str {
90    match value {
91        JsonValue::Null => "null",
92        JsonValue::Bool(_) => "boolean",
93        JsonValue::Number(_) => "number",
94        JsonValue::String(_) => "string",
95        JsonValue::Array(_) => "array",
96        JsonValue::Object(_) => "object",
97    }
98}