use crate::error::TopoError;
use crate::ids::{NodeId, Scope};
use crate::op::Op;
use crate::state::NodeRecord;
use std::collections::HashMap;
pub(crate) fn edge_scope_is_valid(edge: Scope, from: Scope, to: Scope) -> bool {
match (from, to) {
(Scope::Shared, Scope::Shared) => true,
_ => {
let endpoint = match from {
Scope::Id(_) => from,
Scope::Shared => to,
};
edge == endpoint || edge == Scope::Shared
}
}
}
pub(crate) fn prevalidate_edge_scopes(
pre: &HashMap<NodeId, NodeRecord>,
ops: &[Op],
) -> Result<(), TopoError> {
let mut created_scope: HashMap<NodeId, Scope> = HashMap::new();
for op in ops {
match op {
Op::CreateNode { id, scope, .. } => {
created_scope.insert(*id, *scope);
}
Op::CreateEdge {
id,
scope,
from,
to,
..
} => {
let scope_of = |node: &NodeId| -> Option<Scope> {
created_scope
.get(node)
.copied()
.or_else(|| pre.get(node).map(|record| record.scope))
};
let (Some(from_scope), Some(to_scope)) = (scope_of(from), scope_of(to)) else {
continue;
};
if !edge_scope_is_valid(*scope, from_scope, to_scope) {
let endpoint = match from_scope {
Scope::Id(_) => from_scope,
Scope::Shared => to_scope,
};
return Err(TopoError::Rejected(format!(
"CreateEdge {id:?}: edge scope {} is unrelated to its endpoints \
(from: {}, to: {}). An edge touching a node in scope {} must be \
scoped {} or shared, or it is invisible to readers of {} and \
visible to readers of {}.",
label(*scope),
label(from_scope),
label(to_scope),
label(endpoint),
label(endpoint),
label(endpoint),
label(*scope),
)));
}
}
_ => {}
}
}
Ok(())
}
pub(crate) fn prevalidate_create_node_ids(
pre: &HashMap<NodeId, NodeRecord>,
ops: &[Op],
) -> Result<(), TopoError> {
let mut seen_in_batch: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
for op in ops {
if let Op::CreateNode { id, .. } = op {
if pre.contains_key(id) || !seen_in_batch.insert(*id) {
return Err(TopoError::Rejected(format!(
"CreateNode {id:?}: id already exists — ids are mint-once; use \
SetNodeProps to update an existing node instead of re-creating it"
)));
}
}
}
Ok(())
}
fn label(scope: Scope) -> String {
match scope {
Scope::Shared => "shared".to_string(),
Scope::Id(id) => id.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ids::ScopeId;
#[test]
fn shared_endpoints_admit_any_edge_scope() {
let p = Scope::Id(ScopeId::new());
assert!(edge_scope_is_valid(p, Scope::Shared, Scope::Shared));
assert!(edge_scope_is_valid(
Scope::Shared,
Scope::Shared,
Scope::Shared
));
}
#[test]
fn project_scoped_endpoint_admits_only_its_own_scope_or_shared() {
let a = Scope::Id(ScopeId::new());
assert!(edge_scope_is_valid(a, a, a));
assert!(edge_scope_is_valid(Scope::Shared, a, a));
assert!(edge_scope_is_valid(a, a, Scope::Shared));
assert!(edge_scope_is_valid(a, Scope::Shared, a));
assert!(edge_scope_is_valid(Scope::Shared, a, Scope::Shared));
assert!(edge_scope_is_valid(Scope::Shared, Scope::Shared, a));
}
#[test]
fn unrelated_project_scope_is_invalid_when_an_endpoint_is_project_scoped() {
let a = Scope::Id(ScopeId::new());
let p = Scope::Id(ScopeId::new());
assert!(!edge_scope_is_valid(p, a, a));
assert!(!edge_scope_is_valid(p, a, Scope::Shared));
assert!(!edge_scope_is_valid(p, Scope::Shared, a));
}
fn node_record(id: NodeId, scope: Scope) -> NodeRecord {
NodeRecord {
id,
scope,
label: Default::default(),
props: Default::default(),
embedding: None,
}
}
fn create_op(id: NodeId) -> Op {
Op::CreateNode {
id,
scope: Scope::Shared,
label: "X".into(),
props: Default::default(),
}
}
#[test]
fn fresh_id_is_accepted() {
let pre = HashMap::new();
let ops = vec![create_op(NodeId::new())];
assert!(prevalidate_create_node_ids(&pre, &ops).is_ok());
}
#[test]
fn id_already_present_in_pre_state_is_rejected() {
let id = NodeId::new();
let mut pre = HashMap::new();
pre.insert(id, node_record(id, Scope::Shared));
let ops = vec![create_op(id)];
assert!(matches!(
prevalidate_create_node_ids(&pre, &ops),
Err(TopoError::Rejected(_))
));
}
#[test]
fn duplicate_id_within_the_same_ops_slice_is_rejected() {
let pre = HashMap::new();
let id = NodeId::new();
let ops = vec![create_op(id), create_op(id)];
assert!(matches!(
prevalidate_create_node_ids(&pre, &ops),
Err(TopoError::Rejected(_))
));
}
#[test]
fn distinct_ids_and_non_create_ops_are_unaffected() {
let pre = HashMap::new();
let a = NodeId::new();
let b = NodeId::new();
let ops = vec![
create_op(a),
create_op(b),
Op::SetNodeProps {
id: a,
props: Default::default(),
},
];
assert!(prevalidate_create_node_ids(&pre, &ops).is_ok());
}
}