use crate::validation::error::ValidationError;
use regex::Regex;
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Schema {
pub version: String,
pub description: String,
pub tables: HashMap<String, TableDefinition>,
}
#[derive(Debug, Clone)]
pub struct TableDefinition {
pub name: String,
pub is_pattern: bool,
pub pattern_constraint: Option<Regex>,
pub required: bool,
pub description: Option<String>,
pub fields: Vec<FieldDefinition>,
}
#[derive(Debug, Clone)]
pub struct FieldDefinition {
pub name: String,
pub field_type: String,
pub required: bool,
pub required_if: Option<String>,
pub default: Option<toml::Value>,
pub enum_values: Option<Vec<String>>,
pub min: Option<i64>,
pub max: Option<i64>,
pub min_items: Option<usize>,
pub max_items: Option<usize>,
pub array_item_type: Option<String>,
pub pattern: Option<Regex>,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
struct RawSchema {
schema: SchemaMetadata,
table: Vec<RawTable>,
}
#[derive(Debug, Deserialize)]
struct SchemaMetadata {
version: String,
description: String,
}
#[derive(Debug, Deserialize)]
struct RawTable {
name: String,
#[serde(default)]
required: bool,
#[serde(default)]
pattern: bool,
pattern_constraint: Option<String>,
description: Option<String>,
#[serde(default)]
field: Vec<RawField>,
}
#[derive(Debug, Deserialize)]
struct RawField {
name: String,
#[serde(rename = "type")]
field_type: String,
#[serde(default)]
required: bool,
required_if: Option<String>,
default: Option<toml::Value>,
#[serde(rename = "enum")]
enum_values: Option<Vec<String>>,
min: Option<i64>,
max: Option<i64>,
min_items: Option<usize>,
max_items: Option<usize>,
array_item_type: Option<String>,
pattern_constraint: Option<String>,
description: Option<String>,
}
impl Schema {
#[allow(clippy::should_implement_trait)]
pub fn from_str(schema_toml: &str) -> Result<Self, ValidationError> {
let raw: RawSchema = toml::from_str(schema_toml).map_err(|e| {
ValidationError::SchemaParseError(format!("Failed to parse schema TOML: {}", e))
})?;
let mut tables = HashMap::new();
for raw_table in raw.table {
let pattern_constraint = if let Some(pattern_str) = &raw_table.pattern_constraint {
Some(Regex::new(pattern_str).map_err(|e| {
ValidationError::SchemaParseError(format!(
"Invalid pattern constraint '{}': {}",
pattern_str, e
))
})?)
} else {
None
};
let mut fields = Vec::new();
for raw_field in raw_table.field {
let pattern = if let Some(pattern_str) = &raw_field.pattern_constraint {
Some(Regex::new(pattern_str).map_err(|e| {
ValidationError::SchemaParseError(format!(
"Invalid pattern for field '{}': {}",
raw_field.name, e
))
})?)
} else {
None
};
fields.push(FieldDefinition {
name: raw_field.name,
field_type: raw_field.field_type,
required: raw_field.required,
required_if: raw_field.required_if,
default: raw_field.default,
enum_values: raw_field.enum_values,
min: raw_field.min,
max: raw_field.max,
min_items: raw_field.min_items,
max_items: raw_field.max_items,
array_item_type: raw_field.array_item_type,
pattern,
description: raw_field.description,
});
}
let table_def = TableDefinition {
name: raw_table.name.clone(),
is_pattern: raw_table.pattern,
pattern_constraint,
required: raw_table.required,
description: raw_table.description,
fields,
};
tables.insert(raw_table.name, table_def);
}
Ok(Schema {
version: raw.schema.version,
description: raw.schema.description,
tables,
})
}
pub fn find_table(&self, table_path: &str) -> Option<&TableDefinition> {
if let Some(table_def) = self.tables.get(table_path) {
return Some(table_def);
}
self.tables.values().find(|&table_def| {
table_def.is_pattern && self.matches_pattern(table_path, &table_def.name)
})
}
pub fn matches_pattern(&self, table_path: &str, pattern: &str) -> bool {
if !pattern.contains('*') {
return table_path == pattern;
}
let pattern_regex = pattern.replace(".", r"\.").replace("*", "[^.]+");
let pattern_regex = format!("^{}$", pattern_regex);
if let Ok(re) = Regex::new(&pattern_regex) {
re.is_match(table_path)
} else {
false
}
}
pub fn get_concrete_tables(&self) -> impl Iterator<Item = &TableDefinition> {
self.tables.values().filter(|t| !t.is_pattern)
}
}
impl TableDefinition {
pub fn find_field(&self, field_name: &str) -> Option<&FieldDefinition> {
self.fields.iter().find(|f| f.name == field_name)
}
pub fn get_fields(&self) -> &[FieldDefinition] {
&self.fields
}
}
impl FieldDefinition {
pub fn is_conditionally_required(&self, table_data: &toml::Value) -> bool {
if let Some(condition) = &self.required_if {
evaluate_condition(condition, table_data)
} else {
false
}
}
}
fn evaluate_condition(condition: &str, table_data: &toml::Value) -> bool {
let condition = condition.trim();
if !condition.contains("==") && !condition.contains("!=") {
let field_name = condition.replace(" exists", "").trim().to_string();
return table_data.get(&field_name).is_some();
}
if let Some((left, right)) = condition.split_once("==") {
let field_name = left.trim();
let expected_value = right.trim().trim_matches('"').trim_matches('\'');
if let Some(field_value) = table_data.get(field_name) {
match field_value {
toml::Value::String(s) => return s == expected_value,
toml::Value::Boolean(b) => {
return expected_value == "true" && *b || expected_value == "false" && !*b
}
toml::Value::Integer(i) => {
if let Ok(expected_int) = expected_value.parse::<i64>() {
return *i == expected_int;
}
}
_ => {}
}
}
return false;
}
if let Some((left, right)) = condition.split_once("!=") {
let field_name = left.trim();
let expected_value = right.trim().trim_matches('"').trim_matches('\'');
if let Some(field_value) = table_data.get(field_name) {
match field_value {
toml::Value::String(s) => return s != expected_value,
toml::Value::Boolean(b) => {
return !(expected_value == "true" && *b || expected_value == "false" && !*b)
}
toml::Value::Integer(i) => {
if let Ok(expected_int) = expected_value.parse::<i64>() {
return *i != expected_int;
}
}
_ => {}
}
}
return true; }
false
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_schema() -> &'static str {
r#"
[schema]
version = "1.0"
description = "Test schema"
[[table]]
name = "proxy"
required = true
description = "Core proxy configuration"
[[table.field]]
name = "id"
type = "string"
required = true
description = "Proxy ID"
[[table.field]]
name = "log_level"
type = "string"
required = false
default = "error"
enum = ["trace", "debug", "info", "warn", "error"]
[[table.field]]
name = "port"
type = "integer"
required = false
min = 1
max = 65535
[[table]]
name = "network.*"
pattern = true
pattern_constraint = "^[a-z0-9_-]+$"
required = false
[[table.field]]
name = "bind_address"
type = "string"
required = true
"#
}
#[test]
fn test_parse_schema() {
let schema = Schema::from_str(sample_schema()).unwrap();
assert_eq!(schema.version, "1.0");
assert_eq!(schema.description, "Test schema");
assert_eq!(schema.tables.len(), 2);
}
#[test]
fn test_find_table_exact() {
let schema = Schema::from_str(sample_schema()).unwrap();
let table = schema.find_table("proxy");
assert!(table.is_some());
assert_eq!(table.unwrap().name, "proxy");
}
#[test]
fn test_find_table_pattern() {
let schema = Schema::from_str(sample_schema()).unwrap();
let table = schema.find_table("network.default");
assert!(table.is_some());
assert_eq!(table.unwrap().name, "network.*");
}
#[test]
fn test_find_field() {
let schema = Schema::from_str(sample_schema()).unwrap();
let table = schema.find_table("proxy").unwrap();
let field = table.find_field("id");
assert!(field.is_some());
assert_eq!(field.unwrap().field_type, "string");
assert!(field.unwrap().required);
}
#[test]
fn test_enum_values() {
let schema = Schema::from_str(sample_schema()).unwrap();
let table = schema.find_table("proxy").unwrap();
let field = table.find_field("log_level").unwrap();
assert!(field.enum_values.is_some());
let enums = field.enum_values.as_ref().unwrap();
assert_eq!(enums.len(), 5);
assert!(enums.contains(&"error".to_string()));
}
#[test]
fn test_numeric_range() {
let schema = Schema::from_str(sample_schema()).unwrap();
let table = schema.find_table("proxy").unwrap();
let field = table.find_field("port").unwrap();
assert_eq!(field.min, Some(1));
assert_eq!(field.max, Some(65535));
}
#[test]
fn test_evaluate_condition_equals() {
let mut table = toml::map::Map::new();
table.insert("enabled".to_string(), toml::Value::Boolean(true));
let table_value = toml::Value::Table(table);
assert!(evaluate_condition("enabled == true", &table_value));
assert!(!evaluate_condition("enabled == false", &table_value));
}
#[test]
fn test_evaluate_condition_string() {
let mut table = toml::map::Map::new();
table.insert("type".to_string(), toml::Value::String("http".to_string()));
let table_value = toml::Value::Table(table);
assert!(evaluate_condition("type == \"http\"", &table_value));
assert!(!evaluate_condition("type == \"tcp\"", &table_value));
}
#[test]
fn test_evaluate_condition_exists() {
let mut table = toml::map::Map::new();
table.insert(
"field".to_string(),
toml::Value::String("value".to_string()),
);
let table_value = toml::Value::Table(table);
assert!(evaluate_condition("field exists", &table_value));
assert!(evaluate_condition("field", &table_value));
assert!(!evaluate_condition("missing", &table_value));
}
#[test]
fn test_pattern_matching() {
let schema = Schema::from_str(sample_schema()).unwrap();
assert!(schema.matches_pattern("network.default", "network.*"));
assert!(schema.matches_pattern("network.management", "network.*"));
assert!(!schema.matches_pattern("network.sub.deep", "network.*"));
assert!(!schema.matches_pattern("other.default", "network.*"));
assert!(!schema.matches_pattern("network", "network.*"));
}
}