use std::collections::HashMap;
use crate::value::Value;
#[derive(Clone, Debug)]
pub struct VertexSchema {
pub label: String,
pub properties: HashMap<String, PropertyDef>,
pub additional_properties: bool,
}
impl VertexSchema {
pub fn required_properties(&self) -> impl Iterator<Item = &str> {
self.properties
.iter()
.filter(|(_, def)| def.required)
.map(|(key, _)| key.as_str())
}
pub fn optional_properties(&self) -> impl Iterator<Item = &str> {
self.properties
.iter()
.filter(|(_, def)| !def.required)
.map(|(key, _)| key.as_str())
}
pub fn property_type(&self, key: &str) -> Option<&PropertyType> {
self.properties.get(key).map(|def| &def.value_type)
}
pub fn property_default(&self, key: &str) -> Option<&Value> {
self.properties
.get(key)
.and_then(|def| def.default.as_ref())
}
}
#[derive(Clone, Debug)]
pub struct EdgeSchema {
pub label: String,
pub from_labels: Vec<String>,
pub to_labels: Vec<String>,
pub properties: HashMap<String, PropertyDef>,
pub additional_properties: bool,
}
impl EdgeSchema {
pub fn allows_from(&self, label: &str) -> bool {
self.from_labels.iter().any(|l| l == label)
}
pub fn allows_to(&self, label: &str) -> bool {
self.to_labels.iter().any(|l| l == label)
}
pub fn required_properties(&self) -> impl Iterator<Item = &str> {
self.properties
.iter()
.filter(|(_, def)| def.required)
.map(|(key, _)| key.as_str())
}
pub fn property_type(&self, key: &str) -> Option<&PropertyType> {
self.properties.get(key).map(|def| &def.value_type)
}
pub fn property_default(&self, key: &str) -> Option<&Value> {
self.properties
.get(key)
.and_then(|def| def.default.as_ref())
}
}
#[derive(Clone, Debug)]
pub struct PropertyDef {
pub key: String,
pub value_type: PropertyType,
pub required: bool,
pub default: Option<Value>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PropertyType {
Any,
Bool,
Int,
Float,
String,
List(Option<Box<PropertyType>>),
Map(Option<Box<PropertyType>>),
}
impl PropertyType {
pub fn matches(&self, value: &Value) -> bool {
match (self, value) {
(PropertyType::Any, Value::Null) => false,
(PropertyType::Any, _) => true,
(PropertyType::Bool, Value::Bool(_)) => true,
(PropertyType::Int, Value::Int(_)) => true,
(PropertyType::Float, Value::Float(_)) => true,
(PropertyType::String, Value::String(_)) => true,
(PropertyType::List(None), Value::List(_)) => true,
(PropertyType::List(Some(elem_type)), Value::List(items)) => {
items.iter().all(|item| elem_type.matches(item))
}
(PropertyType::Map(None), Value::Map(_)) => true,
(PropertyType::Map(Some(val_type)), Value::Map(map)) => {
map.values().all(|v| val_type.matches(v))
}
(_, Value::Null) => false,
_ => false,
}
}
pub fn type_name(&self) -> &'static str {
match self {
PropertyType::Any => "ANY",
PropertyType::Bool => "BOOL",
PropertyType::Int => "INT",
PropertyType::Float => "FLOAT",
PropertyType::String => "STRING",
PropertyType::List(_) => "LIST",
PropertyType::Map(_) => "MAP",
}
}
}
impl std::fmt::Display for PropertyType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PropertyType::Any => write!(f, "ANY"),
PropertyType::Bool => write!(f, "BOOL"),
PropertyType::Int => write!(f, "INT"),
PropertyType::Float => write!(f, "FLOAT"),
PropertyType::String => write!(f, "STRING"),
PropertyType::List(None) => write!(f, "LIST"),
PropertyType::List(Some(t)) => write!(f, "LIST<{}>", t),
PropertyType::Map(None) => write!(f, "MAP"),
PropertyType::Map(Some(t)) => write!(f, "MAP<{}>", t),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn property_type_matches_bool() {
assert!(PropertyType::Bool.matches(&Value::Bool(true)));
assert!(PropertyType::Bool.matches(&Value::Bool(false)));
assert!(!PropertyType::Bool.matches(&Value::Int(1)));
assert!(!PropertyType::Bool.matches(&Value::String("true".to_string())));
}
#[test]
fn property_type_matches_int() {
assert!(PropertyType::Int.matches(&Value::Int(42)));
assert!(PropertyType::Int.matches(&Value::Int(-1)));
assert!(!PropertyType::Int.matches(&Value::Float(42.0)));
assert!(!PropertyType::Int.matches(&Value::Bool(true)));
}
#[test]
fn property_type_matches_float() {
assert!(PropertyType::Float.matches(&Value::Float(3.14)));
assert!(PropertyType::Float.matches(&Value::Float(-0.5)));
assert!(!PropertyType::Float.matches(&Value::Int(42)));
assert!(!PropertyType::Float.matches(&Value::String("3.14".to_string())));
}
#[test]
fn property_type_matches_string() {
assert!(PropertyType::String.matches(&Value::String("hello".to_string())));
assert!(PropertyType::String.matches(&Value::String("".to_string())));
assert!(!PropertyType::String.matches(&Value::Int(42)));
assert!(!PropertyType::String.matches(&Value::Bool(true)));
}
#[test]
fn property_type_matches_list_untyped() {
let pt = PropertyType::List(None);
assert!(pt.matches(&Value::List(vec![
Value::Int(1),
Value::String("a".to_string())
])));
assert!(pt.matches(&Value::List(vec![])));
assert!(!pt.matches(&Value::Int(1)));
}
#[test]
fn property_type_matches_list_with_element_type() {
let list_of_ints = PropertyType::List(Some(Box::new(PropertyType::Int)));
assert!(list_of_ints.matches(&Value::List(vec![Value::Int(1), Value::Int(2)])));
assert!(list_of_ints.matches(&Value::List(vec![]))); assert!(!list_of_ints.matches(&Value::List(vec![Value::String("a".to_string())])));
assert!(!list_of_ints.matches(&Value::List(vec![
Value::Int(1),
Value::String("a".to_string())
])));
}
#[test]
fn property_type_matches_map_untyped() {
let pt = PropertyType::Map(None);
let mut map = crate::value::ValueMap::new();
map.insert("a".to_string(), Value::Int(1));
map.insert("b".to_string(), Value::String("x".to_string()));
assert!(pt.matches(&Value::Map(map)));
assert!(pt.matches(&Value::Map(crate::value::ValueMap::new())));
assert!(!pt.matches(&Value::List(vec![])));
}
#[test]
fn property_type_matches_map_with_value_type() {
let map_of_ints = PropertyType::Map(Some(Box::new(PropertyType::Int)));
let mut valid_map = crate::value::ValueMap::new();
valid_map.insert("a".to_string(), Value::Int(1));
valid_map.insert("b".to_string(), Value::Int(2));
assert!(map_of_ints.matches(&Value::Map(valid_map)));
let mut invalid_map = crate::value::ValueMap::new();
invalid_map.insert("a".to_string(), Value::String("x".to_string()));
assert!(!map_of_ints.matches(&Value::Map(invalid_map)));
}
#[test]
fn property_type_any_matches_everything_except_null() {
assert!(PropertyType::Any.matches(&Value::Bool(true)));
assert!(PropertyType::Any.matches(&Value::Int(42)));
assert!(PropertyType::Any.matches(&Value::Float(3.14)));
assert!(PropertyType::Any.matches(&Value::String("hello".to_string())));
assert!(PropertyType::Any.matches(&Value::List(vec![])));
assert!(PropertyType::Any.matches(&Value::Map(crate::value::ValueMap::new())));
assert!(!PropertyType::Any.matches(&Value::Null)); }
#[test]
fn property_type_null_always_fails_match() {
assert!(!PropertyType::Bool.matches(&Value::Null));
assert!(!PropertyType::Int.matches(&Value::Null));
assert!(!PropertyType::Float.matches(&Value::Null));
assert!(!PropertyType::String.matches(&Value::Null));
assert!(!PropertyType::List(None).matches(&Value::Null));
assert!(!PropertyType::Map(None).matches(&Value::Null));
assert!(!PropertyType::Any.matches(&Value::Null));
}
#[test]
fn property_type_display() {
assert_eq!(format!("{}", PropertyType::String), "STRING");
assert_eq!(format!("{}", PropertyType::Int), "INT");
assert_eq!(format!("{}", PropertyType::Float), "FLOAT");
assert_eq!(format!("{}", PropertyType::Bool), "BOOL");
assert_eq!(format!("{}", PropertyType::Any), "ANY");
assert_eq!(format!("{}", PropertyType::List(None)), "LIST");
assert_eq!(format!("{}", PropertyType::Map(None)), "MAP");
assert_eq!(
format!("{}", PropertyType::List(Some(Box::new(PropertyType::Int)))),
"LIST<INT>"
);
assert_eq!(
format!(
"{}",
PropertyType::Map(Some(Box::new(PropertyType::String)))
),
"MAP<STRING>"
);
}
#[test]
fn property_type_type_name() {
assert_eq!(PropertyType::Any.type_name(), "ANY");
assert_eq!(PropertyType::Bool.type_name(), "BOOL");
assert_eq!(PropertyType::Int.type_name(), "INT");
assert_eq!(PropertyType::Float.type_name(), "FLOAT");
assert_eq!(PropertyType::String.type_name(), "STRING");
assert_eq!(PropertyType::List(None).type_name(), "LIST");
assert_eq!(PropertyType::Map(None).type_name(), "MAP");
}
#[test]
fn vertex_schema_required_properties() {
let mut properties = HashMap::new();
properties.insert(
"name".to_string(),
PropertyDef {
key: "name".to_string(),
value_type: PropertyType::String,
required: true,
default: None,
},
);
properties.insert(
"age".to_string(),
PropertyDef {
key: "age".to_string(),
value_type: PropertyType::Int,
required: false,
default: None,
},
);
properties.insert(
"email".to_string(),
PropertyDef {
key: "email".to_string(),
value_type: PropertyType::String,
required: true,
default: None,
},
);
let schema = VertexSchema {
label: "Person".to_string(),
properties,
additional_properties: false,
};
let required: Vec<_> = schema.required_properties().collect();
assert_eq!(required.len(), 2);
assert!(required.contains(&"name"));
assert!(required.contains(&"email"));
let optional: Vec<_> = schema.optional_properties().collect();
assert_eq!(optional.len(), 1);
assert!(optional.contains(&"age"));
}
#[test]
fn vertex_schema_property_type_and_default() {
let mut properties = HashMap::new();
properties.insert(
"active".to_string(),
PropertyDef {
key: "active".to_string(),
value_type: PropertyType::Bool,
required: false,
default: Some(Value::Bool(true)),
},
);
let schema = VertexSchema {
label: "Person".to_string(),
properties,
additional_properties: false,
};
assert_eq!(schema.property_type("active"), Some(&PropertyType::Bool));
assert_eq!(schema.property_default("active"), Some(&Value::Bool(true)));
assert_eq!(schema.property_type("nonexistent"), None);
assert_eq!(schema.property_default("nonexistent"), None);
}
#[test]
fn edge_schema_allows_from_to() {
let schema = EdgeSchema {
label: "WORKS_AT".to_string(),
from_labels: vec!["Person".to_string(), "Employee".to_string()],
to_labels: vec!["Company".to_string()],
properties: HashMap::new(),
additional_properties: false,
};
assert!(schema.allows_from("Person"));
assert!(schema.allows_from("Employee"));
assert!(!schema.allows_from("Company"));
assert!(!schema.allows_from("Unknown"));
assert!(schema.allows_to("Company"));
assert!(!schema.allows_to("Person"));
assert!(!schema.allows_to("Unknown"));
}
#[test]
fn edge_schema_properties() {
let mut properties = HashMap::new();
properties.insert(
"since".to_string(),
PropertyDef {
key: "since".to_string(),
value_type: PropertyType::Int,
required: true,
default: None,
},
);
properties.insert(
"weight".to_string(),
PropertyDef {
key: "weight".to_string(),
value_type: PropertyType::Float,
required: false,
default: Some(Value::Float(1.0)),
},
);
let schema = EdgeSchema {
label: "KNOWS".to_string(),
from_labels: vec!["Person".to_string()],
to_labels: vec!["Person".to_string()],
properties,
additional_properties: false,
};
let required: Vec<_> = schema.required_properties().collect();
assert_eq!(required.len(), 1);
assert!(required.contains(&"since"));
assert_eq!(schema.property_type("weight"), Some(&PropertyType::Float));
assert_eq!(schema.property_default("weight"), Some(&Value::Float(1.0)));
}
}