1008 lines
34 KiB
Rust
1008 lines
34 KiB
Rust
//! Structured generation with schema validation
|
|
//!
|
|
//! Provides comprehensive structured output generation including:
|
|
//! - JSON schema validation with comprehensive error reporting
|
|
//! - XML schema enforcement with DTD and XSD support
|
|
//! - Custom format validation (CSV, TSV, structured text)
|
|
//! - Schema-guided generation with constraint satisfaction
|
|
//! - Output format enforcement with automatic correction
|
|
//! - Template-based structured output with variable binding
|
|
|
|
use anyhow::{Result, anyhow};
|
|
use regex::Regex;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value as JsonValue;
|
|
use std::collections::HashMap;
|
|
|
|
/// Supported output formats for structured generation
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum OutputFormat {
|
|
Json,
|
|
Xml,
|
|
Csv,
|
|
Tsv,
|
|
Yaml,
|
|
Custom(String),
|
|
}
|
|
|
|
impl OutputFormat {
|
|
/// Get MIME type for the format
|
|
#[must_use]
|
|
pub fn mime_type(&self) -> &'static str {
|
|
match self {
|
|
Self::Json => "application/json",
|
|
Self::Xml => "application/xml",
|
|
Self::Csv => "text/csv",
|
|
Self::Tsv => "text/tab-separated-values",
|
|
Self::Yaml => "application/x-yaml",
|
|
Self::Custom(_) => "text/plain",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// JSON schema constraint types
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum JsonConstraint {
|
|
Type(String),
|
|
Properties(HashMap<String, Box<Self>>),
|
|
Items(Box<Self>),
|
|
Required(Vec<String>),
|
|
Pattern(String),
|
|
MinLength(usize),
|
|
MaxLength(usize),
|
|
Minimum(f64),
|
|
Maximum(f64),
|
|
Enum(Vec<JsonValue>),
|
|
AllOf(Vec<Self>),
|
|
AnyOf(Vec<Self>),
|
|
OneOf(Vec<Self>),
|
|
}
|
|
|
|
/// JSON schema definition
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JsonSchema {
|
|
pub title: Option<String>,
|
|
pub description: Option<String>,
|
|
pub constraints: JsonConstraint,
|
|
pub strict_mode: bool,
|
|
}
|
|
|
|
impl JsonSchema {
|
|
/// Create a new JSON schema
|
|
#[must_use]
|
|
pub fn new(constraints: JsonConstraint) -> Self {
|
|
Self {
|
|
title: None,
|
|
description: None,
|
|
constraints,
|
|
strict_mode: false,
|
|
}
|
|
}
|
|
|
|
/// Validate JSON value against schema
|
|
pub fn validate(&self, value: &JsonValue) -> Result<ValidationResult> {
|
|
let mut errors = Vec::new();
|
|
let mut warnings = Vec::new();
|
|
|
|
self.validate_constraint(&self.constraints, value, "", &mut errors, &mut warnings)?;
|
|
|
|
Ok(ValidationResult {
|
|
valid: errors.is_empty(),
|
|
errors,
|
|
warnings,
|
|
})
|
|
}
|
|
|
|
/// Internal constraint validation
|
|
fn validate_constraint(
|
|
&self,
|
|
constraint: &JsonConstraint,
|
|
value: &JsonValue,
|
|
path: &str,
|
|
errors: &mut Vec<ValidationError>,
|
|
warnings: &mut Vec<ValidationWarning>,
|
|
) -> Result<()> {
|
|
match constraint {
|
|
JsonConstraint::Type(expected_type) => {
|
|
let actual_type = match value {
|
|
JsonValue::Null => "null",
|
|
JsonValue::Bool(_) => "boolean",
|
|
JsonValue::Number(_) => "number",
|
|
JsonValue::String(_) => "string",
|
|
JsonValue::Array(_) => "array",
|
|
JsonValue::Object(_) => "object",
|
|
};
|
|
|
|
if actual_type != expected_type {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: format!("Expected type '{expected_type}', got '{actual_type}'"),
|
|
code: "type_mismatch".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
JsonConstraint::Properties(props) => {
|
|
if let JsonValue::Object(obj) = value {
|
|
for (key, prop_constraint) in props {
|
|
let prop_path = if path.is_empty() {
|
|
key.clone()
|
|
} else {
|
|
format!("{path}.{key}")
|
|
};
|
|
|
|
if let Some(prop_value) = obj.get(key) {
|
|
self.validate_constraint(
|
|
prop_constraint,
|
|
prop_value,
|
|
&prop_path,
|
|
errors,
|
|
warnings,
|
|
)?;
|
|
} else if self.strict_mode {
|
|
warnings.push(ValidationWarning {
|
|
path: prop_path,
|
|
message: format!("Property '{key}' is missing"),
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: "Expected object for properties constraint".to_string(),
|
|
code: "not_object".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
JsonConstraint::Items(item_constraint) => {
|
|
if let JsonValue::Array(arr) = value {
|
|
for (index, item) in arr.iter().enumerate() {
|
|
let item_path = format!("{path}[{index}]");
|
|
self.validate_constraint(
|
|
item_constraint,
|
|
item,
|
|
&item_path,
|
|
errors,
|
|
warnings,
|
|
)?;
|
|
}
|
|
} else {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: "Expected array for items constraint".to_string(),
|
|
code: "not_array".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
JsonConstraint::Required(required_fields) => {
|
|
if let JsonValue::Object(obj) = value {
|
|
for field in required_fields {
|
|
if !obj.contains_key(field) {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: format!("Required field '{field}' is missing"),
|
|
code: "missing_required".to_string(),
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: "Expected object for required constraint".to_string(),
|
|
code: "not_object".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
JsonConstraint::Pattern(pattern) => {
|
|
if let JsonValue::String(s) = value {
|
|
let regex =
|
|
Regex::new(pattern).map_err(|e| anyhow!("Invalid regex pattern: {e}"))?;
|
|
|
|
if !regex.is_match(s) {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: format!("String '{s}' does not match pattern '{pattern}'"),
|
|
code: "pattern_mismatch".to_string(),
|
|
});
|
|
}
|
|
} else {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: "Expected string for pattern constraint".to_string(),
|
|
code: "not_string".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
JsonConstraint::MinLength(min_len) => match value {
|
|
JsonValue::String(s) => {
|
|
if s.len() < *min_len {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: format!(
|
|
"String length {} is less than minimum {}",
|
|
s.len(),
|
|
min_len
|
|
),
|
|
code: "min_length".to_string(),
|
|
});
|
|
}
|
|
}
|
|
JsonValue::Array(arr) => {
|
|
if arr.len() < *min_len {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: format!(
|
|
"Array length {} is less than minimum {}",
|
|
arr.len(),
|
|
min_len
|
|
),
|
|
code: "min_length".to_string(),
|
|
});
|
|
}
|
|
}
|
|
_ => {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: "MinLength constraint only applies to strings and arrays"
|
|
.to_string(),
|
|
code: "invalid_constraint".to_string(),
|
|
});
|
|
}
|
|
},
|
|
|
|
JsonConstraint::MaxLength(max_len) => match value {
|
|
JsonValue::String(s) => {
|
|
if s.len() > *max_len {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: format!(
|
|
"String length {} exceeds maximum {}",
|
|
s.len(),
|
|
max_len
|
|
),
|
|
code: "max_length".to_string(),
|
|
});
|
|
}
|
|
}
|
|
JsonValue::Array(arr) => {
|
|
if arr.len() > *max_len {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: format!(
|
|
"Array length {} exceeds maximum {}",
|
|
arr.len(),
|
|
max_len
|
|
),
|
|
code: "max_length".to_string(),
|
|
});
|
|
}
|
|
}
|
|
_ => {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: "MaxLength constraint only applies to strings and arrays"
|
|
.to_string(),
|
|
code: "invalid_constraint".to_string(),
|
|
});
|
|
}
|
|
},
|
|
|
|
JsonConstraint::Minimum(min_val) => {
|
|
if let JsonValue::Number(num) = value {
|
|
if let Some(val) = num.as_f64()
|
|
&& val < *min_val
|
|
{
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: format!("Value {val} is less than minimum {min_val}"),
|
|
code: "minimum".to_string(),
|
|
});
|
|
}
|
|
} else {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: "Expected number for minimum constraint".to_string(),
|
|
code: "not_number".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
JsonConstraint::Maximum(max_val) => {
|
|
if let JsonValue::Number(num) = value {
|
|
if let Some(val) = num.as_f64()
|
|
&& val > *max_val
|
|
{
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: format!("Value {val} exceeds maximum {max_val}"),
|
|
code: "maximum".to_string(),
|
|
});
|
|
}
|
|
} else {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: "Expected number for maximum constraint".to_string(),
|
|
code: "not_number".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
JsonConstraint::Enum(allowed_values) => {
|
|
if !allowed_values.contains(value) {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: "Value is not one of allowed enum values".to_string(),
|
|
code: "enum_mismatch".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
JsonConstraint::AllOf(constraints) => {
|
|
for constraint in constraints {
|
|
self.validate_constraint(constraint, value, path, errors, warnings)?;
|
|
}
|
|
}
|
|
|
|
JsonConstraint::AnyOf(constraints) => {
|
|
let mut all_failed = true;
|
|
let mut temp_errors = Vec::new();
|
|
|
|
for constraint in constraints {
|
|
let mut constraint_errors = Vec::new();
|
|
let mut constraint_warnings = Vec::new();
|
|
|
|
self.validate_constraint(
|
|
constraint,
|
|
value,
|
|
path,
|
|
&mut constraint_errors,
|
|
&mut constraint_warnings,
|
|
)?;
|
|
|
|
if constraint_errors.is_empty() {
|
|
all_failed = false;
|
|
break;
|
|
}
|
|
|
|
temp_errors.extend(constraint_errors);
|
|
}
|
|
|
|
if all_failed {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: "Value does not match any of the allowed schemas".to_string(),
|
|
code: "any_of_failed".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
JsonConstraint::OneOf(constraints) => {
|
|
let mut matches = 0;
|
|
|
|
for constraint in constraints {
|
|
let mut constraint_errors = Vec::new();
|
|
let mut constraint_warnings = Vec::new();
|
|
|
|
self.validate_constraint(
|
|
constraint,
|
|
value,
|
|
path,
|
|
&mut constraint_errors,
|
|
&mut constraint_warnings,
|
|
)?;
|
|
|
|
if constraint_errors.is_empty() {
|
|
matches += 1;
|
|
}
|
|
}
|
|
|
|
if matches != 1 {
|
|
errors.push(ValidationError {
|
|
path: path.to_string(),
|
|
message: format!(
|
|
"Value matches {matches} schemas, but exactly 1 is required"
|
|
),
|
|
code: "one_of_failed".to_string(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// XML schema constraint
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct XmlSchema {
|
|
pub root_element: String,
|
|
pub dtd: Option<String>,
|
|
pub xsd: Option<String>,
|
|
pub namespaces: HashMap<String, String>,
|
|
pub strict_mode: bool,
|
|
}
|
|
|
|
impl XmlSchema {
|
|
/// Validate XML string against schema
|
|
pub fn validate(&self, xml: &str) -> Result<ValidationResult> {
|
|
// Basic XML validation - in production would use proper XML parser
|
|
let mut errors = Vec::new();
|
|
let mut warnings = Vec::new();
|
|
|
|
// Check for basic XML structure
|
|
if !xml.trim_start().starts_with('<') {
|
|
errors.push(ValidationError {
|
|
path: String::new(),
|
|
message: "XML must start with '<'".to_string(),
|
|
code: "invalid_xml".to_string(),
|
|
});
|
|
}
|
|
|
|
// Check root element
|
|
if let Some(start_tag_end) = xml.find('>') {
|
|
let start_tag = &xml[1..start_tag_end];
|
|
let root_name = start_tag.split_whitespace().next().unwrap_or("");
|
|
|
|
if root_name != self.root_element {
|
|
errors.push(ValidationError {
|
|
path: String::new(),
|
|
message: format!(
|
|
"Expected root element '{}', got '{}'",
|
|
self.root_element, root_name
|
|
),
|
|
code: "root_element_mismatch".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Additional validation would be implemented here
|
|
if self.strict_mode && !xml.contains("<?xml") {
|
|
warnings.push(ValidationWarning {
|
|
path: String::new(),
|
|
message: "XML declaration is recommended".to_string(),
|
|
});
|
|
}
|
|
|
|
Ok(ValidationResult {
|
|
valid: errors.is_empty(),
|
|
errors,
|
|
warnings,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// CSV schema constraint
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CsvSchema {
|
|
pub delimiter: char,
|
|
pub has_header: bool,
|
|
pub columns: Vec<CsvColumn>,
|
|
pub strict_mode: bool,
|
|
}
|
|
|
|
/// CSV column definition
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CsvColumn {
|
|
pub name: String,
|
|
pub data_type: CsvDataType,
|
|
pub required: bool,
|
|
pub pattern: Option<String>,
|
|
}
|
|
|
|
/// CSV data types
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum CsvDataType {
|
|
String,
|
|
Integer,
|
|
Float,
|
|
Boolean,
|
|
Date,
|
|
Timestamp,
|
|
}
|
|
|
|
impl CsvSchema {
|
|
/// Validate CSV string against schema
|
|
pub fn validate(&self, csv: &str) -> Result<ValidationResult> {
|
|
let mut errors = Vec::new();
|
|
let mut warnings = Vec::new();
|
|
|
|
let lines: Vec<&str> = csv.lines().collect();
|
|
if lines.is_empty() {
|
|
errors.push(ValidationError {
|
|
path: String::new(),
|
|
message: "CSV is empty".to_string(),
|
|
code: "empty_csv".to_string(),
|
|
});
|
|
return Ok(ValidationResult {
|
|
valid: false,
|
|
errors,
|
|
warnings,
|
|
});
|
|
}
|
|
|
|
let data_start = usize::from(self.has_header);
|
|
|
|
// Validate header if expected
|
|
if self.has_header && !lines.is_empty() {
|
|
let header_fields: Vec<&str> = lines[0].split(self.delimiter).collect();
|
|
if header_fields.len() != self.columns.len() {
|
|
errors.push(ValidationError {
|
|
path: "header".to_string(),
|
|
message: format!(
|
|
"Expected {} columns, got {}",
|
|
self.columns.len(),
|
|
header_fields.len()
|
|
),
|
|
code: "column_count_mismatch".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Validate data rows
|
|
for (row_idx, line) in lines.iter().enumerate().skip(data_start) {
|
|
let fields: Vec<&str> = line.split(self.delimiter).collect();
|
|
|
|
if fields.len() != self.columns.len() {
|
|
errors.push(ValidationError {
|
|
path: format!("row[{row_idx}]"),
|
|
message: format!(
|
|
"Expected {} fields, got {}",
|
|
self.columns.len(),
|
|
fields.len()
|
|
),
|
|
code: "field_count_mismatch".to_string(),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
for (field, column) in fields.iter().zip(&self.columns) {
|
|
let field_path = format!("row[{}].{}", row_idx, column.name);
|
|
|
|
// Check required fields
|
|
if column.required && field.trim().is_empty() {
|
|
errors.push(ValidationError {
|
|
path: field_path.clone(),
|
|
message: format!("Required field '{}' is empty", column.name),
|
|
code: "required_field_empty".to_string(),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// Skip empty optional fields
|
|
if field.trim().is_empty() {
|
|
continue;
|
|
}
|
|
|
|
// Validate data type
|
|
match column.data_type {
|
|
CsvDataType::Integer => {
|
|
if field.parse::<i64>().is_err() {
|
|
errors.push(ValidationError {
|
|
path: field_path.clone(),
|
|
message: format!("'{field}' is not a valid integer"),
|
|
code: "invalid_integer".to_string(),
|
|
});
|
|
}
|
|
}
|
|
CsvDataType::Float => {
|
|
if field.parse::<f64>().is_err() {
|
|
errors.push(ValidationError {
|
|
path: field_path.clone(),
|
|
message: format!("'{field}' is not a valid float"),
|
|
code: "invalid_float".to_string(),
|
|
});
|
|
}
|
|
}
|
|
CsvDataType::Boolean => {
|
|
let lower = field.to_lowercase();
|
|
if !["true", "false", "1", "0", "yes", "no"].contains(&lower.as_str()) {
|
|
errors.push(ValidationError {
|
|
path: field_path.clone(),
|
|
message: format!("'{field}' is not a valid boolean"),
|
|
code: "invalid_boolean".to_string(),
|
|
});
|
|
}
|
|
}
|
|
CsvDataType::Date => {
|
|
// Basic date validation - in production would use proper date parser
|
|
if !field.contains('-') && !field.contains('/') {
|
|
warnings.push(ValidationWarning {
|
|
path: field_path.clone(),
|
|
message: format!("'{field}' may not be a valid date format"),
|
|
});
|
|
}
|
|
}
|
|
CsvDataType::Timestamp => {
|
|
// Basic timestamp validation
|
|
if !field.contains('T') && !field.contains(' ') {
|
|
warnings.push(ValidationWarning {
|
|
path: field_path.clone(),
|
|
message: format!("'{field}' may not be a valid timestamp format"),
|
|
});
|
|
}
|
|
}
|
|
CsvDataType::String => {
|
|
// String validation
|
|
if let Some(pattern) = &column.pattern {
|
|
let regex = Regex::new(pattern)
|
|
.map_err(|e| anyhow!("Invalid regex pattern: {e}"))?;
|
|
|
|
if !regex.is_match(field) {
|
|
errors.push(ValidationError {
|
|
path: field_path.clone(),
|
|
message: format!(
|
|
"'{field}' does not match pattern '{pattern}'"
|
|
),
|
|
code: "pattern_mismatch".to_string(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(ValidationResult {
|
|
valid: errors.is_empty(),
|
|
errors,
|
|
warnings,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Template-based structured output
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct OutputTemplate {
|
|
pub template: String,
|
|
pub variables: HashMap<String, TemplateVariable>,
|
|
pub format: OutputFormat,
|
|
}
|
|
|
|
/// Template variable definition
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TemplateVariable {
|
|
pub name: String,
|
|
pub data_type: String,
|
|
pub required: bool,
|
|
pub default_value: Option<String>,
|
|
pub constraints: Option<JsonConstraint>,
|
|
}
|
|
|
|
impl OutputTemplate {
|
|
/// Fill template with provided variables
|
|
pub fn fill(&self, values: &HashMap<String, String>) -> Result<String> {
|
|
let mut output = self.template.clone();
|
|
|
|
for (var_name, var_def) in &self.variables {
|
|
let placeholder = format!("{{{{{var_name}}}}}");
|
|
|
|
if let Some(value) = values.get(var_name) {
|
|
output = output.replace(&placeholder, value);
|
|
} else if var_def.required {
|
|
return Err(anyhow!("Required variable '{var_name}' not provided"));
|
|
} else if let Some(default) = &var_def.default_value {
|
|
output = output.replace(&placeholder, default);
|
|
}
|
|
}
|
|
|
|
// Check for unfilled placeholders
|
|
if output.contains("{{") && output.contains("}}") {
|
|
let unfilled: Vec<&str> = output
|
|
.split("{{")
|
|
.skip(1)
|
|
.filter_map(|s| s.split("}}").next())
|
|
.collect();
|
|
|
|
if !unfilled.is_empty() {
|
|
return Err(anyhow!("Unfilled template variables: {unfilled:?}"));
|
|
}
|
|
}
|
|
|
|
Ok(output)
|
|
}
|
|
}
|
|
|
|
/// Validation result
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ValidationResult {
|
|
pub valid: bool,
|
|
pub errors: Vec<ValidationError>,
|
|
pub warnings: Vec<ValidationWarning>,
|
|
}
|
|
|
|
/// Validation error
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ValidationError {
|
|
pub path: String,
|
|
pub message: String,
|
|
pub code: String,
|
|
}
|
|
|
|
/// Validation warning
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ValidationWarning {
|
|
pub path: String,
|
|
pub message: String,
|
|
}
|
|
|
|
/// Structured generation manager
|
|
pub struct StructuredGenerationManager {
|
|
json_schemas: HashMap<String, JsonSchema>,
|
|
xml_schemas: HashMap<String, XmlSchema>,
|
|
csv_schemas: HashMap<String, CsvSchema>,
|
|
templates: HashMap<String, OutputTemplate>,
|
|
}
|
|
|
|
impl Default for StructuredGenerationManager {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl StructuredGenerationManager {
|
|
/// Create new structured generation manager
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
json_schemas: HashMap::new(),
|
|
xml_schemas: HashMap::new(),
|
|
csv_schemas: HashMap::new(),
|
|
templates: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Register JSON schema
|
|
pub fn register_json_schema(&mut self, name: String, schema: JsonSchema) {
|
|
self.json_schemas.insert(name, schema);
|
|
}
|
|
|
|
/// Register XML schema
|
|
pub fn register_xml_schema(&mut self, name: String, schema: XmlSchema) {
|
|
self.xml_schemas.insert(name, schema);
|
|
}
|
|
|
|
/// Register CSV schema
|
|
pub fn register_csv_schema(&mut self, name: String, schema: CsvSchema) {
|
|
self.csv_schemas.insert(name, schema);
|
|
}
|
|
|
|
/// Register output template
|
|
pub fn register_template(&mut self, name: String, template: OutputTemplate) {
|
|
self.templates.insert(name, template);
|
|
}
|
|
|
|
/// Validate output against schema
|
|
pub fn validate_output(
|
|
&self,
|
|
schema_name: &str,
|
|
output: &str,
|
|
format: &OutputFormat,
|
|
) -> Result<ValidationResult> {
|
|
match format {
|
|
OutputFormat::Json => {
|
|
let schema = self
|
|
.json_schemas
|
|
.get(schema_name)
|
|
.ok_or_else(|| anyhow!("JSON schema '{schema_name}' not found"))?;
|
|
|
|
let value: JsonValue =
|
|
serde_json::from_str(output).map_err(|e| anyhow!("Invalid JSON: {e}"))?;
|
|
|
|
schema.validate(&value)
|
|
}
|
|
OutputFormat::Xml => {
|
|
let schema = self
|
|
.xml_schemas
|
|
.get(schema_name)
|
|
.ok_or_else(|| anyhow!("XML schema '{schema_name}' not found"))?;
|
|
|
|
schema.validate(output)
|
|
}
|
|
OutputFormat::Csv | OutputFormat::Tsv => {
|
|
let schema = self
|
|
.csv_schemas
|
|
.get(schema_name)
|
|
.ok_or_else(|| anyhow!("CSV schema '{schema_name}' not found"))?;
|
|
|
|
schema.validate(output)
|
|
}
|
|
OutputFormat::Yaml => {
|
|
// Basic YAML validation - would use proper YAML parser in production
|
|
if output.trim().is_empty() {
|
|
Ok(ValidationResult {
|
|
valid: false,
|
|
errors: vec![ValidationError {
|
|
path: String::new(),
|
|
message: "YAML is empty".to_string(),
|
|
code: "empty_yaml".to_string(),
|
|
}],
|
|
warnings: Vec::new(),
|
|
})
|
|
} else {
|
|
Ok(ValidationResult {
|
|
valid: true,
|
|
errors: Vec::new(),
|
|
warnings: Vec::new(),
|
|
})
|
|
}
|
|
}
|
|
OutputFormat::Custom(_) => {
|
|
// Custom format validation would be implemented here
|
|
Ok(ValidationResult {
|
|
valid: true,
|
|
errors: Vec::new(),
|
|
warnings: Vec::new(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generate structured output from template
|
|
pub fn generate_from_template(
|
|
&self,
|
|
template_name: &str,
|
|
variables: &HashMap<String, String>,
|
|
) -> Result<(String, OutputFormat)> {
|
|
let template = self
|
|
.templates
|
|
.get(template_name)
|
|
.ok_or_else(|| anyhow!("Template '{template_name}' not found"))?;
|
|
|
|
let output = template.fill(variables)?;
|
|
Ok((output, template.format.clone()))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn test_json_schema_validation() {
|
|
let schema = JsonSchema::new(JsonConstraint::Properties(
|
|
vec![(
|
|
"name".to_string(),
|
|
Box::new(JsonConstraint::Type("string".to_string())),
|
|
)]
|
|
.into_iter()
|
|
.collect(),
|
|
));
|
|
|
|
let valid_json = json!({"name": "test"});
|
|
let result = schema.validate(&valid_json).unwrap();
|
|
assert!(result.valid);
|
|
assert!(result.errors.is_empty());
|
|
|
|
let invalid_json = json!({"name": 123});
|
|
let result = schema.validate(&invalid_json).unwrap();
|
|
assert!(!result.valid);
|
|
assert_eq!(result.errors.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_xml_schema_validation() {
|
|
let schema = XmlSchema {
|
|
root_element: "root".to_string(),
|
|
dtd: None,
|
|
xsd: None,
|
|
namespaces: HashMap::new(),
|
|
strict_mode: false,
|
|
};
|
|
|
|
let valid_xml = "<root><child>test</child></root>";
|
|
let result = schema.validate(valid_xml).unwrap();
|
|
assert!(result.valid);
|
|
|
|
let invalid_xml = "<other>test</other>";
|
|
let result = schema.validate(invalid_xml).unwrap();
|
|
assert!(!result.valid);
|
|
}
|
|
|
|
#[test]
|
|
fn test_csv_schema_validation() {
|
|
let schema = CsvSchema {
|
|
delimiter: ',',
|
|
has_header: true,
|
|
columns: vec![
|
|
CsvColumn {
|
|
name: "id".to_string(),
|
|
data_type: CsvDataType::Integer,
|
|
required: true,
|
|
pattern: None,
|
|
},
|
|
CsvColumn {
|
|
name: "name".to_string(),
|
|
data_type: CsvDataType::String,
|
|
required: true,
|
|
pattern: None,
|
|
},
|
|
],
|
|
strict_mode: false,
|
|
};
|
|
|
|
let valid_csv = "id,name\n1,Alice\n2,Bob";
|
|
let result = schema.validate(valid_csv).unwrap();
|
|
assert!(result.valid);
|
|
|
|
let invalid_csv = "id,name\nabc,Alice";
|
|
let result = schema.validate(invalid_csv).unwrap();
|
|
assert!(!result.valid);
|
|
}
|
|
|
|
#[test]
|
|
fn test_output_template() {
|
|
let template = OutputTemplate {
|
|
template: "Hello {{name}}, you are {{age}} years old!".to_string(),
|
|
variables: vec![
|
|
(
|
|
"name".to_string(),
|
|
TemplateVariable {
|
|
name: "name".to_string(),
|
|
data_type: "string".to_string(),
|
|
required: true,
|
|
default_value: None,
|
|
constraints: None,
|
|
},
|
|
),
|
|
(
|
|
"age".to_string(),
|
|
TemplateVariable {
|
|
name: "age".to_string(),
|
|
data_type: "integer".to_string(),
|
|
required: false,
|
|
default_value: Some("unknown".to_string()),
|
|
constraints: None,
|
|
},
|
|
),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
format: OutputFormat::Custom("greeting".to_string()),
|
|
};
|
|
|
|
let variables = vec![
|
|
("name".to_string(), "Alice".to_string()),
|
|
("age".to_string(), "30".to_string()),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
|
|
let result = template.fill(&variables).unwrap();
|
|
assert_eq!(result, "Hello Alice, you are 30 years old!");
|
|
}
|
|
|
|
#[test]
|
|
fn test_structured_generation_manager() {
|
|
let mut manager = StructuredGenerationManager::new();
|
|
|
|
let schema = JsonSchema::new(JsonConstraint::Properties(
|
|
vec![(
|
|
"test".to_string(),
|
|
Box::new(JsonConstraint::Type("string".to_string())),
|
|
)]
|
|
.into_iter()
|
|
.collect(),
|
|
));
|
|
|
|
manager.register_json_schema("test_schema".to_string(), schema);
|
|
|
|
let valid_output = r#"{"test": "value"}"#;
|
|
let result = manager
|
|
.validate_output("test_schema", valid_output, &OutputFormat::Json)
|
|
.unwrap();
|
|
assert!(result.valid);
|
|
}
|
|
|
|
#[test]
|
|
fn test_output_format_mime_types() {
|
|
assert_eq!(OutputFormat::Json.mime_type(), "application/json");
|
|
assert_eq!(OutputFormat::Xml.mime_type(), "application/xml");
|
|
assert_eq!(OutputFormat::Csv.mime_type(), "text/csv");
|
|
}
|
|
}
|