#![forbid(unsafe_code)]
#![doc(html_root_url = "https://docs.rs/cyrs-schema/0.0.1")]
use smol_str::SmolStr;
mod standard_library;
pub use standard_library::StandardLibrary;
mod in_memory;
pub use in_memory::{BuilderError, InMemorySchema, InMemorySchemaBuilder, RelDecl};
#[cfg(feature = "file")]
pub mod file;
pub mod diff;
pub mod lint;
pub trait SchemaProvider: Send + Sync + 'static {
fn labels(&self) -> Vec<SmolStr>;
fn relationship_types(&self) -> Vec<SmolStr>;
fn has_label(&self, name: &str) -> bool {
self.labels().iter().any(|l| l == name)
}
fn has_relationship_type(&self, name: &str) -> bool {
self.relationship_types().iter().any(|r| r == name)
}
fn node_properties(&self, label: &str) -> Option<Vec<PropertyDecl>>;
fn relationship_properties(&self, rel_type: &str) -> Option<Vec<PropertyDecl>>;
fn relationship_endpoints(&self, rel_type: &str) -> Vec<EndpointDecl>;
fn inverse_of(&self, rel_type: &str) -> Option<SmolStr>;
fn function(&self, name: &str) -> Option<FunctionSignature>;
fn procedure(&self, name: &str) -> Option<ProcedureSignature>;
fn schema_digest(&self) -> [u8; 32];
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct PropertyDecl {
pub name: SmolStr,
pub ty: PropertyType,
pub required: bool,
}
impl PropertyDecl {
#[must_use]
pub fn new(name: impl Into<SmolStr>, ty: PropertyType, required: bool) -> Self {
Self {
name: name.into(),
ty,
required,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[allow(missing_docs)]
pub enum PropertyType {
String,
Int,
Float,
Bool,
Date,
Datetime,
List(Box<PropertyType>),
Enum(SmolStr, Vec<SmolStr>),
Opaque(SmolStr),
Any,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EndpointDecl {
pub from: SmolStr,
pub to: SmolStr,
pub cardinality: Cardinality,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum Cardinality {
OneToOne,
OneToMany,
ManyToOne,
ManyToMany,
}
pub struct FunctionSignature {
pub name: SmolStr,
pub params: Vec<ParamDecl>,
pub variadic: Option<ParamDecl>,
pub return_ty: ReturnTy,
pub categories: FnCategories,
}
impl core::fmt::Debug for FunctionSignature {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("FunctionSignature")
.field("name", &self.name)
.field("params", &self.params)
.field("variadic", &self.variadic)
.field("categories", &self.categories)
.finish_non_exhaustive()
}
}
impl Clone for FunctionSignature {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
params: self.params.clone(),
variadic: self.variadic.clone(),
return_ty: match &self.return_ty {
ReturnTy::Constant(t) => ReturnTy::Constant(t.clone()),
ReturnTy::Dynamic(_) => ReturnTy::Constant(PropertyType::Any),
},
categories: self.categories,
}
}
}
pub type DynamicReturnFn = Box<dyn Fn(&[PropertyType]) -> PropertyType + Send + Sync>;
pub enum ReturnTy {
Constant(PropertyType),
Dynamic(DynamicReturnFn),
}
impl core::fmt::Debug for ReturnTy {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Constant(t) => f.debug_tuple("Constant").field(t).finish(),
Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"<fn>").finish(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FnCategories {
pub pure: bool,
pub aggregate: bool,
pub deterministic: bool,
}
#[derive(Debug, Clone)]
pub struct ProcedureSignature {
pub name: SmolStr,
pub params: Vec<ParamDecl>,
pub yields: Vec<YieldDecl>,
pub mode: ProcMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum ProcMode {
Read,
Write,
Schema,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ParamDecl {
pub name: SmolStr,
pub ty: PropertyType,
pub default: Option<SmolStr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct YieldDecl {
pub name: SmolStr,
pub ty: PropertyType,
}
#[derive(Debug, Default)]
pub struct EmptySchema;
impl SchemaProvider for EmptySchema {
fn labels(&self) -> Vec<SmolStr> {
Vec::new()
}
fn relationship_types(&self) -> Vec<SmolStr> {
Vec::new()
}
fn node_properties(&self, _: &str) -> Option<Vec<PropertyDecl>> {
None
}
fn relationship_properties(&self, _: &str) -> Option<Vec<PropertyDecl>> {
None
}
fn relationship_endpoints(&self, _: &str) -> Vec<EndpointDecl> {
Vec::new()
}
fn inverse_of(&self, _: &str) -> Option<SmolStr> {
None
}
fn function(&self, _: &str) -> Option<FunctionSignature> {
None
}
fn procedure(&self, _: &str) -> Option<ProcedureSignature> {
None
}
fn schema_digest(&self) -> [u8; 32] {
[0u8; 32]
}
}
#[doc(hidden)]
pub fn _assert_object_safe(_: &dyn SchemaProvider) {}
const _: fn() = || {
fn assert_send_sync_static<T: Send + Sync + 'static>() {}
assert_send_sync_static::<EmptySchema>();
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_schema_knows_nothing() {
let s = EmptySchema;
assert!(s.labels().is_empty());
assert!(!s.has_label("Person"));
assert_eq!(s.schema_digest(), [0u8; 32]);
}
#[test]
fn property_type_spec_variants_construct() {
let specs: [PropertyType; 9] = [
PropertyType::String,
PropertyType::Int,
PropertyType::Float,
PropertyType::Bool,
PropertyType::Date,
PropertyType::Datetime,
PropertyType::List(Box::new(PropertyType::Int)),
PropertyType::Enum(SmolStr::new("Color"), vec![SmolStr::new("Red")]),
PropertyType::Opaque(SmolStr::new("Uuid")),
];
assert_eq!(specs.len(), 9);
for t in &specs {
assert_eq!(t, &t.clone());
}
}
#[test]
fn property_type_any_fallback_distinct_from_spec_variants() {
let any = PropertyType::Any;
assert_ne!(any, PropertyType::String);
assert_ne!(any, PropertyType::Opaque(SmolStr::new("X")));
}
#[test]
fn cardinality_has_exactly_four_variants() {
let all: [Cardinality; 4] = [
Cardinality::OneToOne,
Cardinality::OneToMany,
Cardinality::ManyToOne,
Cardinality::ManyToMany,
];
for (i, c) in all.iter().enumerate() {
match c {
Cardinality::OneToOne
| Cardinality::OneToMany
| Cardinality::ManyToOne
| Cardinality::ManyToMany => {}
}
assert_eq!(*c, all[i]);
}
}
#[test]
fn proc_mode_has_exactly_three_variants() {
let all: [ProcMode; 3] = [ProcMode::Read, ProcMode::Write, ProcMode::Schema];
for m in &all {
match m {
ProcMode::Read | ProcMode::Write | ProcMode::Schema => {}
}
}
assert_eq!(all[0], ProcMode::Read);
assert_ne!(ProcMode::Read, ProcMode::Write);
}
#[test]
fn procedure_signature_read_mode_clones_and_compares_on_mode() {
let sig = ProcedureSignature {
name: SmolStr::new("db.ping"),
params: vec![],
yields: vec![YieldDecl {
name: SmolStr::new("ok"),
ty: PropertyType::Bool,
}],
mode: ProcMode::Read,
};
let cloned = sig.clone();
assert_eq!(cloned.mode, ProcMode::Read);
assert_eq!(cloned.yields.len(), 1);
assert_eq!(cloned.yields[0].ty, PropertyType::Bool);
}
#[test]
fn endpoint_decl_shape() {
let e = EndpointDecl {
from: SmolStr::new("Person"),
to: SmolStr::new("Company"),
cardinality: Cardinality::ManyToMany,
};
assert_eq!(e.clone(), e);
}
#[test]
fn property_decl_shape() {
let p = PropertyDecl {
name: SmolStr::new("age"),
ty: PropertyType::Int,
required: true,
};
assert!(p.required);
assert_eq!(p.clone(), p);
}
#[test]
fn function_signature_clone_preserves_params_and_categories() {
let c = FunctionSignature {
name: SmolStr::new("size"),
params: vec![ParamDecl {
name: SmolStr::new("x"),
ty: PropertyType::List(Box::new(PropertyType::Any)),
default: None,
}],
variadic: None,
return_ty: ReturnTy::Constant(PropertyType::Int),
categories: FnCategories {
pure: true,
aggregate: false,
deterministic: true,
},
};
let cloned = c.clone();
assert_eq!(cloned.name, c.name);
assert_eq!(cloned.params, c.params);
assert_eq!(cloned.categories, c.categories);
match cloned.return_ty {
ReturnTy::Constant(PropertyType::Int) => {}
_ => panic!("expected Constant(Int)"),
}
}
#[test]
fn function_signature_clone_dynamic_collapses_to_any() {
let d = FunctionSignature {
name: SmolStr::new("coalesce"),
params: vec![],
variadic: Some(ParamDecl {
name: SmolStr::new("args"),
ty: PropertyType::Any,
default: None,
}),
return_ty: ReturnTy::Dynamic(Box::new(|tys| {
tys.first().cloned().unwrap_or(PropertyType::Any)
})),
categories: FnCategories {
pure: true,
aggregate: false,
deterministic: true,
},
};
let cloned = d.clone();
assert_eq!(cloned.params, d.params);
assert_eq!(cloned.variadic, d.variadic);
assert_eq!(cloned.categories, d.categories);
match cloned.return_ty {
ReturnTy::Constant(PropertyType::Any) => {}
_ => panic!("Dynamic clone must collapse to Constant(Any)"),
}
}
}