use std::collections::HashMap;
use crate::value::Value;
use super::{GraphSchema, PropertyDef, SchemaError, SchemaResult, ValidationMode};
#[derive(Debug, Clone)]
pub enum ValidationResult {
Ok,
Warning(SchemaError),
Error(SchemaError),
}
impl ValidationResult {
pub fn is_ok(&self) -> bool {
matches!(self, ValidationResult::Ok)
}
pub fn is_warning(&self) -> bool {
matches!(self, ValidationResult::Warning(_))
}
pub fn is_error(&self) -> bool {
matches!(self, ValidationResult::Error(_))
}
pub fn into_error(self) -> Option<SchemaError> {
match self {
ValidationResult::Ok => None,
ValidationResult::Warning(e) | ValidationResult::Error(e) => Some(e),
}
}
}
pub fn validate_vertex(
schema: &GraphSchema,
label: &str,
properties: &HashMap<String, Value>,
) -> SchemaResult<Vec<ValidationResult>> {
let mut results = Vec::new();
let vertex_schema = match schema.vertex_schemas.get(label) {
Some(vs) => vs,
None => {
match schema.mode {
ValidationMode::None => return Ok(results),
ValidationMode::Warn => {
results.push(ValidationResult::Warning(SchemaError::UnknownVertexLabel {
label: label.to_string(),
}));
return Ok(results);
}
ValidationMode::Strict => return Ok(results), ValidationMode::Closed => {
return Err(SchemaError::UnknownVertexLabel {
label: label.to_string(),
});
}
}
}
};
validate_properties(
&mut results,
schema.mode,
"vertex",
label,
vertex_schema.additional_properties,
&vertex_schema.properties,
properties,
)?;
Ok(results)
}
pub fn validate_edge(
schema: &GraphSchema,
label: &str,
from_label: &str,
to_label: &str,
properties: &HashMap<String, Value>,
) -> SchemaResult<Vec<ValidationResult>> {
let mut results = Vec::new();
let edge_schema = match schema.edge_schemas.get(label) {
Some(es) => es,
None => {
match schema.mode {
ValidationMode::None => return Ok(results),
ValidationMode::Warn => {
results.push(ValidationResult::Warning(SchemaError::UnknownEdgeLabel {
label: label.to_string(),
}));
return Ok(results);
}
ValidationMode::Strict => return Ok(results), ValidationMode::Closed => {
return Err(SchemaError::UnknownEdgeLabel {
label: label.to_string(),
});
}
}
}
};
if !edge_schema.allows_from(from_label) {
let err = SchemaError::InvalidSourceLabel {
edge_label: label.to_string(),
from_label: from_label.to_string(),
allowed: edge_schema.from_labels.clone(),
};
match schema.mode {
ValidationMode::None => {}
ValidationMode::Warn => results.push(ValidationResult::Warning(err)),
ValidationMode::Strict | ValidationMode::Closed => return Err(err),
}
}
if !edge_schema.allows_to(to_label) {
let err = SchemaError::InvalidTargetLabel {
edge_label: label.to_string(),
to_label: to_label.to_string(),
allowed: edge_schema.to_labels.clone(),
};
match schema.mode {
ValidationMode::None => {}
ValidationMode::Warn => results.push(ValidationResult::Warning(err)),
ValidationMode::Strict | ValidationMode::Closed => return Err(err),
}
}
validate_properties(
&mut results,
schema.mode,
"edge",
label,
edge_schema.additional_properties,
&edge_schema.properties,
properties,
)?;
Ok(results)
}
pub fn validate_property_update(
schema: &GraphSchema,
label: &str,
property_key: &str,
value: &Value,
is_vertex: bool,
) -> SchemaResult<Vec<ValidationResult>> {
let mut results = Vec::new();
let (schema_props, additional_allowed) = if is_vertex {
match schema.vertex_schemas.get(label) {
Some(vs) => (&vs.properties, vs.additional_properties),
None => {
match schema.mode {
ValidationMode::None | ValidationMode::Strict => return Ok(results),
ValidationMode::Warn => {
results.push(ValidationResult::Warning(SchemaError::UnknownVertexLabel {
label: label.to_string(),
}));
return Ok(results);
}
ValidationMode::Closed => {
return Err(SchemaError::UnknownVertexLabel {
label: label.to_string(),
});
}
}
}
}
} else {
match schema.edge_schemas.get(label) {
Some(es) => (&es.properties, es.additional_properties),
None => match schema.mode {
ValidationMode::None | ValidationMode::Strict => return Ok(results),
ValidationMode::Warn => {
results.push(ValidationResult::Warning(SchemaError::UnknownEdgeLabel {
label: label.to_string(),
}));
return Ok(results);
}
ValidationMode::Closed => {
return Err(SchemaError::UnknownEdgeLabel {
label: label.to_string(),
});
}
},
}
};
let element_type = if is_vertex { "vertex" } else { "edge" };
if let Some(prop_def) = schema_props.get(property_key) {
if prop_def.required && matches!(value, Value::Null) {
let err = SchemaError::NullRequired {
element_type,
label: label.to_string(),
property: property_key.to_string(),
};
match schema.mode {
ValidationMode::None => {}
ValidationMode::Warn => results.push(ValidationResult::Warning(err)),
ValidationMode::Strict | ValidationMode::Closed => return Err(err),
}
}
if !matches!(value, Value::Null) && !prop_def.value_type.matches(value) {
let err = SchemaError::TypeMismatch {
property: property_key.to_string(),
expected: prop_def.value_type.clone(),
actual: value_type_name(value).to_string(),
};
match schema.mode {
ValidationMode::None => {}
ValidationMode::Warn => results.push(ValidationResult::Warning(err)),
ValidationMode::Strict | ValidationMode::Closed => return Err(err),
}
}
} else if !additional_allowed {
let err = SchemaError::UnexpectedProperty {
element_type,
label: label.to_string(),
property: property_key.to_string(),
};
match schema.mode {
ValidationMode::None => {}
ValidationMode::Warn => results.push(ValidationResult::Warning(err)),
ValidationMode::Strict | ValidationMode::Closed => return Err(err),
}
}
Ok(results)
}
pub fn apply_defaults(
schema: &GraphSchema,
label: &str,
properties: &HashMap<String, Value>,
is_vertex: bool,
) -> HashMap<String, Value> {
let schema_props = if is_vertex {
schema.vertex_schemas.get(label).map(|vs| &vs.properties)
} else {
schema.edge_schemas.get(label).map(|es| &es.properties)
};
let mut result = properties.clone();
if let Some(props) = schema_props {
for (key, def) in props {
if !result.contains_key(key) {
if let Some(default) = &def.default {
result.insert(key.clone(), default.clone());
}
}
}
}
result
}
fn validate_properties(
results: &mut Vec<ValidationResult>,
mode: ValidationMode,
element_type: &'static str,
label: &str,
additional_properties: bool,
schema_props: &HashMap<String, PropertyDef>,
properties: &HashMap<String, Value>,
) -> SchemaResult<()> {
for (key, def) in schema_props {
if def.required {
match properties.get(key) {
None => {
let err = SchemaError::MissingRequired {
element_type,
label: label.to_string(),
property: key.clone(),
};
match mode {
ValidationMode::None => {}
ValidationMode::Warn => results.push(ValidationResult::Warning(err)),
ValidationMode::Strict | ValidationMode::Closed => return Err(err),
}
}
Some(Value::Null) => {
let err = SchemaError::NullRequired {
element_type,
label: label.to_string(),
property: key.clone(),
};
match mode {
ValidationMode::None => {}
ValidationMode::Warn => results.push(ValidationResult::Warning(err)),
ValidationMode::Strict | ValidationMode::Closed => return Err(err),
}
}
Some(_) => {} }
}
}
for (key, value) in properties {
if let Some(def) = schema_props.get(key) {
if !matches!(value, Value::Null) && !def.value_type.matches(value) {
let err = SchemaError::TypeMismatch {
property: key.clone(),
expected: def.value_type.clone(),
actual: value_type_name(value).to_string(),
};
match mode {
ValidationMode::None => {}
ValidationMode::Warn => results.push(ValidationResult::Warning(err)),
ValidationMode::Strict | ValidationMode::Closed => return Err(err),
}
}
} else if !additional_properties {
let err = SchemaError::UnexpectedProperty {
element_type,
label: label.to_string(),
property: key.clone(),
};
match mode {
ValidationMode::None => {}
ValidationMode::Warn => results.push(ValidationResult::Warning(err)),
ValidationMode::Strict | ValidationMode::Closed => return Err(err),
}
}
}
Ok(())
}
fn value_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "NULL",
Value::Bool(_) => "BOOL",
Value::Int(_) => "INT",
Value::Float(_) => "FLOAT",
Value::String(_) => "STRING",
Value::List(_) => "LIST",
Value::Map(_) => "MAP",
Value::Vertex(_) => "VERTEX",
Value::Edge(_) => "EDGE",
Value::Point(_) => "POINT",
Value::Polygon(_) => "POLYGON",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{PropertyType, SchemaBuilder};
fn make_test_schema(mode: ValidationMode) -> GraphSchema {
SchemaBuilder::new()
.mode(mode)
.vertex("Person")
.property("name", PropertyType::String)
.optional("age", PropertyType::Int)
.optional_with_default("active", PropertyType::Bool, Value::Bool(true))
.done()
.vertex("Flexible")
.property("type", PropertyType::String)
.allow_additional()
.done()
.edge("KNOWS")
.from(&["Person"])
.to(&["Person"])
.optional("since", PropertyType::Int)
.done()
.edge("WORKS_AT")
.from(&["Person"])
.to(&["Company"])
.property("role", PropertyType::String)
.done()
.build()
}
#[test]
fn validate_vertex_valid() {
let schema = make_test_schema(ValidationMode::Strict);
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String("Alice".to_string()));
props.insert("age".to_string(), Value::Int(30));
let results = validate_vertex(&schema, "Person", &props).unwrap();
assert!(results.iter().all(|r| r.is_ok()));
}
#[test]
fn validate_vertex_missing_required_strict() {
let schema = make_test_schema(ValidationMode::Strict);
let props = HashMap::new();
let result = validate_vertex(&schema, "Person", &props);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SchemaError::MissingRequired { .. }
));
}
#[test]
fn validate_vertex_missing_required_warn() {
let schema = make_test_schema(ValidationMode::Warn);
let props = HashMap::new();
let results = validate_vertex(&schema, "Person", &props).unwrap();
assert!(results.iter().any(|r| r.is_warning()));
}
#[test]
fn validate_vertex_null_required() {
let schema = make_test_schema(ValidationMode::Strict);
let mut props = HashMap::new();
props.insert("name".to_string(), Value::Null);
let result = validate_vertex(&schema, "Person", &props);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SchemaError::NullRequired { .. }
));
}
#[test]
fn validate_vertex_type_mismatch() {
let schema = make_test_schema(ValidationMode::Strict);
let mut props = HashMap::new();
props.insert("name".to_string(), Value::Int(42));
let result = validate_vertex(&schema, "Person", &props);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SchemaError::TypeMismatch { .. }
));
}
#[test]
fn validate_vertex_unexpected_property() {
let schema = make_test_schema(ValidationMode::Strict);
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String("Alice".to_string()));
props.insert("unknown".to_string(), Value::String("value".to_string()));
let result = validate_vertex(&schema, "Person", &props);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SchemaError::UnexpectedProperty { .. }
));
}
#[test]
fn validate_vertex_additional_properties_allowed() {
let schema = make_test_schema(ValidationMode::Strict);
let mut props = HashMap::new();
props.insert("type".to_string(), Value::String("test".to_string()));
props.insert("extra".to_string(), Value::Int(42));
let results = validate_vertex(&schema, "Flexible", &props).unwrap();
assert!(results.iter().all(|r| r.is_ok()));
}
#[test]
fn validate_vertex_unknown_label_strict() {
let schema = make_test_schema(ValidationMode::Strict);
let props = HashMap::new();
let results = validate_vertex(&schema, "Unknown", &props).unwrap();
assert!(results.is_empty());
}
#[test]
fn validate_vertex_unknown_label_closed() {
let schema = make_test_schema(ValidationMode::Closed);
let props = HashMap::new();
let result = validate_vertex(&schema, "Unknown", &props);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SchemaError::UnknownVertexLabel { .. }
));
}
#[test]
fn validate_edge_valid() {
let schema = make_test_schema(ValidationMode::Strict);
let mut props = HashMap::new();
props.insert("since".to_string(), Value::Int(2020));
let results = validate_edge(&schema, "KNOWS", "Person", "Person", &props).unwrap();
assert!(results.iter().all(|r| r.is_ok()));
}
#[test]
fn validate_edge_invalid_source() {
let schema = make_test_schema(ValidationMode::Strict);
let mut props = HashMap::new();
props.insert("role".to_string(), Value::String("Manager".to_string()));
let result = validate_edge(&schema, "WORKS_AT", "Company", "Company", &props);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SchemaError::InvalidSourceLabel { .. }
));
}
#[test]
fn validate_edge_invalid_target() {
let schema = make_test_schema(ValidationMode::Strict);
let mut props = HashMap::new();
props.insert("role".to_string(), Value::String("Manager".to_string()));
let result = validate_edge(&schema, "WORKS_AT", "Person", "Person", &props);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SchemaError::InvalidTargetLabel { .. }
));
}
#[test]
fn validate_edge_missing_required() {
let schema = make_test_schema(ValidationMode::Strict);
let props = HashMap::new();
let result = validate_edge(&schema, "WORKS_AT", "Person", "Company", &props);
assert!(result.is_err());
}
#[test]
fn validate_property_update_valid() {
let schema = make_test_schema(ValidationMode::Strict);
let results =
validate_property_update(&schema, "Person", "age", &Value::Int(31), true).unwrap();
assert!(results.iter().all(|r| r.is_ok()));
}
#[test]
fn validate_property_update_type_mismatch() {
let schema = make_test_schema(ValidationMode::Strict);
let result = validate_property_update(
&schema,
"Person",
"age",
&Value::String("thirty".to_string()),
true,
);
assert!(result.is_err());
}
#[test]
fn validate_property_update_null_required() {
let schema = make_test_schema(ValidationMode::Strict);
let result = validate_property_update(&schema, "Person", "name", &Value::Null, true);
assert!(result.is_err());
}
#[test]
fn apply_defaults_fills_missing() {
let schema = make_test_schema(ValidationMode::Strict);
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String("Alice".to_string()));
let with_defaults = apply_defaults(&schema, "Person", &props, true);
assert_eq!(
with_defaults.get("name"),
Some(&Value::String("Alice".to_string()))
);
assert_eq!(with_defaults.get("active"), Some(&Value::Bool(true)));
}
#[test]
fn apply_defaults_does_not_overwrite() {
let schema = make_test_schema(ValidationMode::Strict);
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String("Alice".to_string()));
props.insert("active".to_string(), Value::Bool(false));
let with_defaults = apply_defaults(&schema, "Person", &props, true);
assert_eq!(with_defaults.get("active"), Some(&Value::Bool(false)));
}
#[test]
fn apply_defaults_unknown_label() {
let schema = make_test_schema(ValidationMode::Strict);
let props = HashMap::new();
let with_defaults = apply_defaults(&schema, "Unknown", &props, true);
assert!(with_defaults.is_empty()); }
#[test]
fn validation_result_methods() {
let ok = ValidationResult::Ok;
assert!(ok.is_ok());
assert!(!ok.is_warning());
assert!(!ok.is_error());
assert!(ok.into_error().is_none());
let warning = ValidationResult::Warning(SchemaError::UnknownVertexLabel {
label: "Test".to_string(),
});
assert!(!warning.is_ok());
assert!(warning.is_warning());
assert!(!warning.is_error());
assert!(warning.into_error().is_some());
let error = ValidationResult::Error(SchemaError::UnknownEdgeLabel {
label: "Test".to_string(),
});
assert!(!error.is_ok());
assert!(!error.is_warning());
assert!(error.is_error());
assert!(error.into_error().is_some());
}
#[test]
fn validate_mode_none_allows_everything() {
let schema = make_test_schema(ValidationMode::None);
let props = HashMap::new();
let results = validate_vertex(&schema, "Person", &props).unwrap();
assert!(results.iter().all(|r| r.is_ok()));
let results = validate_vertex(&schema, "Unknown", &props).unwrap();
assert!(results.is_empty());
let mut props = HashMap::new();
props.insert("name".to_string(), Value::Int(42));
let results = validate_vertex(&schema, "Person", &props).unwrap();
assert!(results.iter().all(|r| r.is_ok()));
}
#[test]
fn validate_edge_for_edge_property() {
let schema = make_test_schema(ValidationMode::Strict);
let results =
validate_property_update(&schema, "KNOWS", "since", &Value::Int(2020), false).unwrap();
assert!(results.iter().all(|r| r.is_ok()));
}
}