Files
storybook/src/resolve/links.rs
Sienna Meridian Satterwhite 8e4bdd3942 refactor(ast): rename value types to Number/Decimal/Text/Boolean
Renamed AST value types for v0.3 naming convention:
- Value::Int -> Value::Number
- Value::Float -> Value::Decimal
- Value::String -> Value::Text
- Value::Bool -> Value::Boolean
- Expr::IntLit -> Expr::NumberLit
- Expr::FloatLit -> Expr::DecimalLit
- Expr::StringLit -> Expr::TextLit
- Expr::BoolLit -> Expr::BooleanLit

Updated all references across parser, resolver, validator, LSP,
query engine, tests, and editor.
2026-02-14 14:03:21 +00:00

300 lines
9.4 KiB
Rust

//! 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>,
/// Fields from this participant's block
pub fields: Vec<Field>,
}
/// 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(),
fields: p.fields.clone(),
})
.collect();
// Merge shared fields (fields outside participant blocks)
let mut merged_fields = base.fields.clone();
// Merge additional declarations
for decl in decls.iter().skip(1) {
// Merge participant fields
for participant in &decl.relationship.participants {
// Find this participant in our merged list
if let Some(pf_idx) = participant_fields
.iter()
.position(|pf| pf.participant_name == participant.name)
{
// Merge fields for this participant
merge_fields(
&mut participant_fields[pf_idx].fields,
participant.fields.clone(),
)?;
}
}
// Merge shared relationship fields
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()),
fields: vec![],
span: Span::new(0, 10),
}
}
fn make_field(name: &str, value: i64) -> Field {
Field {
name: name.to_string(),
value: Value::Number(value),
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]
fn test_relationship_merge() {
let mut martha_participant = make_participant("Martha", Some("spouse"));
martha_participant.fields = vec![make_field("commitment", 90)];
let mut david_participant = make_participant("David", Some("spouse"));
david_participant.fields = vec![make_field("trust", 85)];
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);
p1.fields = vec![make_field("bond", 80)];
let mut p2 = make_participant("Alice", None);
p2.fields = vec![make_field("bond", 90)]; // Different value
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());
}
}