use crate::error::{NomlError, Result};
use crate::value::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct Schema {
pub fields: HashMap<String, FieldSchema>,
pub allow_additional: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FieldSchema {
pub field_type: FieldType,
pub required: bool,
pub description: Option<String>,
pub default: Option<Value>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FieldType {
String,
Integer,
Float,
Bool,
Binary,
DateTime,
Array(Box<FieldType>),
Table(Schema),
Any,
Union(Vec<FieldType>),
}
impl Schema {
pub fn new() -> Self {
Self {
fields: HashMap::new(),
allow_additional: true,
}
}
pub fn required_field(mut self, name: &str, field_type: FieldType) -> Self {
self.fields.insert(
name.to_string(),
FieldSchema {
field_type,
required: true,
description: None,
default: None,
},
);
self
}
pub fn optional_field(mut self, name: &str, field_type: FieldType) -> Self {
self.fields.insert(
name.to_string(),
FieldSchema {
field_type,
required: false,
description: None,
default: None,
},
);
self
}
pub fn field_with_default(mut self, name: &str, field_type: FieldType, default: Value) -> Self {
self.fields.insert(
name.to_string(),
FieldSchema {
field_type,
required: false,
description: None,
default: Some(default),
},
);
self
}
pub fn allow_additional(mut self, allow: bool) -> Self {
self.allow_additional = allow;
self
}
pub fn validate(&self, value: &Value) -> Result<()> {
match value {
Value::Table(table) => {
for (field_name, field_schema) in &self.fields {
if field_schema.required && !table.contains_key(field_name) {
return Err(NomlError::validation(format!(
"Required field '{field_name}' is missing"
)));
}
}
for (key, val) in table {
if let Some(field_schema) = self.fields.get(key) {
self.validate_field_type(val, &field_schema.field_type, key)?;
} else if !self.allow_additional {
return Err(NomlError::validation(format!(
"Additional field '{key}' is not allowed"
)));
}
}
Ok(())
}
_ => Err(NomlError::validation(
"Schema validation requires a table/object at the root".to_string(),
)),
}
}
fn validate_field_type(
&self,
value: &Value,
expected_type: &FieldType,
field_path: &str,
) -> Result<()> {
match (value, expected_type) {
(Value::String(_), FieldType::String) => Ok(()),
(Value::Integer(_), FieldType::Integer) => Ok(()),
(Value::Float(_), FieldType::Float) => Ok(()),
(Value::Bool(_), FieldType::Bool) => Ok(()),
(Value::Binary(_), FieldType::Binary) => Ok(()),
#[cfg(feature = "chrono")]
(Value::DateTime(_), FieldType::DateTime) => Ok(()),
(_, FieldType::Any) => Ok(()),
(Value::Array(arr), FieldType::Array(element_type)) => {
for (i, item) in arr.iter().enumerate() {
let item_path = format!("{field_path}[{i}]");
self.validate_field_type(item, element_type, &item_path)?;
}
Ok(())
}
(Value::Table(_), FieldType::Table(nested_schema)) => nested_schema.validate(value),
(val, FieldType::Union(types)) => {
for field_type in types {
if self
.validate_field_type(val, field_type, field_path)
.is_ok()
{
return Ok(());
}
}
Err(NomlError::validation(format!(
"Field '{field_path}' does not match any of the expected types"
)))
}
_ => Err(NomlError::validation(format!(
"Field '{field_path}' has incorrect type. Expected {expected_type:?}, got {:?}",
self.value_type_name(value)
))),
}
}
fn value_type_name(&self, value: &Value) -> &'static str {
match value {
Value::String(_) => "String",
Value::Integer(_) => "Integer",
Value::Float(_) => "Float",
Value::Bool(_) => "Bool",
Value::Array(_) => "Array",
Value::Table(_) => "Table",
Value::Null => "Null",
Value::Size(_) => "Size",
Value::Duration(_) => "Duration",
Value::Binary(_) => "Binary",
#[cfg(feature = "chrono")]
Value::DateTime(_) => "DateTime",
}
}
}
impl Default for Schema {
fn default() -> Self {
Self::new()
}
}
pub struct SchemaBuilder {
schema: Schema,
}
impl SchemaBuilder {
pub fn new() -> Self {
Self {
schema: Schema::new(),
}
}
pub fn require_string(mut self, name: &str) -> Self {
self.schema = self.schema.required_field(name, FieldType::String);
self
}
pub fn require_integer(mut self, name: &str) -> Self {
self.schema = self.schema.required_field(name, FieldType::Integer);
self
}
pub fn optional_bool(mut self, name: &str) -> Self {
self.schema = self.schema.optional_field(name, FieldType::Bool);
self
}
pub fn build(self) -> Schema {
self.schema
}
}
impl Default for SchemaBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::Value;
use std::collections::BTreeMap;
#[test]
fn test_basic_schema_validation() {
let schema = Schema::new()
.required_field("name", FieldType::String)
.required_field("port", FieldType::Integer)
.optional_field("debug", FieldType::Bool);
let mut config = BTreeMap::new();
config.insert("name".to_string(), Value::String("test".to_string()));
config.insert("port".to_string(), Value::Integer(8080));
config.insert("debug".to_string(), Value::Bool(true));
let valid_value = Value::Table(config);
assert!(schema.validate(&valid_value).is_ok());
let mut invalid_config = BTreeMap::new();
invalid_config.insert("name".to_string(), Value::String("test".to_string()));
let invalid_value = Value::Table(invalid_config);
assert!(schema.validate(&invalid_value).is_err());
}
#[test]
fn test_schema_builder() {
let schema = SchemaBuilder::new()
.require_string("app_name")
.require_integer("version")
.optional_bool("debug")
.build();
let mut config = BTreeMap::new();
config.insert("app_name".to_string(), Value::String("MyApp".to_string()));
config.insert("version".to_string(), Value::Integer(1));
let value = Value::Table(config);
assert!(schema.validate(&value).is_ok());
}
#[test]
fn test_array_validation() {
let schema =
Schema::new().required_field("tags", FieldType::Array(Box::new(FieldType::String)));
let mut config = BTreeMap::new();
config.insert(
"tags".to_string(),
Value::Array(vec![
Value::String("web".to_string()),
Value::String("api".to_string()),
]),
);
let value = Value::Table(config);
assert!(schema.validate(&value).is_ok());
let mut invalid_config = BTreeMap::new();
invalid_config.insert(
"tags".to_string(),
Value::Array(vec![
Value::String("web".to_string()),
Value::Integer(123), ]),
);
let invalid_value = Value::Table(invalid_config);
assert!(schema.validate(&invalid_value).is_err());
}
}