use std::collections::HashMap;
use crate::config::value::{ConfigDict, ConfigValue};
#[derive(Debug, Clone)]
pub struct ValidationError {
pub path: String,
pub message: String,
}
impl ValidationError {
pub fn new(path: &str, message: &str) -> Self {
Self {
path: path.to_string(),
message: message.to_string(),
}
}
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.path, self.message)
}
}
impl std::error::Error for ValidationError {}
#[derive(Debug, Clone, PartialEq)]
pub enum TypeSpec {
Any,
Null,
Bool,
Int,
Float,
String,
List(Box<TypeSpec>),
Dict(Box<TypeSpec>),
Optional(Box<TypeSpec>),
Union(Vec<TypeSpec>),
}
impl TypeSpec {
pub fn parse(s: &str) -> Option<Self> {
let s = s.trim();
match s.to_lowercase().as_str() {
"any" => return Some(TypeSpec::Any),
"none" | "null" => return Some(TypeSpec::Null),
"bool" | "boolean" => return Some(TypeSpec::Bool),
"int" | "integer" => return Some(TypeSpec::Int),
"float" | "number" => return Some(TypeSpec::Float),
"str" | "string" => return Some(TypeSpec::String),
_ => {}
}
if let Some(inner) = s.strip_prefix("List[").and_then(|s| s.strip_suffix(']')) {
return TypeSpec::parse(inner).map(|t| TypeSpec::List(Box::new(t)));
}
if let Some(inner) = s
.strip_prefix("Optional[")
.and_then(|s| s.strip_suffix(']'))
{
return TypeSpec::parse(inner).map(|t| TypeSpec::Optional(Box::new(t)));
}
if let Some(inner) = s.strip_prefix("Union[").and_then(|s| s.strip_suffix(']')) {
let types: Vec<_> = inner
.split(',')
.filter_map(|t| TypeSpec::parse(t.trim()))
.collect();
if !types.is_empty() {
return Some(TypeSpec::Union(types));
}
}
if let Some(inner) = s.strip_prefix("Dict[").and_then(|s| s.strip_suffix(']')) {
if let Some(comma) = inner.find(',') {
let value_type = &inner[comma + 1..];
return TypeSpec::parse(value_type.trim()).map(|t| TypeSpec::Dict(Box::new(t)));
}
}
None
}
pub fn matches(&self, value: &ConfigValue) -> bool {
match (self, value) {
(TypeSpec::Any, _) => true,
(TypeSpec::Null, ConfigValue::Null) => true,
(TypeSpec::Bool, ConfigValue::Bool(_)) => true,
(TypeSpec::Int, ConfigValue::Int(_)) => true,
(TypeSpec::Float, ConfigValue::Float(_)) => true,
(TypeSpec::Float, ConfigValue::Int(_)) => true, (TypeSpec::String, ConfigValue::String(_)) => true,
(TypeSpec::String, ConfigValue::Interpolation(_)) => true, (TypeSpec::List(inner), ConfigValue::List(items)) => {
items.iter().all(|item| inner.matches(item))
}
(TypeSpec::Dict(inner), ConfigValue::Dict(dict)) => {
dict.values().all(|v| inner.matches(v))
}
(TypeSpec::Optional(_inner), ConfigValue::Null) => true,
(TypeSpec::Optional(inner), value) => inner.matches(value),
(TypeSpec::Union(types), value) => types.iter().any(|t| t.matches(value)),
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub struct ConfigSchema {
pub fields: HashMap<String, (TypeSpec, bool, Option<ConfigValue>)>,
}
impl ConfigSchema {
pub fn new() -> Self {
Self {
fields: HashMap::new(),
}
}
pub fn required(mut self, name: &str, type_spec: TypeSpec) -> Self {
self.fields
.insert(name.to_string(), (type_spec, true, None));
self
}
pub fn optional(mut self, name: &str, type_spec: TypeSpec, default: ConfigValue) -> Self {
self.fields
.insert(name.to_string(), (type_spec, false, Some(default)));
self
}
pub fn validate(&self, config: &ConfigDict) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
for (name, (type_spec, required, _default)) in &self.fields {
if *required && config.get(name).is_none() {
errors.push(ValidationError::new(name, "Missing required field"));
continue;
}
if let Some(value) = config.get(name) {
if !type_spec.matches(value) {
errors.push(ValidationError::new(
name,
&format!("Type mismatch: expected {:?}", type_spec),
));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn apply_defaults(&self, config: &mut ConfigDict) {
for (name, (_type_spec, _required, default)) in &self.fields {
if config.get(name).is_none() {
if let Some(default_value) = default {
config.insert(name.clone(), default_value.clone());
}
}
}
}
}
impl Default for ConfigSchema {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_type_spec_parse() {
assert_eq!(TypeSpec::parse("int"), Some(TypeSpec::Int));
assert_eq!(TypeSpec::parse("str"), Some(TypeSpec::String));
assert_eq!(TypeSpec::parse("bool"), Some(TypeSpec::Bool));
assert_eq!(TypeSpec::parse("float"), Some(TypeSpec::Float));
assert_eq!(TypeSpec::parse("Any"), Some(TypeSpec::Any));
}
#[test]
fn test_type_spec_parse_generic() {
assert_eq!(
TypeSpec::parse("List[int]"),
Some(TypeSpec::List(Box::new(TypeSpec::Int)))
);
assert_eq!(
TypeSpec::parse("Optional[str]"),
Some(TypeSpec::Optional(Box::new(TypeSpec::String)))
);
}
#[test]
fn test_type_spec_matches() {
assert!(TypeSpec::Int.matches(&ConfigValue::Int(42)));
assert!(!TypeSpec::Int.matches(&ConfigValue::String("42".to_string())));
assert!(TypeSpec::Float.matches(&ConfigValue::Int(42))); assert!(TypeSpec::Optional(Box::new(TypeSpec::Int)).matches(&ConfigValue::Null));
assert!(TypeSpec::Optional(Box::new(TypeSpec::Int)).matches(&ConfigValue::Int(42)));
}
#[test]
fn test_schema_validation() {
let schema = ConfigSchema::new()
.required("name", TypeSpec::String)
.required("port", TypeSpec::Int)
.optional(
"host",
TypeSpec::String,
ConfigValue::String("localhost".to_string()),
);
let mut config = ConfigDict::new();
config.insert("name".to_string(), ConfigValue::String("test".to_string()));
config.insert("port".to_string(), ConfigValue::Int(8080));
assert!(schema.validate(&config).is_ok());
let mut bad_config = ConfigDict::new();
bad_config.insert("name".to_string(), ConfigValue::String("test".to_string()));
assert!(schema.validate(&bad_config).is_err());
}
#[test]
fn test_schema_apply_defaults() {
let schema = ConfigSchema::new()
.optional(
"host",
TypeSpec::String,
ConfigValue::String("localhost".to_string()),
)
.optional("port", TypeSpec::Int, ConfigValue::Int(8080));
let mut config = ConfigDict::new();
schema.apply_defaults(&mut config);
assert_eq!(config.get("host").unwrap().as_str(), Some("localhost"));
assert_eq!(config.get("port").unwrap().as_int(), Some(8080));
}
}