use crate::errors::SelfwareError;
use crate::swl::types::schema::{FieldType, StateField, StateSchema};
use serde_json::Value;
use std::collections::HashMap;
use std::result::Result as StdResult;
#[derive(Debug, Clone)]
pub struct ValidationError {
pub field: String,
pub message: String,
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Field '{}': {}", self.field, self.message)
}
}
impl std::error::Error for ValidationError {}
pub fn validate_state_against_schema(
state: &HashMap<String, Value>,
schema: &StateSchema,
) -> StdResult<(), SelfwareError> {
let mut errors = Vec::new();
for field in &schema.fields {
if field.default.is_none() && !state.contains_key(&field.name) {
errors.push(ValidationError {
field: field.name.clone(),
message: "Required field is missing".to_string(),
});
continue;
}
if let Some(value) = state.get(&field.name) {
if let Err(e) = validate_value_type(value, &field.field_type) {
errors.push(ValidationError {
field: field.name.clone(),
message: e,
});
}
}
}
let known_fields: std::collections::HashSet<_> =
schema.fields.iter().map(|f| &f.name).collect();
for key in state.keys() {
if !known_fields.contains(key) {
errors.push(ValidationError {
field: key.clone(),
message: "Unknown field (not defined in schema)".to_string(),
});
}
}
if !errors.is_empty() {
let error_messages: Vec<_> = errors.iter().map(|e| e.to_string()).collect();
return Err(SelfwareError::Internal(format!(
"State validation failed:\n{}",
error_messages.join("\n")
)));
}
Ok(())
}
pub fn validate_value_type(
value: &Value,
field_type: &FieldType,
) -> std::result::Result<(), String> {
match field_type {
FieldType::String => {
if !value.is_string() {
return Err(format!("Expected string, got {}", json_type_name(value)));
}
}
FieldType::Integer => {
if !value.is_i64() && !value.is_u64() {
return Err(format!("Expected integer, got {}", json_type_name(value)));
}
}
FieldType::Float => {
if !value.is_f64() && !value.is_i64() && !value.is_u64() {
return Err(format!("Expected float, got {}", json_type_name(value)));
}
}
FieldType::Boolean => {
if !value.is_boolean() {
return Err(format!("Expected boolean, got {}", json_type_name(value)));
}
}
FieldType::Array(element_type) => {
if let Some(arr) = value.as_array() {
for (i, item) in arr.iter().enumerate() {
if let Err(e) = validate_value_type(item, element_type) {
return Err(format!("Array element {}: {}", i, e));
}
}
} else {
return Err(format!("Expected array, got {}", json_type_name(value)));
}
}
FieldType::Object(_) => {
if !value.is_object() {
return Err(format!("Expected object, got {}", json_type_name(value)));
}
}
FieldType::AgentRef => {
if !value.is_string() {
return Err(format!(
"Expected agent reference (string), got {}",
json_type_name(value)
));
}
}
FieldType::ToolRef => {
if !value.is_string() {
return Err(format!(
"Expected tool reference (string), got {}",
json_type_name(value)
));
}
}
}
Ok(())
}
fn json_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(n) => {
if n.is_i64() || n.is_u64() {
"integer"
} else {
"float"
}
}
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
pub fn validate_field(
state: &HashMap<String, Value>,
field: &StateField,
) -> std::result::Result<(), ValidationError> {
if field.default.is_none() && !state.contains_key(&field.name) {
return Err(ValidationError {
field: field.name.clone(),
message: "Required field is missing".to_string(),
});
}
if let Some(value) = state.get(&field.name) {
if let Err(msg) = validate_value_type(value, &field.field_type) {
return Err(ValidationError {
field: field.name.clone(),
message: msg,
});
}
}
Ok(())
}
pub fn apply_defaults(state: &mut HashMap<String, Value>, schema: &StateSchema) {
for field in &schema.fields {
if !state.contains_key(&field.name) {
if let Some(ref default) = field.default {
if let Ok(json_value) = yaml_to_json(default) {
state.insert(field.name.clone(), json_value);
}
}
}
}
}
fn yaml_to_json(value: &serde_yaml::Value) -> std::result::Result<Value, serde_json::Error> {
serde_json::to_value(value)
}
pub fn get_validation_issues(
state: &HashMap<String, Value>,
schema: &StateSchema,
) -> Vec<ValidationError> {
let mut issues = Vec::new();
for field in &schema.fields {
if let Err(e) = validate_field(state, field) {
issues.push(e);
}
}
let known_fields: std::collections::HashSet<_> =
schema.fields.iter().map(|f| &f.name).collect();
for key in state.keys() {
if !known_fields.contains(key) {
issues.push(ValidationError {
field: key.clone(),
message: "Unknown field (not defined in schema)".to_string(),
});
}
}
issues
}
pub fn validate_state_transition(
old_state: &HashMap<String, Value>,
new_state: &HashMap<String, Value>,
schema: &StateSchema,
) -> StdResult<(), SelfwareError> {
for field in &schema.fields {
if field.default.is_none()
&& old_state.contains_key(&field.name)
&& !new_state.contains_key(&field.name)
{
return Err(SelfwareError::Internal(format!(
"State transition invalid: required field '{}' was removed",
field.name
)));
}
}
validate_state_against_schema(new_state, schema)
}
#[cfg(test)]
#[path = "../../../tests/unit/swl/state/validation/validation_test.rs"]
mod tests;