use super::*;
#[test]
fn builder_dedupes_nodes_and_edges() {
let mut builder = GraphBuilder::new();
let talk = builder
.node("Talk", "talk-1")
.prop("title", "A Talk")
.finish();
let person = builder.node("Person", "person-1").finish();
builder
.node("Talk", "talk-1")
.prop("description", "Updated")
.finish();
builder.edge("PRESENTS", &person, &talk).finish();
builder.edge("PRESENTS", &person, &talk).finish();
let graph = builder.build();
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
assert!(graph.nodes.iter().any(|node| {
node.id == NodeId::from("talk-1")
&& node.props.get("id") == Some(&Value::from("talk-1"))
&& node.props.get("description") == Some(&Value::from("Updated"))
}));
}
#[test]
fn graph_loads_from_yaml() {
let graph = Graph::from_yaml(
r#"
nodes:
- id: employee:nia
label: Employee
props:
level: IC4
department: Engineering
- id: employee:marco
label: Employee
edges:
- label: REPORTS_TO
from: employee:nia
to: employee:marco
props:
source: hris
"#,
)
.expect("graph yaml should load");
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
assert_eq!(
graph.nodes[0].props.get("id"),
Some(&Value::from("employee:nia"))
);
assert_eq!(graph.nodes[0].props.get("level"), Some(&Value::from("IC4")));
}
#[test]
fn graph_yaml_rejects_edges_with_unknown_nodes() {
let error = Graph::from_yaml(
r#"
nodes:
- id: employee:nia
label: Employee
edges:
- label: REPORTS_TO
from: employee:nia
to: employee:missing
"#,
)
.expect_err("missing node should fail validation");
assert!(
error
.to_string()
.contains("unknown to node 'employee:missing'")
);
}
#[test]
fn graph_loads_from_json() {
let graph = Graph::from_json(
r#"
{
"nodes": [
{
"id": "employee:nia",
"label": "Employee",
"props": {
"level": "IC4",
"department": "Engineering"
}
},
{
"id": "employee:marco",
"label": "Employee"
}
],
"edges": [
{
"label": "REPORTS_TO",
"from": "employee:nia",
"to": "employee:marco",
"props": {
"source": "hris"
}
}
]
}
"#,
)
.expect("graph json should load");
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
assert_eq!(
graph.nodes[0].props.get("id"),
Some(&Value::from("employee:nia"))
);
assert_eq!(graph.nodes[0].props.get("level"), Some(&Value::from("IC4")));
}
#[test]
fn graph_json_rejects_edges_with_unknown_nodes() {
let error = Graph::from_json(
r#"
{
"nodes": [
{ "id": "employee:nia", "label": "Employee" }
],
"edges": [
{ "label": "REPORTS_TO", "from": "employee:nia", "to": "employee:missing" }
]
}
"#,
)
.expect_err("missing node should fail validation");
assert!(
error
.to_string()
.contains("unknown to node 'employee:missing'")
);
}
#[test]
fn graph_serializes_to_json() {
let mut builder = GraphBuilder::new();
builder
.node("Employee", "employee:nia")
.prop("level", "IC4")
.finish();
builder.node("Employee", "employee:marco").finish();
builder
.edge("REPORTS_TO", "employee:nia", "employee:marco")
.finish();
let json = builder.build().to_json().expect("graph should serialize");
assert!(json.contains("\"employee:nia\""));
assert!(json.contains("\"REPORTS_TO\""));
assert!(!json.contains("\"value\": \"employee:nia\""));
}
#[test]
fn graph_loads_from_xml() {
let graph = Graph::from_xml(
r#"
<graph>
<nodes>
<node>
<id>employee:nia</id>
<label>Employee</label>
<props>
<prop>
<key>level</key>
<value>
<type>string</type>
<value>IC4</value>
</value>
</prop>
</props>
</node>
<node>
<id>employee:marco</id>
<label>Employee</label>
</node>
</nodes>
<edges>
<edge>
<label>REPORTS_TO</label>
<from>employee:nia</from>
<to>employee:marco</to>
</edge>
</edges>
</graph>
"#,
)
.expect("graph xml should load");
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
assert_eq!(
graph.nodes[0].props.get("id"),
Some(&Value::from("employee:nia"))
);
assert_eq!(graph.nodes[0].props.get("level"), Some(&Value::from("IC4")));
}
#[test]
fn graph_xml_rejects_edges_with_unknown_nodes() {
let error = Graph::from_xml(
r#"
<graph>
<nodes>
<node>
<id>employee:nia</id>
<label>Employee</label>
</node>
</nodes>
<edges>
<edge>
<label>REPORTS_TO</label>
<from>employee:nia</from>
<to>employee:missing</to>
</edge>
</edges>
</graph>
"#,
)
.expect_err("missing node should fail validation");
assert!(
error
.to_string()
.contains("unknown to node 'employee:missing'")
);
}
#[test]
fn graph_serializes_to_xml() {
let mut builder = GraphBuilder::new();
builder
.node("Employee", "employee:nia")
.prop("level", "IC4")
.finish();
builder.node("Employee", "employee:marco").finish();
builder
.edge("REPORTS_TO", "employee:nia", "employee:marco")
.finish();
let graph = builder.build();
let xml = graph.to_xml().expect("graph should serialize");
let reparsed = Graph::from_xml(&xml).expect("serialized xml should load");
assert!(xml.contains("<id>employee:nia</id>"));
assert!(xml.contains("<label>REPORTS_TO</label>"));
assert_eq!(reparsed, graph);
}
#[test]
fn graph_schema_validates_labels_fields_and_edge_endpoints() {
let schema = GraphSchema::builder()
.node(
"Person",
vec![
Field::required("name", FieldType::String),
Field::optional("age", FieldType::Int),
],
)
.node("Project", vec![Field::required("name", FieldType::String)])
.edge(
"WORKS_ON",
vec![Label::new("Person")],
vec![Label::new("Project")],
vec![Field::required("role", FieldType::String)],
)
.build();
let mut builder = Graph::builder();
builder
.node("Person", "person:ada")
.prop("name", "Ada")
.prop("age", 36i64)
.finish();
builder
.node("Project", "project:grust")
.prop("name", "Grust")
.finish();
builder
.edge("WORKS_ON", "person:ada", "project:grust")
.prop("role", "maintainer")
.finish();
schema
.validate_graph(&builder.build())
.expect("graph should match schema");
}
#[test]
fn graph_schema_rejects_wrong_field_type() {
let schema = GraphSchema::builder()
.node("Person", vec![Field::required("age", FieldType::Int)])
.build();
let mut builder = Graph::builder();
builder
.node("Person", "person:ada")
.prop("age", "old")
.finish();
let error = schema
.validate_graph(&builder.build())
.expect_err("string age should fail int field validation");
assert!(error.to_string().contains("field 'age' expected Int"));
}
#[test]
fn graph_schema_rejects_wrong_edge_endpoint_label() {
let schema = GraphSchema::builder()
.node("Person", Vec::<Field>::new())
.node("Project", Vec::<Field>::new())
.edge(
"WORKS_ON",
vec![Label::new("Person")],
vec![Label::new("Project")],
Vec::<Field>::new(),
)
.build();
let mut builder = Graph::builder();
builder.node("Project", "project:source").finish();
builder.node("Project", "project:target").finish();
builder
.edge("WORKS_ON", "project:source", "project:target")
.finish();
let error = schema
.validate_graph(&builder.build())
.expect_err("wrong from label should fail");
assert!(
error
.to_string()
.contains("cannot start from node label 'Project'")
);
}
#[cfg(feature = "typed-garde")]
mod typed_garde_tests {
use serde::{Deserialize, Serialize};
use super::*;
use crate::typed::{TypedEdge, TypedGraphBuilder, TypedNode, garde};
#[derive(Debug, Deserialize, Serialize, garde::Validate)]
#[garde(allow_unvalidated)]
struct Person {
#[garde(length(min = 1))]
id: String,
#[garde(length(min = 1))]
name: String,
#[garde(length(min = 2))]
skills: Vec<String>,
}
impl TypedNode for Person {
const LABEL: &'static str = "Person";
fn node_id(&self) -> NodeId {
format!("person:{}", self.id).into()
}
}
#[derive(Debug, Deserialize, Serialize, garde::Validate)]
#[garde(allow_unvalidated)]
struct Project {
#[garde(length(min = 1))]
id: String,
#[garde(length(min = 1))]
title: String,
}
impl TypedNode for Project {
const LABEL: &'static str = "Project";
fn node_id(&self) -> NodeId {
format!("project:{}", self.id).into()
}
}
#[derive(Debug, Deserialize, Serialize, garde::Validate)]
#[garde(allow_unvalidated)]
struct WorksOn {
#[garde(length(min = 1))]
person_id: String,
#[garde(length(min = 1))]
project_id: String,
#[garde(range(min = 1, max = 100))]
allocation: u8,
}
impl TypedEdge for WorksOn {
const LABEL: &'static str = "WORKS_ON";
fn source_node_id(&self) -> NodeId {
format!("person:{}", self.person_id).into()
}
fn target_node_id(&self) -> NodeId {
format!("project:{}", self.project_id).into()
}
}
#[test]
fn typed_builder_validates_and_lowers_nodes_and_edges() {
let mut builder = TypedGraphBuilder::new();
builder
.add_node(&Person {
id: "nia".to_string(),
name: "Nia".to_string(),
skills: vec!["rust".to_string(), "graphs".to_string()],
})
.expect("person is valid");
builder
.add_node(&Project {
id: "grust".to_string(),
title: "Grust".to_string(),
})
.expect("project is valid");
builder
.add_edge(&WorksOn {
person_id: "nia".to_string(),
project_id: "grust".to_string(),
allocation: 80,
})
.expect("edge is valid");
let graph = builder.build();
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
assert_eq!(graph.nodes[0].label, Label::from("Person"));
assert_eq!(
graph.nodes[0].props.get("skills"),
Some(&Value::from(vec!["rust".to_string(), "graphs".to_string()]))
);
assert_eq!(graph.edges[0].label, Label::from("WORKS_ON"));
assert_eq!(graph.edges[0].from, NodeId::from("person:nia"));
assert_eq!(graph.edges[0].to, NodeId::from("project:grust"));
}
#[test]
fn typed_builder_rejects_invalid_values_before_building_graph() {
let mut builder = TypedGraphBuilder::new();
let error = builder
.add_edge(&WorksOn {
person_id: "nia".to_string(),
project_id: "grust".to_string(),
allocation: 0,
})
.expect_err("allocation must be in range");
assert!(error.to_string().contains("WORKS_ON validation failed"));
}
#[test]
fn typed_builder_coexists_with_raw_nodes_and_edges() {
let mut builder = TypedGraphBuilder::new();
builder.add_raw_node(Node::new("Document", "doc:proposal", Props::new()));
builder
.add_node(&Person {
id: "nia".to_string(),
name: "Nia".to_string(),
skills: vec!["rust".to_string(), "graphs".to_string()],
})
.expect("person is valid");
builder.add_raw_edge(Edge::new(
"AUTHORED",
"person:nia",
"doc:proposal",
Props::new(),
));
let graph = builder.build();
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
assert!(graph.nodes.iter().any(|node| {
node.id == NodeId::from("doc:proposal") && node.label == Label::from("Document")
}));
assert!(graph.nodes.iter().any(|node| {
node.id == NodeId::from("person:nia") && node.label == Label::from("Person")
}));
assert_eq!(graph.edges[0].label, Label::from("AUTHORED"));
}
#[test]
fn typed_builder_can_extend_an_existing_graph() {
let existing = Graph::new(
vec![Node::new("Document", "doc:proposal", Props::new())],
Vec::new(),
);
let mut builder = TypedGraphBuilder::from_graph(existing);
builder
.add_node(&Person {
id: "nia".to_string(),
name: "Nia".to_string(),
skills: vec!["rust".to_string(), "graphs".to_string()],
})
.expect("person is valid");
let mut builder = builder.into_builder();
builder
.edge("AUTHORED", "person:nia", "doc:proposal")
.finish();
let graph = TypedGraphBuilder::from_builder(builder).build();
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
assert_eq!(graph.edges[0].from, NodeId::from("person:nia"));
assert_eq!(graph.edges[0].to, NodeId::from("doc:proposal"));
}
#[cfg(feature = "typed-zod-rs")]
#[test]
fn zod_rs_schema_can_feed_typed_garde_nodes_and_edges() {
use serde_json::json;
use zod_rs::prelude::{Schema, number, object, string};
let person_schema = object()
.field("id", string().min(1))
.field("name", string().min(1))
.field("skills", string().min(1).array())
.strict();
let project_schema = object()
.field("id", string().min(1))
.field("title", string().min(1))
.strict();
let works_on_schema = object()
.field("person_id", string().min(1))
.field("project_id", string().min(1))
.field("allocation", number().int().min(1.0).max(100.0))
.strict();
let mut builder = TypedGraphBuilder::new();
builder
.add_node_from_json::<Person, _>(
&person_schema,
&json!({
"id": "nia",
"name": "Nia",
"skills": ["rust", "graphs"]
}),
)
.expect("person JSON is valid");
builder
.add_node_from_json::<Project, _>(
&project_schema,
&json!({
"id": "grust",
"title": "Grust"
}),
)
.expect("project JSON is valid");
builder
.add_edge_from_json::<WorksOn, _>(
&works_on_schema,
&json!({
"person_id": "nia",
"project_id": "grust",
"allocation": 80
}),
)
.expect("edge JSON is valid");
let graph = builder.build();
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
assert_eq!(graph.edges[0].label, Label::from("WORKS_ON"));
}
#[cfg(feature = "typed-zod-rs")]
#[test]
fn zod_rs_schema_errors_before_deserialize_and_garde_errors_after() {
use serde_json::json;
use zod_rs::prelude::{Schema, object, string};
let loose_person_schema = object()
.field("id", string().min(1))
.field("name", string().min(1))
.field("skills", string().array())
.strict();
let shape_error = crate::typed::parse_typed_json::<Person, _>(
&loose_person_schema,
&json!({
"id": "nia",
"name": "Nia",
"skills": "rust"
}),
)
.expect_err("zod-rs should reject a non-array skills field");
assert!(shape_error.to_string().contains("zod-rs validation failed"));
let domain_error = crate::typed::parse_typed_json::<Person, _>(
&loose_person_schema,
&json!({
"id": "nia",
"name": "Nia",
"skills": ["rust"]
}),
)
.expect_err("garde should reject too few skills");
assert!(domain_error.to_string().contains("typed validation failed"));
}
}
#[test]
fn graph_serializes_to_yaml() {
let mut builder = GraphBuilder::new();
builder
.node("Employee", "employee:nia")
.prop("level", "IC4")
.finish();
builder.node("Employee", "employee:marco").finish();
builder
.edge("REPORTS_TO", "employee:nia", "employee:marco")
.finish();
let yaml = builder.build().to_yaml().expect("graph should serialize");
assert!(yaml.contains("employee:nia"));
assert!(yaml.contains("REPORTS_TO"));
assert!(!yaml.contains("value: employee:nia"));
}
#[test]
fn graph_builder_label_conflict_replaces_node() {
let mut builder = GraphBuilder::new();
builder
.node("Person", "entity:1")
.prop("name", "Ada")
.finish();
builder
.node("Robot", "entity:1")
.prop("model", "Mark I")
.finish();
let graph = builder.build();
assert_eq!(graph.nodes.len(), 1);
let node = &graph.nodes[0];
assert_eq!(node.label, Label::from("Robot"));
assert!(node.props.contains_key("model"));
assert!(
!node.props.contains_key("name"),
"label conflict should replace the node, not merge props"
);
}
#[test]
fn graph_builder_same_label_merges_props() {
let mut builder = GraphBuilder::new();
builder
.node("Person", "person:ada")
.prop("name", "Ada")
.finish();
builder
.node("Person", "person:ada")
.prop("age", 36i64)
.finish();
let graph = builder.build();
assert_eq!(graph.nodes.len(), 1);
let node = &graph.nodes[0];
assert_eq!(node.props.get("name"), Some(&Value::from("Ada")));
assert_eq!(node.props.get("age"), Some(&Value::from(36i64)));
}
#[test]
#[should_panic(expected = "Traversal::to() must follow")]
fn traversal_to_without_step_panics() {
let _ = Traversal::from_node("person:ada").to("Person");
}
#[test]
fn validate_edge_with_uses_label_lookup() {
let schema = GraphSchema::builder()
.node("Person", Vec::<Field>::new())
.node("Project", Vec::<Field>::new())
.edge(
"WORKS_ON",
vec![Label::new("Person")],
vec![Label::new("Project")],
Vec::<Field>::new(),
)
.build();
let person = Label::new("Person");
let project = Label::new("Project");
let lookup = |id: &NodeId| match id.as_str() {
"person:ada" => Some(&person),
"project:grust" => Some(&project),
_ => None,
};
let edge = Edge::new("WORKS_ON", "person:ada", "project:grust", Props::new());
schema
.validate_edge_with(&edge, lookup)
.expect("edge should validate via lookup");
let dangling = Edge::new("WORKS_ON", "person:ada", "project:missing", Props::new());
let error = schema
.validate_edge_with(&dangling, lookup)
.expect_err("dangling edge should fail");
assert!(error.to_string().contains("unknown to node"));
let wrong = Edge::new("WORKS_ON", "project:grust", "project:grust", Props::new());
let error = schema
.validate_edge_with(&wrong, lookup)
.expect_err("wrong from label should fail");
assert!(error.to_string().contains("cannot start from node label"));
}
#[test]
fn validate_edge_props_checks_label_and_fields_only() {
let schema = GraphSchema::builder()
.edge(
"WORKS_ON",
vec![Label::new("Person")],
vec![Label::new("Project")],
vec![Field::required("role", FieldType::String)],
)
.build();
let edge = Edge::new("WORKS_ON", "anyone", "anything", Props::new());
let error = schema
.validate_edge_props(&edge)
.expect_err("missing required field should fail");
assert!(error.to_string().contains("missing required field 'role'"));
let mut props = Props::new();
props.insert("role".to_string(), Value::from("maintainer"));
let edge = Edge::new("WORKS_ON", "anyone", "anything", props);
schema
.validate_edge_props(&edge)
.expect("props-only validation should pass");
let unknown = Edge::new("UNKNOWN", "a", "b", Props::new());
let error = schema
.validate_edge_props(&unknown)
.expect_err("unknown edge label should fail");
assert!(error.to_string().contains("schema has no edge type"));
}
#[test]
fn datetime_value_validates_rfc3339() {
assert!(Value::datetime("2026-06-12T09:30:00Z").is_ok());
assert!(Value::datetime("2026-06-12T09:30:00.123+02:00").is_ok());
assert!(Value::datetime("2026-02-29T09:30:00Z").is_err());
assert!(Value::datetime("2024-02-29T09:30:00Z").is_ok());
assert!(Value::datetime("2026-06-12 09:30:00Z").is_err());
assert!(Value::datetime("2026-06-12T09:30:00z").is_err());
assert!(Value::datetime("not a date").is_err());
assert!(Value::datetime("2026-06-12").is_err());
assert!(Value::datetime("2026-13-12T09:30:00Z").is_err());
assert!(Value::datetime("2026-04-31T09:30:00Z").is_err());
assert!(Value::datetime("2026-06-12T25:30:00Z").is_err());
assert!(Value::datetime("2026-06-12T09:30:00").is_err());
assert!(Value::datetime("2026-06-12T09:30:00.Z").is_err());
}
#[test]
fn schema_validates_datetime_and_array_fields() {
let schema = GraphSchema::builder()
.node(
"Event",
vec![
Field::required("when", FieldType::DateTime),
Field::optional("counts", FieldType::IntArray),
Field::optional("scores", FieldType::FloatArray),
],
)
.build();
let mut builder = Graph::builder();
builder
.node("Event", "event:1")
.prop("when", Value::datetime("2026-06-12T09:30:00Z").unwrap())
.prop("counts", vec![1i64, 2, 3])
.prop("scores", vec![0.5f64, 0.9])
.finish();
schema
.validate_graph(&builder.build())
.expect("typed values should validate");
let mut builder = Graph::builder();
builder
.node("Event", "event:2")
.prop("when", "2026-06-12T09:30:00Z")
.finish();
schema
.validate_graph(&builder.build())
.expect("RFC 3339 string should validate as DateTime");
let mut builder = Graph::builder();
builder
.node("Event", "event:3")
.prop("when", "soon")
.finish();
let error = schema
.validate_graph(&builder.build())
.expect_err("non-RFC 3339 string should fail DateTime field");
assert!(error.to_string().contains("expected DateTime"));
}
#[test]
fn value_json_round_trips_plain_and_tagged() {
assert_eq!(
Value::from(vec![1i64, 2]).to_json(),
serde_json::json!([1, 2])
);
assert_eq!(
Value::from_json(serde_json::json!([1, 2])),
Value::IntArray(vec![1, 2])
);
assert_eq!(
Value::from_json(serde_json::json!([1.5, 2.0])),
Value::FloatArray(vec![1.5, 2.0])
);
assert_eq!(
Value::from_json(serde_json::json!(["a", "b"])),
Value::StringArray(vec!["a".to_string(), "b".to_string()])
);
let dt = Value::datetime("2026-06-12T09:30:00Z").unwrap();
assert_eq!(dt.to_json(), serde_json::json!("2026-06-12T09:30:00Z"));
let tagged = serde_json::to_value(&dt).unwrap();
assert_eq!(Value::from_json(tagged), dt);
}
#[test]
fn builder_add_edge_reports_outcome() {
let mut builder = GraphBuilder::new();
builder.node("Person", "a").finish();
builder.node("Person", "b").finish();
let first = builder.edge("KNOWS", "a", "b").finish();
let second = builder.edge("KNOWS", "a", "b").finish();
assert_eq!(first, PutOutcome::Inserted);
assert_eq!(second, PutOutcome::Deduped);
}
#[test]
fn schema_enforces_edge_uniqueness() {
let schema = GraphSchema::builder()
.node("Person", Vec::<Field>::new())
.edge(
"KNOWS",
vec![Label::new("Person")],
vec![Label::new("Person")],
Vec::<Field>::new(),
)
.build();
let mut builder = GraphBuilder::new().edge_policy(EdgePolicy::AllowDuplicates);
builder.node("Person", "a").finish();
builder.node("Person", "b").finish();
builder.edge("KNOWS", "a", "b").finish();
builder.edge("KNOWS", "a", "b").finish();
let error = schema
.validate_graph(&builder.build())
.expect_err("duplicate edge should violate uniqueness");
assert!(error.to_string().contains("violates"));
}
#[test]
fn undirected_edge_type_accepts_reversed_endpoints_and_unordered_uniqueness() {
let schema = GraphSchema::builder()
.node("Person", Vec::<Field>::new())
.node("Project", Vec::<Field>::new())
.edge_type(EdgeType {
label: Label::new("LINKED"),
from: vec![Label::new("Person")],
to: vec![Label::new("Project")],
fields: Vec::new(),
directed: false,
uniqueness: EdgeUniqueness::FromLabelTo,
})
.build();
let mut builder = GraphBuilder::new();
builder.node("Person", "person:ada").finish();
builder.node("Project", "project:grust").finish();
builder
.edge("LINKED", "project:grust", "person:ada")
.finish();
schema
.validate_graph(&builder.build())
.expect("reversed undirected edge should validate");
let mut builder = GraphBuilder::new().edge_policy(EdgePolicy::AllowDuplicates);
builder.node("Person", "person:ada").finish();
builder.node("Project", "project:grust").finish();
builder
.edge("LINKED", "person:ada", "project:grust")
.finish();
builder
.edge("LINKED", "project:grust", "person:ada")
.finish();
let error = schema
.validate_graph(&builder.build())
.expect_err("reversed duplicate should violate uniqueness");
assert!(error.to_string().contains("violates"));
let directed = GraphSchema::builder()
.node("Person", Vec::<Field>::new())
.node("Project", Vec::<Field>::new())
.edge(
"LINKED",
vec![Label::new("Person"), Label::new("Project")],
vec![Label::new("Person"), Label::new("Project")],
Vec::<Field>::new(),
)
.build();
let mut builder = GraphBuilder::new().edge_policy(EdgePolicy::AllowDuplicates);
builder.node("Person", "person:ada").finish();
builder.node("Project", "project:grust").finish();
builder
.edge("LINKED", "person:ada", "project:grust")
.finish();
builder
.edge("LINKED", "project:grust", "person:ada")
.finish();
directed
.validate_graph(&builder.build())
.expect("opposite directed edges are distinct");
}