use crate::types::{Document, Value};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Schema {
pub name: String,
pub fields: HashMap<String, FieldSchema>,
pub required: Vec<String>,
pub additional_properties: bool,
}
impl Schema {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
fields: HashMap::new(),
required: Vec::new(),
additional_properties: true,
}
}
pub fn field(mut self, name: impl Into<String>, schema: FieldSchema) -> Self {
self.fields.insert(name.into(), schema);
self
}
pub fn require(mut self, name: impl Into<String>) -> Self {
self.required.push(name.into());
self
}
pub fn additional_properties(mut self, allow: bool) -> Self {
self.additional_properties = allow;
self
}
pub fn validate(&self, doc: &Document) -> ValidationResult {
let mut errors = Vec::new();
for required in &self.required {
if !doc.contains(required) {
errors.push(format!("Missing required field: {}", required));
}
}
for (field_name, field_schema) in &self.fields {
if let Some(value) = doc.get(field_name) {
if let Err(err) = field_schema.validate(value) {
errors.push(format!("Field '{}': {}", field_name, err));
}
}
}
if !self.additional_properties {
for key in doc.keys() {
if !self.fields.contains_key(key) {
errors.push(format!("Unknown field: {}", key));
}
}
}
ValidationResult {
is_valid: errors.is_empty(),
errors,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldSchema {
pub field_type: FieldType,
pub nullable: bool,
pub min: Option<f64>,
pub max: Option<f64>,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub pattern: Option<String>,
pub enum_values: Option<Vec<Value>>,
pub items: Option<Box<FieldSchema>>,
pub properties: Option<HashMap<String, FieldSchema>>,
}
impl FieldSchema {
pub fn new(field_type: FieldType) -> Self {
Self {
field_type,
nullable: false,
min: None,
max: None,
min_length: None,
max_length: None,
pattern: None,
enum_values: None,
items: None,
properties: None,
}
}
pub fn string() -> Self {
Self::new(FieldType::String)
}
pub fn int() -> Self {
Self::new(FieldType::Int)
}
pub fn float() -> Self {
Self::new(FieldType::Float)
}
pub fn bool() -> Self {
Self::new(FieldType::Bool)
}
pub fn array(items: FieldSchema) -> Self {
let mut schema = Self::new(FieldType::Array);
schema.items = Some(Box::new(items));
schema
}
pub fn object() -> Self {
Self::new(FieldType::Object)
}
pub fn nullable(mut self) -> Self {
self.nullable = true;
self
}
pub fn min(mut self, min: f64) -> Self {
self.min = Some(min);
self
}
pub fn max(mut self, max: f64) -> Self {
self.max = Some(max);
self
}
pub fn min_length(mut self, len: usize) -> Self {
self.min_length = Some(len);
self
}
pub fn max_length(mut self, len: usize) -> Self {
self.max_length = Some(len);
self
}
pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
self.pattern = Some(pattern.into());
self
}
pub fn enum_values(mut self, values: Vec<Value>) -> Self {
self.enum_values = Some(values);
self
}
pub fn validate(&self, value: &Value) -> Result<(), String> {
if value.is_null() {
if self.nullable {
return Ok(());
}
return Err("Value cannot be null".to_string());
}
if !self.field_type.matches(value) {
return Err(format!(
"Expected type {:?}, got {:?}",
self.field_type,
value_type(value)
));
}
if let Some(ref enum_values) = self.enum_values {
if !enum_values.contains(value) {
return Err("Value not in allowed enum values".to_string());
}
}
match value {
Value::Int(n) => {
if let Some(min) = self.min {
if (*n as f64) < min {
return Err(format!("Value {} is less than minimum {}", n, min));
}
}
if let Some(max) = self.max {
if (*n as f64) > max {
return Err(format!("Value {} is greater than maximum {}", n, max));
}
}
}
Value::Float(f) => {
if let Some(min) = self.min {
if *f < min {
return Err(format!("Value {} is less than minimum {}", f, min));
}
}
if let Some(max) = self.max {
if *f > max {
return Err(format!("Value {} is greater than maximum {}", f, max));
}
}
}
Value::String(s) => {
if let Some(min_len) = self.min_length {
if s.len() < min_len {
return Err(format!(
"String length {} is less than minimum {}",
s.len(),
min_len
));
}
}
if let Some(max_len) = self.max_length {
if s.len() > max_len {
return Err(format!(
"String length {} is greater than maximum {}",
s.len(),
max_len
));
}
}
if let Some(ref pattern) = self.pattern {
let re = regex::RegexBuilder::new(pattern)
.size_limit(1024 * 1024) .build()
.map_err(|e| format!("Invalid regex pattern: {}", e))?;
if !re.is_match(s) {
return Err(format!("String does not match pattern: {}", pattern));
}
}
}
Value::Array(arr) => {
if let Some(min_len) = self.min_length {
if arr.len() < min_len {
return Err(format!(
"Array length {} is less than minimum {}",
arr.len(),
min_len
));
}
}
if let Some(max_len) = self.max_length {
if arr.len() > max_len {
return Err(format!(
"Array length {} is greater than maximum {}",
arr.len(),
max_len
));
}
}
if let Some(ref items_schema) = self.items {
for (i, item) in arr.iter().enumerate() {
if let Err(e) = items_schema.validate(item) {
return Err(format!("Array item {}: {}", i, e));
}
}
}
}
Value::Object(obj) => {
if let Some(ref props) = self.properties {
for (key, prop_schema) in props {
if let Some(value) = obj.get(key) {
if let Err(e) = prop_schema.validate(value) {
return Err(format!("Property '{}': {}", key, e));
}
}
}
}
}
_ => {}
}
Ok(())
}
}
fn value_type(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Int(_) => "int",
Value::Float(_) => "float",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FieldType {
String,
Int,
Float,
Number,
Bool,
Array,
Object,
Any,
}
impl FieldType {
fn matches(&self, value: &Value) -> bool {
matches!(
(self, value),
(Self::Any, _)
| (Self::String, Value::String(_))
| (Self::Int, Value::Int(_))
| (Self::Float, Value::Float(_))
| (Self::Number, Value::Int(_) | Value::Float(_))
| (Self::Bool, Value::Bool(_))
| (Self::Array, Value::Array(_))
| (Self::Object, Value::Object(_))
)
}
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub is_valid: bool,
pub errors: Vec<String>,
}
impl ValidationResult {
pub fn valid() -> Self {
Self {
is_valid: true,
errors: Vec::new(),
}
}
pub fn invalid(errors: Vec<String>) -> Self {
Self {
is_valid: false,
errors,
}
}
}
pub struct SchemaBuilder {
schema: Schema,
}
impl SchemaBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
schema: Schema::new(name),
}
}
pub fn field(mut self, name: impl Into<String>, schema: FieldSchema) -> Self {
self.schema.fields.insert(name.into(), schema);
self
}
pub fn required_field(mut self, name: impl Into<String>, schema: FieldSchema) -> Self {
let name = name.into();
self.schema.fields.insert(name.clone(), schema);
self.schema.required.push(name);
self
}
pub fn additional_properties(mut self, allow: bool) -> Self {
self.schema.additional_properties = allow;
self
}
pub fn build(self) -> Schema {
self.schema
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_type_validation() {
let schema = FieldSchema::string();
assert!(schema.validate(&Value::String("hello".to_string())).is_ok());
assert!(schema.validate(&Value::Int(42)).is_err());
let schema = FieldSchema::int();
assert!(schema.validate(&Value::Int(42)).is_ok());
assert!(schema.validate(&Value::String("42".to_string())).is_err());
}
#[test]
fn test_nullable() {
let schema = FieldSchema::string();
assert!(schema.validate(&Value::Null).is_err());
let schema = FieldSchema::string().nullable();
assert!(schema.validate(&Value::Null).is_ok());
}
#[test]
fn test_range_validation() {
let schema = FieldSchema::int().min(0.0).max(100.0);
assert!(schema.validate(&Value::Int(50)).is_ok());
assert!(schema.validate(&Value::Int(-1)).is_err());
assert!(schema.validate(&Value::Int(101)).is_err());
}
#[test]
fn test_string_length() {
let schema = FieldSchema::string().min_length(3).max_length(10);
assert!(schema.validate(&Value::String("hello".to_string())).is_ok());
assert!(schema.validate(&Value::String("hi".to_string())).is_err());
assert!(schema
.validate(&Value::String("hello world!".to_string()))
.is_err());
}
#[test]
fn test_pattern_validation() {
let schema = FieldSchema::string().pattern(r"^\d{3}-\d{4}$");
assert!(schema
.validate(&Value::String("123-4567".to_string()))
.is_ok());
assert!(schema
.validate(&Value::String("invalid".to_string()))
.is_err());
}
#[test]
fn test_schema_validation() {
let schema = SchemaBuilder::new("User")
.required_field("name", FieldSchema::string().min_length(1))
.required_field("age", FieldSchema::int().min(0.0))
.field("email", FieldSchema::string().nullable())
.build();
let mut doc = Document::new();
doc.set("name", "Alice");
doc.set("age", 30i64);
let result = schema.validate(&doc);
assert!(result.is_valid);
let mut invalid_doc = Document::new();
invalid_doc.set("name", "Bob");
let result = schema.validate(&invalid_doc);
assert!(!result.is_valid);
assert!(result.errors.iter().any(|e| e.contains("age")));
}
#[test]
fn test_enum_validation() {
let schema = FieldSchema::string().enum_values(vec![
Value::String("active".to_string()),
Value::String("inactive".to_string()),
]);
assert!(schema
.validate(&Value::String("active".to_string()))
.is_ok());
assert!(schema
.validate(&Value::String("unknown".to_string()))
.is_err());
}
#[test]
fn test_array_validation() {
let schema = FieldSchema::array(FieldSchema::int())
.min_length(1)
.max_length(5);
assert!(schema
.validate(&Value::Array(vec![Value::Int(1), Value::Int(2)]))
.is_ok());
assert!(schema.validate(&Value::Array(vec![])).is_err());
assert!(schema
.validate(&Value::Array(vec![Value::String("not an int".to_string())]))
.is_err());
}
}