2026-02-08 13:24:35 +00:00
//! Bidirectional relationship resolution
//!
//! Handles relationships that can be declared from either participant's
//! perspective, merging self/other blocks and validating consistency.
use std ::collections ::HashMap ;
use crate ::{
resolve ::{
ResolveError ,
Result ,
} ,
syntax ::ast ::{
Declaration ,
Field ,
File ,
Participant ,
Relationship ,
} ,
} ;
/// A relationship key that's order-independent
/// (Martha, David) and (David, Martha) map to the same key
#[ derive(Debug, Clone, PartialEq, Eq, Hash) ]
struct RelationshipKey {
participants : Vec < String > ,
name : String ,
}
impl RelationshipKey {
fn new ( mut participants : Vec < String > , name : String ) -> Self {
// Sort participants to make key order-independent
participants . sort ( ) ;
Self { participants , name }
}
}
/// Information about a relationship declaration
#[ derive(Debug, Clone) ]
struct RelationshipDecl {
relationship : Relationship ,
}
/// Resolved bidirectional relationship
#[ derive(Debug, Clone) ]
pub struct ResolvedRelationship {
pub name : String ,
pub participants : Vec < Participant > ,
pub fields : Vec < Field > ,
/// Merged self/other blocks for each participant
pub participant_fields : Vec < ParticipantFields > ,
}
#[ derive(Debug, Clone) ]
pub struct ParticipantFields {
pub participant_name : Vec < String > ,
pub role : Option < String > ,
2026-02-13 21:52:03 +00:00
/// Fields from this participant's block
pub fields : Vec < Field > ,
2026-02-08 13:24:35 +00:00
}
/// Resolve bidirectional relationships in a file
pub fn resolve_relationships ( file : & File ) -> Result < Vec < ResolvedRelationship > > {
// Group relationships by key
let mut relationship_groups : HashMap < RelationshipKey , Vec < RelationshipDecl > > = HashMap ::new ( ) ;
for decl in & file . declarations {
if let Declaration ::Relationship ( rel ) = decl {
// Extract participant names
let participant_names : Vec < String > =
rel . participants . iter ( ) . map ( | p | p . name . join ( " :: " ) ) . collect ( ) ;
let key = RelationshipKey ::new ( participant_names , rel . name . clone ( ) ) ;
relationship_groups
. entry ( key )
. or_default ( )
. push ( RelationshipDecl {
relationship : rel . clone ( ) ,
} ) ;
}
}
// Merge grouped relationships
let mut resolved = Vec ::new ( ) ;
for ( key , decls ) in relationship_groups {
let merged = merge_relationship_declarations ( & key , decls ) ? ;
resolved . push ( merged ) ;
}
Ok ( resolved )
}
/// Merge multiple declarations of the same relationship
fn merge_relationship_declarations (
key : & RelationshipKey ,
decls : Vec < RelationshipDecl > ,
) -> Result < ResolvedRelationship > {
if decls . is_empty ( ) {
return Err ( ResolveError ::ValidationError {
message : " Empty relationship group " . to_string ( ) ,
help : Some ( " This is an internal error - relationship groups should never be empty. Please report this as a bug. " . to_string ( ) ) ,
} ) ;
}
// Start with the first declaration
let base = & decls [ 0 ] . relationship ;
let mut participant_fields : Vec < ParticipantFields > = base
. participants
. iter ( )
. map ( | p | ParticipantFields {
participant_name : p . name . clone ( ) ,
role : p . role . clone ( ) ,
2026-02-13 21:52:03 +00:00
fields : p . fields . clone ( ) ,
2026-02-08 13:24:35 +00:00
} )
. collect ( ) ;
2026-02-13 21:52:03 +00:00
// Merge shared fields (fields outside participant blocks)
let mut merged_fields = base . fields . clone ( ) ;
2026-02-08 13:24:35 +00:00
// Merge additional declarations
for decl in decls . iter ( ) . skip ( 1 ) {
2026-02-13 21:52:03 +00:00
// Merge participant fields
for participant in & decl . relationship . participants {
2026-02-08 13:24:35 +00:00
// Find this participant in our merged list
2026-02-13 21:52:03 +00:00
if let Some ( pf_idx ) = participant_fields
2026-02-08 13:24:35 +00:00
. iter ( )
2026-02-13 21:52:03 +00:00
. position ( | pf | pf . participant_name = = participant . name )
2026-02-08 13:24:35 +00:00
{
2026-02-13 21:52:03 +00:00
// Merge fields for this participant
merge_fields (
& mut participant_fields [ pf_idx ] . fields ,
participant . fields . clone ( ) ,
) ? ;
2026-02-08 13:24:35 +00:00
}
}
2026-02-13 21:52:03 +00:00
// Merge shared relationship fields
2026-02-08 13:24:35 +00:00
merge_fields ( & mut merged_fields , decl . relationship . fields . clone ( ) ) ? ;
}
Ok ( ResolvedRelationship {
name : key . name . clone ( ) ,
participants : base . participants . clone ( ) ,
fields : merged_fields ,
participant_fields ,
} )
}
/// Merge field lists, detecting conflicts
fn merge_fields ( target : & mut Vec < Field > , source : Vec < Field > ) -> Result < ( ) > {
for new_field in source {
// Check if field already exists
if let Some ( existing ) = target . iter ( ) . find ( | f | f . name = = new_field . name ) {
// Fields must have the same value
if existing . value ! = new_field . value {
return Err ( ResolveError ::ValidationError {
message : format ! (
" Conflicting values for field '{}' in relationship " ,
new_field . name
) ,
help : Some ( format! (
" The field '{}' has different values in different declarations of the same relationship. Make sure all declarations of this relationship use the same value for shared fields. " ,
new_field . name
) ) ,
} ) ;
}
// Same value, no need to add again
} else {
// New field, add it
target . push ( new_field ) ;
}
}
Ok ( ( ) )
}
#[ cfg(test) ]
mod tests {
use super ::* ;
use crate ::syntax ::ast ::{
Span ,
Value ,
} ;
fn make_participant ( name : & str , role : Option < & str > ) -> Participant {
Participant {
name : vec ! [ name . to_string ( ) ] ,
role : role . map ( | s | s . to_string ( ) ) ,
2026-02-13 21:52:03 +00:00
fields : vec ! [ ] ,
2026-02-08 13:24:35 +00:00
span : Span ::new ( 0 , 10 ) ,
}
}
fn make_field ( name : & str , value : i64 ) -> Field {
Field {
name : name . to_string ( ) ,
2026-02-14 14:03:21 +00:00
value : Value ::Number ( value ) ,
2026-02-08 13:24:35 +00:00
span : Span ::new ( 0 , 10 ) ,
}
}
#[ test ]
fn test_relationship_key_order_independent ( ) {
let key1 = RelationshipKey ::new (
vec! [ " Martha " . to_string ( ) , " David " . to_string ( ) ] ,
" Marriage " . to_string ( ) ,
) ;
let key2 = RelationshipKey ::new (
vec! [ " David " . to_string ( ) , " Martha " . to_string ( ) ] ,
" Marriage " . to_string ( ) ,
) ;
assert_eq! ( key1 , key2 ) ;
}
#[ test ]
fn test_single_relationship_declaration ( ) {
let file = File {
declarations : vec ! [ Declaration ::Relationship ( Relationship {
name : " Friendship " . to_string ( ) ,
participants : vec ! [
make_participant ( " Alice " , None ) ,
make_participant ( " Bob " , None ) ,
] ,
fields : vec ! [ make_field ( " bond " , 80 ) ] ,
span : Span ::new ( 0 , 10 ) ,
} ) ] ,
} ;
let resolved = resolve_relationships ( & file ) . unwrap ( ) ;
assert_eq! ( resolved . len ( ) , 1 ) ;
assert_eq! ( resolved [ 0 ] . name , " Friendship " ) ;
assert_eq! ( resolved [ 0 ] . participants . len ( ) , 2 ) ;
}
#[ test ]
2026-02-13 21:52:03 +00:00
fn test_relationship_merge ( ) {
2026-02-08 13:24:35 +00:00
let mut martha_participant = make_participant ( " Martha " , Some ( " spouse " ) ) ;
2026-02-13 21:52:03 +00:00
martha_participant . fields = vec! [ make_field ( " commitment " , 90 ) ] ;
2026-02-08 13:24:35 +00:00
let mut david_participant = make_participant ( " David " , Some ( " spouse " ) ) ;
2026-02-13 21:52:03 +00:00
david_participant . fields = vec! [ make_field ( " trust " , 85 ) ] ;
2026-02-08 13:24:35 +00:00
let file = File {
declarations : vec ! [
Declaration ::Relationship ( Relationship {
name : " Marriage " . to_string ( ) ,
participants : vec ! [
martha_participant . clone ( ) ,
make_participant ( " David " , Some ( " spouse " ) ) ,
] ,
fields : vec ! [ ] ,
span : Span ::new ( 0 , 10 ) ,
} ) ,
Declaration ::Relationship ( Relationship {
name : " Marriage " . to_string ( ) ,
participants : vec ! [
david_participant . clone ( ) ,
make_participant ( " Martha " , Some ( " spouse " ) ) ,
] ,
fields : vec ! [ ] ,
span : Span ::new ( 20 , 30 ) ,
} ) ,
] ,
} ;
let resolved = resolve_relationships ( & file ) . unwrap ( ) ;
assert_eq! ( resolved . len ( ) , 1 ) ;
assert_eq! ( resolved [ 0 ] . name , " Marriage " ) ;
}
#[ test ]
fn test_conflicting_field_values ( ) {
let mut p1 = make_participant ( " Alice " , None ) ;
2026-02-13 21:52:03 +00:00
p1 . fields = vec! [ make_field ( " bond " , 80 ) ] ;
2026-02-08 13:24:35 +00:00
let mut p2 = make_participant ( " Alice " , None ) ;
2026-02-13 21:52:03 +00:00
p2 . fields = vec! [ make_field ( " bond " , 90 ) ] ; // Different value
2026-02-08 13:24:35 +00:00
let file = File {
declarations : vec ! [
Declaration ::Relationship ( Relationship {
name : " Test " . to_string ( ) ,
participants : vec ! [ p1 , make_participant ( " Bob " , None ) ] ,
fields : vec ! [ ] ,
span : Span ::new ( 0 , 10 ) ,
} ) ,
Declaration ::Relationship ( Relationship {
name : " Test " . to_string ( ) ,
participants : vec ! [ p2 , make_participant ( " Bob " , None ) ] ,
fields : vec ! [ ] ,
span : Span ::new ( 20 , 30 ) ,
} ) ,
] ,
} ;
let result = resolve_relationships ( & file ) ;
assert! ( result . is_err ( ) ) ;
}
}