use crate::types::{PropKeyId, PropValue, PropValueTag};
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchemaType {
String,
Int,
Float,
Bool,
Vector,
}
impl SchemaType {
pub fn to_tag(&self) -> PropValueTag {
match self {
SchemaType::String => PropValueTag::String,
SchemaType::Int => PropValueTag::I64,
SchemaType::Float => PropValueTag::F64,
SchemaType::Bool => PropValueTag::Bool,
SchemaType::Vector => PropValueTag::VectorF32,
}
}
}
impl fmt::Display for SchemaType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SchemaType::String => write!(f, "string"),
SchemaType::Int => write!(f, "int"),
SchemaType::Float => write!(f, "float"),
SchemaType::Bool => write!(f, "bool"),
SchemaType::Vector => write!(f, "vector"),
}
}
}
#[derive(Debug, Clone)]
pub struct PropDef {
pub name: String,
pub schema_type: SchemaType,
pub optional: bool,
pub default: Option<PropValue>,
pub(crate) key_id: Option<PropKeyId>,
}
impl PropDef {
fn new(name: &str, schema_type: SchemaType) -> Self {
Self {
name: name.to_string(),
schema_type,
optional: false,
default: None,
key_id: None,
}
}
pub fn optional(mut self) -> Self {
self.optional = true;
self
}
pub fn default(mut self, value: PropValue) -> Self {
self.default = Some(value);
self
}
pub fn validate(&self, value: &PropValue) -> bool {
match (self.schema_type, value) {
(SchemaType::String, PropValue::String(_)) => true,
(SchemaType::Int, PropValue::I64(_)) => true,
(SchemaType::Float, PropValue::F64(_)) => true,
(SchemaType::Bool, PropValue::Bool(_)) => true,
(SchemaType::Vector, PropValue::VectorF32(_)) => true,
(_, PropValue::Null) => self.optional,
_ => false,
}
}
}
pub mod prop {
use super::*;
pub fn string(name: &str) -> PropDef {
PropDef::new(name, SchemaType::String)
}
pub fn int(name: &str) -> PropDef {
PropDef::new(name, SchemaType::Int)
}
pub fn float(name: &str) -> PropDef {
PropDef::new(name, SchemaType::Float)
}
pub fn bool(name: &str) -> PropDef {
PropDef::new(name, SchemaType::Bool)
}
pub fn vector(name: &str) -> PropDef {
PropDef::new(name, SchemaType::Vector)
}
}
pub type KeyFn = Arc<dyn Fn(&str) -> String + Send + Sync>;
pub fn key_fn<F>(f: F) -> KeyFn
where
F: Fn(&str) -> String + Send + Sync + 'static,
{
Arc::new(f)
}
#[derive(Clone)]
pub struct NodeSchema {
pub name: String,
pub key_fn: KeyFn,
pub props: HashMap<String, PropDef>,
pub key_prefix: String,
pub(crate) label_id: Option<u32>,
pub(crate) prop_key_ids: HashMap<String, PropKeyId>,
}
impl fmt::Debug for NodeSchema {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NodeSchema")
.field("name", &self.name)
.field("key_prefix", &self.key_prefix)
.field("props", &self.props)
.finish()
}
}
impl NodeSchema {
pub fn key(&self, id: &str) -> String {
(self.key_fn)(id)
}
pub fn get_prop(&self, name: &str) -> Option<&PropDef> {
self.props.get(name)
}
pub fn required_props(&self) -> Vec<&str> {
self
.props
.iter()
.filter(|(_, def)| !def.optional)
.map(|(name, _)| name.as_str())
.collect()
}
pub fn validate(&self, props: &HashMap<String, PropValue>) -> Result<(), ValidationError> {
for (name, def) in &self.props {
if !def.optional && !props.contains_key(name) && def.default.is_none() {
return Err(ValidationError::MissingRequired(name.clone()));
}
}
for (name, value) in props {
if let Some(def) = self.props.get(name) {
if !def.validate(value) {
return Err(ValidationError::TypeMismatch {
prop: name.clone(),
expected: def.schema_type,
got: value.tag(),
});
}
}
}
Ok(())
}
}
pub struct NodeSchemaBuilder {
name: String,
key_fn: Option<KeyFn>,
key_prefix: String,
props: HashMap<String, PropDef>,
}
impl NodeSchemaBuilder {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
key_fn: None,
key_prefix: format!("{name}:"),
props: HashMap::new(),
}
}
pub fn key<F>(mut self, f: F) -> Self
where
F: Fn(&str) -> String + Send + Sync + 'static,
{
let test_key = f("__test__");
if let Some(pos) = test_key.find("__test__") {
self.key_prefix = test_key[..pos].to_string();
}
self.key_fn = Some(Arc::new(f));
self
}
pub fn key_prefix(mut self, prefix: &str) -> Self {
self.key_prefix = prefix.to_string();
self
}
pub fn prop(mut self, prop_def: PropDef) -> Self {
self.props.insert(prop_def.name.clone(), prop_def);
self
}
pub fn build(self) -> NodeSchema {
let name = self.name.clone();
let key_fn = self.key_fn.unwrap_or_else(|| {
let name_clone = name.clone();
Arc::new(move |id: &str| format!("{name_clone}:{id}"))
});
NodeSchema {
name: self.name,
key_fn,
key_prefix: self.key_prefix,
props: self.props,
label_id: None,
prop_key_ids: HashMap::new(),
}
}
}
pub fn node(name: &str) -> NodeSchemaBuilder {
NodeSchemaBuilder::new(name)
}
#[deprecated(since = "0.2.0", note = "Use `node()` instead")]
pub fn define_node(name: &str) -> NodeSchemaBuilder {
node(name)
}
#[derive(Debug, Clone)]
pub struct EdgeSchema {
pub name: String,
pub props: HashMap<String, PropDef>,
pub(crate) etype_id: Option<u32>,
pub(crate) prop_key_ids: HashMap<String, PropKeyId>,
}
impl EdgeSchema {
pub fn get_prop(&self, name: &str) -> Option<&PropDef> {
self.props.get(name)
}
pub fn has_props(&self) -> bool {
!self.props.is_empty()
}
pub fn validate(&self, props: &HashMap<String, PropValue>) -> Result<(), ValidationError> {
for (name, def) in &self.props {
if !def.optional && !props.contains_key(name) && def.default.is_none() {
return Err(ValidationError::MissingRequired(name.clone()));
}
}
for (name, value) in props {
if let Some(def) = self.props.get(name) {
if !def.validate(value) {
return Err(ValidationError::TypeMismatch {
prop: name.clone(),
expected: def.schema_type,
got: value.tag(),
});
}
}
}
Ok(())
}
}
pub struct EdgeSchemaBuilder {
name: String,
props: HashMap<String, PropDef>,
}
impl EdgeSchemaBuilder {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
props: HashMap::new(),
}
}
pub fn prop(mut self, prop_def: PropDef) -> Self {
self.props.insert(prop_def.name.clone(), prop_def);
self
}
pub fn build(self) -> EdgeSchema {
EdgeSchema {
name: self.name,
props: self.props,
etype_id: None,
prop_key_ids: HashMap::new(),
}
}
}
pub fn edge(name: &str) -> EdgeSchemaBuilder {
EdgeSchemaBuilder::new(name)
}
#[deprecated(since = "0.2.0", note = "Use `edge()` instead")]
pub fn define_edge(name: &str) -> EdgeSchemaBuilder {
edge(name)
}
#[derive(Debug, Clone)]
pub enum ValidationError {
MissingRequired(String),
TypeMismatch {
prop: String,
expected: SchemaType,
got: PropValueTag,
},
UnknownNodeType(String),
UnknownEdgeType(String),
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationError::MissingRequired(prop) => {
write!(f, "Missing required property: {prop}")
}
ValidationError::TypeMismatch {
prop,
expected,
got,
} => {
write!(
f,
"Type mismatch for '{prop}': expected {expected}, got {got:?}"
)
}
ValidationError::UnknownNodeType(name) => {
write!(f, "Unknown node type: {name}")
}
ValidationError::UnknownEdgeType(name) => {
write!(f, "Unknown edge type: {name}")
}
}
}
}
impl std::error::Error for ValidationError {}
#[derive(Debug, Clone, Default)]
pub struct DatabaseSchema {
pub nodes: HashMap<String, NodeSchema>,
pub edges: HashMap<String, EdgeSchema>,
key_prefix_to_node: HashMap<String, String>,
}
impl DatabaseSchema {
pub fn new() -> Self {
Self::default()
}
pub fn node(mut self, schema: NodeSchema) -> Self {
self
.key_prefix_to_node
.insert(schema.key_prefix.clone(), schema.name.clone());
self.nodes.insert(schema.name.clone(), schema);
self
}
pub fn edge(mut self, schema: EdgeSchema) -> Self {
self.edges.insert(schema.name.clone(), schema);
self
}
pub fn get_node(&self, name: &str) -> Option<&NodeSchema> {
self.nodes.get(name)
}
pub fn get_edge(&self, name: &str) -> Option<&EdgeSchema> {
self.edges.get(name)
}
pub fn node_type_from_key(&self, key: &str) -> Option<&str> {
for (prefix, node_type) in &self.key_prefix_to_node {
if key.starts_with(prefix) {
return Some(node_type);
}
}
None
}
pub fn node_types(&self) -> Vec<&str> {
self.nodes.keys().map(|s| s.as_str()).collect()
}
pub fn edge_types(&self) -> Vec<&str> {
self.edges.keys().map(|s| s.as_str()).collect()
}
}
#[macro_export]
macro_rules! schema {
(
nodes: [ $($node:expr),* $(,)? ],
edges: [ $($edge:expr),* $(,)? ]
) => {{
let mut schema = $crate::api::schema::DatabaseSchema::new();
$(
schema = schema.node($node);
)*
$(
schema = schema.edge($edge);
)*
schema
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prop_builders() {
let name = prop::string("name");
assert_eq!(name.name, "name");
assert_eq!(name.schema_type, SchemaType::String);
assert!(!name.optional);
let age = prop::int("age").optional();
assert_eq!(age.name, "age");
assert_eq!(age.schema_type, SchemaType::Int);
assert!(age.optional);
let score = prop::float("score").default(PropValue::F64(0.0));
assert_eq!(score.name, "score");
assert!(score.default.is_some());
}
#[test]
fn test_prop_validation() {
let name_prop = prop::string("name");
assert!(name_prop.validate(&PropValue::String("Alice".to_string())));
assert!(!name_prop.validate(&PropValue::I64(42)));
assert!(!name_prop.validate(&PropValue::Null));
let age_prop = prop::int("age").optional();
assert!(age_prop.validate(&PropValue::I64(30)));
assert!(age_prop.validate(&PropValue::Null)); assert!(!age_prop.validate(&PropValue::String("thirty".to_string())));
}
#[test]
fn test_node() {
let user = node("user")
.key(|id| format!("user:{}", id))
.prop(prop::string("name"))
.prop(prop::int("age").optional())
.build();
assert_eq!(user.name, "user");
assert_eq!(user.key_prefix, "user:");
assert_eq!(user.key("alice"), "user:alice");
assert_eq!(user.props.len(), 2);
assert!(user.get_prop("name").is_some());
assert!(user.get_prop("age").is_some());
assert!(!user.get_prop("name").unwrap().optional);
assert!(user.get_prop("age").unwrap().optional);
}
#[test]
fn test_node_default_key() {
let post = node("post").prop(prop::string("title")).build();
assert_eq!(post.name, "post");
assert_eq!(post.key("123"), "post:123");
}
#[test]
fn test_edge() {
let knows = edge("knows")
.prop(prop::int("since"))
.prop(prop::float("weight").optional())
.build();
assert_eq!(knows.name, "knows");
assert_eq!(knows.props.len(), 2);
assert!(knows.has_props());
}
#[test]
fn test_edge_no_props() {
let follows = edge("follows").build();
assert_eq!(follows.name, "follows");
assert!(follows.props.is_empty());
assert!(!follows.has_props());
}
#[test]
fn test_node_schema_validation() {
let user = node("user")
.prop(prop::string("name"))
.prop(prop::int("age").optional())
.build();
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Alice".to_string()));
assert!(user.validate(&props).is_ok());
props.insert("age".to_string(), PropValue::I64(30));
assert!(user.validate(&props).is_ok());
let empty: HashMap<String, PropValue> = HashMap::new();
assert!(matches!(
user.validate(&empty),
Err(ValidationError::MissingRequired(_))
));
let mut wrong_type = HashMap::new();
wrong_type.insert("name".to_string(), PropValue::I64(42));
assert!(matches!(
user.validate(&wrong_type),
Err(ValidationError::TypeMismatch { .. })
));
}
#[test]
fn test_database_schema() {
let user = node("user")
.key(|id| format!("user:{}", id))
.prop(prop::string("name"))
.build();
let post = node("post")
.key(|id| format!("post:{}", id))
.prop(prop::string("title"))
.build();
let follows = edge("follows").build();
let authored = edge("authored").build();
let schema = DatabaseSchema::new()
.node(user)
.node(post)
.edge(follows)
.edge(authored);
assert_eq!(schema.node_types().len(), 2);
assert_eq!(schema.edge_types().len(), 2);
assert!(schema.get_node("user").is_some());
assert!(schema.get_edge("follows").is_some());
assert_eq!(schema.node_type_from_key("user:alice"), Some("user"));
assert_eq!(schema.node_type_from_key("post:123"), Some("post"));
assert!(schema.node_type_from_key("unknown:key").is_none());
}
#[test]
fn test_schema_macro() {
let schema = schema! {
nodes: [
node("user")
.prop(prop::string("name"))
.build(),
node("post")
.prop(prop::string("title"))
.build(),
],
edges: [
edge("follows").build(),
edge("authored").build(),
]
};
assert_eq!(schema.node_types().len(), 2);
assert_eq!(schema.edge_types().len(), 2);
}
#[test]
fn test_required_props() {
let user = node("user")
.prop(prop::string("name"))
.prop(prop::string("email"))
.prop(prop::int("age").optional())
.build();
let required = user.required_props();
assert_eq!(required.len(), 2);
assert!(required.contains(&"name"));
assert!(required.contains(&"email"));
assert!(!required.contains(&"age"));
}
#[test]
fn test_schema_type_conversion() {
assert_eq!(SchemaType::String.to_tag(), PropValueTag::String);
assert_eq!(SchemaType::Int.to_tag(), PropValueTag::I64);
assert_eq!(SchemaType::Float.to_tag(), PropValueTag::F64);
assert_eq!(SchemaType::Bool.to_tag(), PropValueTag::Bool);
assert_eq!(SchemaType::Vector.to_tag(), PropValueTag::VectorF32);
}
#[test]
fn test_vector_prop() {
let doc = node("document")
.prop(prop::string("title"))
.prop(prop::vector("embedding"))
.build();
assert!(doc.get_prop("embedding").is_some());
assert_eq!(
doc.get_prop("embedding").unwrap().schema_type,
SchemaType::Vector
);
let embedding_prop = prop::vector("embedding");
assert!(embedding_prop.validate(&PropValue::VectorF32(vec![0.1, 0.2, 0.3])));
assert!(!embedding_prop.validate(&PropValue::String("not a vector".to_string())));
}
#[test]
fn test_edge_validation() {
let knows = edge("knows")
.prop(prop::int("since"))
.prop(prop::float("weight").optional())
.build();
let mut props = HashMap::new();
props.insert("since".to_string(), PropValue::I64(2020));
assert!(knows.validate(&props).is_ok());
props.insert("weight".to_string(), PropValue::F64(0.95));
assert!(knows.validate(&props).is_ok());
let empty: HashMap<String, PropValue> = HashMap::new();
assert!(matches!(
knows.validate(&empty),
Err(ValidationError::MissingRequired(_))
));
}
#[test]
fn test_prop_with_default() {
let status = prop::string("status").default(PropValue::String("active".to_string()));
assert!(status.default.is_some());
let user = node("user")
.prop(prop::string("name"))
.prop(prop::string("status").default(PropValue::String("active".to_string())))
.build();
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Alice".to_string()));
assert!(user.validate(&props).is_ok());
}
}