#![cfg(feature = "file")]
use cyrs_schema::{
InMemorySchema, PropertyType, SchemaProvider,
file::{SchemaLoadError, load_from_toml_str, serialise_to_toml},
};
use smol_str::SmolStr;
const SCHEMA_FIXTURE: &str = r#"
[meta]
cyrs_schema_version = "0.1.0"
schema_name = "test"
description = "round-trip fixture."
[[label]]
name = "Person"
properties = [
{ name = "name", type = "STRING", required = true },
{ name = "age", type = "INTEGER" },
{ name = "active", type = "BOOLEAN" },
]
[[label]]
name = "Movie"
properties = [
{ name = "title", type = "STRING", required = true },
{ name = "released", type = "INTEGER" },
{ name = "genres", type = "LIST<STRING>" },
]
[[rel_type]]
name = "ACTED_IN"
start_labels = ["Person"]
end_labels = ["Movie"]
properties = [
{ name = "role", type = "STRING" },
]
[[parameter]]
name = "since_year"
type = "INTEGER"
default = 1990
[[parameter]]
name = "person_name"
type = "STRING"
default = "anonymous"
"#;
fn semantic_eq(a: &InMemorySchema, b: &InMemorySchema) -> bool {
if a.label_names() != b.label_names() {
return false;
}
if a.rel_type_names() != b.rel_type_names() {
return false;
}
for label in a.label_names() {
if a.node_properties(&label) != b.node_properties(&label) {
return false;
}
}
let a_rels: Vec<_> = a.rel_types().collect();
let b_rels: Vec<_> = b.rel_types().collect();
if a_rels != b_rels {
return false;
}
let a_params: Vec<_> = a.parameters().collect();
let b_params: Vec<_> = b.parameters().collect();
if a_params != b_params {
return false;
}
a.schema_name() == b.schema_name() && a.description() == b.description()
}
#[test]
fn load_reads_every_declaration() {
let schema = load_from_toml_str(SCHEMA_FIXTURE).expect("loads");
assert_eq!(schema.label_count(), 2);
assert_eq!(
schema.label_names(),
vec![SmolStr::new("Movie"), SmolStr::new("Person")],
);
let person_props = schema.node_properties("Person").expect("Person exists");
assert_eq!(person_props.len(), 3);
assert_eq!(person_props[0].name, SmolStr::new("name"));
assert_eq!(person_props[0].ty, PropertyType::String);
assert!(person_props[0].required);
assert_eq!(person_props[1].ty, PropertyType::Int);
assert_eq!(person_props[2].ty, PropertyType::Bool);
let movie_props = schema.node_properties("Movie").expect("Movie exists");
assert_eq!(movie_props.len(), 3);
assert_eq!(
movie_props[2].ty,
PropertyType::List(Box::new(PropertyType::String)),
);
assert_eq!(schema.rel_type_count(), 1);
assert_eq!(schema.relationship_types(), vec![SmolStr::new("ACTED_IN")]);
let endpoints = schema.relationship_endpoints("ACTED_IN");
assert_eq!(endpoints.len(), 1);
assert_eq!(endpoints[0].from, SmolStr::new("Person"));
assert_eq!(endpoints[0].to, SmolStr::new("Movie"));
assert_eq!(schema.parameter_count(), 2);
let params: Vec<_> = schema.parameters().collect();
assert_eq!(params[0].name, SmolStr::new("person_name"));
assert_eq!(params[0].ty, PropertyType::String);
assert_eq!(params[0].default.as_deref(), Some("anonymous"));
assert_eq!(params[1].name, SmolStr::new("since_year"));
assert_eq!(params[1].ty, PropertyType::Int);
assert_eq!(params[1].default.as_deref(), Some("1990"));
assert_eq!(schema.schema_name(), Some("test"));
assert_eq!(schema.description(), Some("round-trip fixture."));
}
#[test]
fn round_trip_is_semantically_stable() {
let original = load_from_toml_str(SCHEMA_FIXTURE).expect("loads");
let rendered = serialise_to_toml(&original);
let reloaded = load_from_toml_str(&rendered).expect("round-trip parses");
assert!(
semantic_eq(&original, &reloaded),
"round-trip schema must be semantically equal\n--- rendered ---\n{rendered}",
);
let rendered_again = serialise_to_toml(&reloaded);
assert_eq!(rendered, rendered_again);
}
#[test]
fn unknown_label_ref_surfaces_as_typed_error() {
let input = r#"
[[label]]
name = "Known"
[[rel_type]]
name = "BAD"
start_labels = ["Known"]
end_labels = ["Stranger"]
"#;
let err = load_from_toml_str(input).expect_err("should fail");
match err {
SchemaLoadError::UnknownLabelRef(n) => {
assert_eq!(n, SmolStr::new("Stranger"));
}
other => panic!("expected UnknownLabelRef, got {other:?}"),
}
}
#[test]
fn duplicate_label_surfaces_as_typed_error() {
let input = r#"
[[label]]
name = "Dup"
[[label]]
name = "Dup"
"#;
let err = load_from_toml_str(input).expect_err("should fail");
match err {
SchemaLoadError::DuplicateLabel(n) => {
assert_eq!(n, SmolStr::new("Dup"));
}
other => panic!("expected DuplicateLabel, got {other:?}"),
}
}
#[test]
fn malformed_type_string_surfaces_as_bad_type() {
let input = r#"
[[label]]
name = "X"
properties = [ { name = "p", type = "NOT_A_TYPE" } ]
"#;
let err = load_from_toml_str(input).expect_err("should fail");
match err {
SchemaLoadError::BadType(s) => {
assert!(s.contains("NOT_A_TYPE"), "message should mention type: {s}");
}
other => panic!("expected BadType, got {other:?}"),
}
}
#[test]
fn unsupported_schema_version_rejected() {
let input = r#"
[meta]
cyrs_schema_version = "99.0.0"
"#;
let err = load_from_toml_str(input).expect_err("should fail");
assert!(matches!(err, SchemaLoadError::BadType(_)));
}
#[test]
fn empty_file_loads_to_empty_schema() {
let schema = load_from_toml_str("").expect("empty file loads");
assert_eq!(schema.label_count(), 0);
assert_eq!(schema.rel_type_count(), 0);
assert_eq!(schema.parameter_count(), 0);
}