use parking_lot::RwLock;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use crate::catalog::manager::CatalogManager;
use crate::catalog::operations::{CatalogResponse, QueryType};
use crate::schema::types::{Constraint, DataType, GraphTypeDefinition, NodeTypeDefinition};
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
#[error("Unknown label: {0}")]
UnknownLabel(String),
#[error("Unknown property '{property}' for label '{label}'")]
#[allow(dead_code)] UnknownProperty { label: String, property: String },
#[error("Missing required property '{property}' for label '{label}'")]
MissingRequiredProperty { label: String, property: String },
#[error("Invalid property type for '{property}': expected {expected}, got {got}")]
InvalidPropertyType {
property: String,
expected: String,
got: String,
},
#[error("Constraint violation: {constraint} - {message}")]
ConstraintViolation { constraint: String, message: String },
#[error("No graph type associated with graph '{0}'")]
NoGraphType(String),
#[error("Catalog error: {0}")]
CatalogError(String),
#[error("Invalid value: {0}")]
InvalidValue(String),
}
#[allow(dead_code)] pub struct SchemaValidator {
#[allow(dead_code)] catalog_manager: Arc<RwLock<CatalogManager>>,
#[allow(dead_code)] allow_unknown_properties: bool,
}
impl SchemaValidator {
pub fn new(catalog_manager: Arc<RwLock<CatalogManager>>) -> Self {
Self {
catalog_manager,
allow_unknown_properties: false, }
}
#[allow(dead_code)] pub fn with_config(
catalog_manager: Arc<RwLock<CatalogManager>>,
allow_unknown_properties: bool,
) -> Self {
Self {
catalog_manager,
allow_unknown_properties,
}
}
#[allow(dead_code)] pub fn validate_node(
&self,
graph_name: &str,
label: &str,
properties: &HashMap<String, Value>,
) -> Result<(), ValidationError> {
let graph_type = self.get_graph_type_for_graph(graph_name)?;
self.validate_node_with_type(&graph_type, label, properties)
}
pub fn validate_node_with_type(
&self,
graph_type: &GraphTypeDefinition,
label: &str,
properties: &HashMap<String, Value>,
) -> Result<(), ValidationError> {
let node_type = graph_type
.node_types
.iter()
.find(|nt| nt.label == label)
.ok_or_else(|| ValidationError::UnknownLabel(label.to_string()))?;
for prop_def in &node_type.properties {
if prop_def.required && !properties.contains_key(&prop_def.name) {
return Err(ValidationError::MissingRequiredProperty {
label: label.to_string(),
property: prop_def.name.clone(),
});
}
}
for (prop_name, prop_value) in properties {
if let Some(prop_def) = node_type.properties.iter().find(|p| p.name == *prop_name) {
self.validate_property_type(prop_value, &prop_def.data_type)?;
}
}
self.validate_node_constraints(node_type)?;
Ok(())
}
#[allow(dead_code)] pub fn validate_partial_node(
&self,
graph_type: &GraphTypeDefinition,
label: &str,
properties: &HashMap<String, Value>,
) -> Result<(), ValidationError> {
let node_type = graph_type
.node_types
.iter()
.find(|nt| nt.label == label)
.ok_or_else(|| ValidationError::UnknownLabel(label.to_string()))?;
for (prop_name, prop_value) in properties {
if let Some(prop_def) = node_type.properties.iter().find(|p| p.name == *prop_name) {
self.validate_property_type(prop_value, &prop_def.data_type)?;
for constraint in &prop_def.constraints {
self.validate_property_constraint(prop_value, constraint)?;
}
} else if !self.allow_unknown_properties {
return Err(ValidationError::UnknownProperty {
label: label.to_string(),
property: prop_name.clone(),
});
}
}
Ok(())
}
#[allow(dead_code)] pub fn validate_edge(
&self,
graph_type: &GraphTypeDefinition,
edge_type: &str,
from_label: &str,
to_label: &str,
properties: &HashMap<String, Value>,
) -> Result<(), ValidationError> {
let edge_def = graph_type
.edge_types
.iter()
.find(|et| et.type_name == edge_type)
.ok_or_else(|| ValidationError::UnknownLabel(edge_type.to_string()))?;
if !edge_def.from_node_types.contains(&from_label.to_string()) {
return Err(ValidationError::InvalidValue(format!(
"Edge type '{}' cannot originate from node type '{}'",
edge_type, from_label
)));
}
if !edge_def.to_node_types.contains(&to_label.to_string()) {
return Err(ValidationError::InvalidValue(format!(
"Edge type '{}' cannot terminate at node type '{}'",
edge_type, to_label
)));
}
for prop_def in &edge_def.properties {
if prop_def.required && !properties.contains_key(&prop_def.name) {
return Err(ValidationError::MissingRequiredProperty {
label: edge_type.to_string(),
property: prop_def.name.clone(),
});
}
}
for (prop_name, prop_value) in properties {
if let Some(prop_def) = edge_def.properties.iter().find(|p| p.name == *prop_name) {
self.validate_property_type(prop_value, &prop_def.data_type)?;
}
}
Ok(())
}
#[allow(dead_code)] pub fn validate_index_schema(
&self,
graph_name: &str,
label: &str,
properties: &[String],
) -> Result<(), ValidationError> {
let graph_type = self.get_graph_type_for_graph(graph_name)?;
let node_type = graph_type
.node_types
.iter()
.find(|nt| nt.label == label)
.ok_or_else(|| ValidationError::UnknownLabel(label.to_string()))?;
for property in properties {
if !node_type.properties.iter().any(|p| p.name == *property) {
return Err(ValidationError::UnknownProperty {
label: label.to_string(),
property: property.clone(),
});
}
}
Ok(())
}
#[allow(dead_code)] fn get_graph_type_for_graph(
&self,
graph_name: &str,
) -> Result<GraphTypeDefinition, ValidationError> {
let catalog_manager = self.catalog_manager.read();
let graph_metadata = catalog_manager
.query_read_only(
"graph",
QueryType::Get,
serde_json::json!({ "name": graph_name }),
)
.map_err(|e| ValidationError::CatalogError(e.to_string()))?;
let graph_type_name = match graph_metadata {
CatalogResponse::Success { data: Some(data) } => data
.get("graph_type")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| ValidationError::NoGraphType(graph_name.to_string()))?,
CatalogResponse::Query { results } => results
.get("graph_type")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| ValidationError::NoGraphType(graph_name.to_string()))?,
_ => {
return Err(ValidationError::NoGraphType(graph_name.to_string()));
}
};
let graph_type_response = catalog_manager
.query_read_only(
"graph_type",
QueryType::GetGraphType,
serde_json::json!({ "name": graph_type_name }),
)
.map_err(|e| ValidationError::CatalogError(e.to_string()))?;
match graph_type_response {
CatalogResponse::Success { data: Some(data) } => serde_json::from_value(data)
.map_err(|e| ValidationError::CatalogError(e.to_string())),
CatalogResponse::Query { results } => serde_json::from_value(results)
.map_err(|e| ValidationError::CatalogError(e.to_string())),
_ => Err(ValidationError::NoGraphType(graph_name.to_string())),
}
}
fn validate_property_type(
&self,
value: &Value,
expected_type: &DataType,
) -> Result<(), ValidationError> {
let valid = match (expected_type, value) {
(DataType::String | DataType::Text, Value::String(_)) => true,
(DataType::Integer, Value::Number(n)) => n.is_i64(),
(DataType::BigInt, Value::Number(n)) => n.is_i64() || n.is_u64(),
(DataType::Float | DataType::Double, Value::Number(n)) => n.is_f64() || n.is_i64(),
(DataType::Boolean, Value::Bool(_)) => true,
(DataType::Json, _) => true, (DataType::Array(_), Value::Array(_)) => true, (DataType::UUID, Value::String(s)) => {
s.len() == 36 && s.chars().filter(|c| *c == '-').count() == 4
}
_ => false,
};
if !valid {
return Err(ValidationError::InvalidPropertyType {
property: "".to_string(), expected: format!("{:?}", expected_type),
got: format!("{}", value),
});
}
Ok(())
}
#[allow(dead_code)] fn validate_property_constraint(
&self,
value: &Value,
constraint: &Constraint,
) -> Result<(), ValidationError> {
match constraint {
Constraint::MinLength(min) => {
if let Value::String(s) = value {
if s.len() < *min {
return Err(ValidationError::ConstraintViolation {
constraint: "MinLength".to_string(),
message: format!(
"String length {} is less than minimum {}",
s.len(),
min
),
});
}
}
}
Constraint::MaxLength(max) => {
if let Value::String(s) = value {
if s.len() > *max {
return Err(ValidationError::ConstraintViolation {
constraint: "MaxLength".to_string(),
message: format!("String length {} exceeds maximum {}", s.len(), max),
});
}
}
}
Constraint::Pattern(pattern) => {
if let Value::String(s) = value {
let regex = regex::Regex::new(pattern)
.map_err(|e| ValidationError::InvalidValue(e.to_string()))?;
if !regex.is_match(s) {
return Err(ValidationError::ConstraintViolation {
constraint: "Pattern".to_string(),
message: format!("Value '{}' does not match pattern '{}'", s, pattern),
});
}
}
}
Constraint::MinValue(min) => {
if let Value::Number(n) = value {
if let Some(num_val) = n.as_f64() {
if num_val < *min {
return Err(ValidationError::ConstraintViolation {
constraint: "MinValue".to_string(),
message: format!("Value {} is less than minimum {}", num_val, min),
});
}
}
}
}
Constraint::MaxValue(max) => {
if let Value::Number(n) = value {
if let Some(num_val) = n.as_f64() {
if num_val > *max {
return Err(ValidationError::ConstraintViolation {
constraint: "MaxValue".to_string(),
message: format!("Value {} exceeds maximum {}", num_val, max),
});
}
}
}
}
Constraint::Unique => {
}
_ => {
}
}
Ok(())
}
fn validate_node_constraints(
&self,
node_type: &NodeTypeDefinition,
) -> Result<(), ValidationError> {
for constraint in &node_type.constraints {
match constraint {
Constraint::NotNull => {
}
Constraint::Unique => {
}
Constraint::MinLength(_) => {
}
Constraint::MaxLength(_) => {
}
Constraint::Pattern(_) => {
}
_ => {
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_validate_property_type() {
}
#[test]
fn test_validate_node_constraints() {
}
}