Skip to content

Commit 4273ef7

Browse files
committed
SeqVerifier: Add caller and callee check
1 parent bc5233f commit 4273ef7

1 file changed

Lines changed: 140 additions & 8 deletions

File tree

plantuml/parser/puml_resolver/src/sequence_diagram/src/sequence_resolver.rs

Lines changed: 140 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
use crate::logic_parser::build_tree;
1515
use resolver_traits::DiagramResolver;
1616
use sequence_logic::SequenceTree;
17+
use sequence_parser::syntax_ast::{MessageContent, ParticipantIdentifier, Statement};
1718
use sequence_parser::SeqPumlDocument;
19+
use std::collections::HashSet;
1820

1921
/// Resolver for sequence diagrams.
2022
///
@@ -24,27 +26,82 @@ use sequence_parser::SeqPumlDocument;
2426
pub struct SequenceResolver;
2527

2628
/// Error type for `SequenceResolver`.
27-
///
28-
/// `build_tree` is currently infallible, so this enum has no variants.
29-
/// It satisfies the `std::error::Error` bound required by the CLI's generic
30-
/// `puml_resolver<R>` helper.
3129
#[derive(Debug)]
32-
pub enum SequenceResolverError {}
30+
pub enum SequenceResolverError {
31+
/// A message references a participant that was not declared in a
32+
/// `participant` (or actor/boundary/…) statement.
33+
UndeclaredParticipant { name: String, role: &'static str },
34+
}
3335

3436
impl std::fmt::Display for SequenceResolverError {
35-
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36-
match *self {}
37+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38+
match self {
39+
SequenceResolverError::UndeclaredParticipant { name, role } => {
40+
write!(f, "{role} '{name}' is not declared as a participant")
41+
}
42+
}
3743
}
3844
}
3945

4046
impl std::error::Error for SequenceResolverError {}
4147

48+
/// Collect all identifiers that a `ParticipantIdentifier` makes available.
49+
fn collect_participant_names(id: &ParticipantIdentifier, out: &mut HashSet<String>) {
50+
match id {
51+
ParticipantIdentifier::QuotedAsId { quoted, id } => {
52+
out.insert(quoted.clone());
53+
out.insert(id.clone());
54+
}
55+
ParticipantIdentifier::IdAsQuoted { id, quoted } => {
56+
out.insert(id.clone());
57+
out.insert(quoted.clone());
58+
}
59+
ParticipantIdentifier::IdAsId { id1, id2 } => {
60+
out.insert(id1.clone());
61+
out.insert(id2.clone());
62+
}
63+
ParticipantIdentifier::Quoted(s) | ParticipantIdentifier::Id(s) => {
64+
out.insert(s.clone());
65+
}
66+
}
67+
}
68+
4269
impl DiagramResolver for SequenceResolver {
4370
type Document = SeqPumlDocument;
4471
type Output = SequenceTree;
4572
type Error = SequenceResolverError;
4673

4774
fn resolve(&mut self, document: &SeqPumlDocument) -> Result<SequenceTree, Self::Error> {
75+
// 1. Collect declared participants.
76+
let mut declared = HashSet::new();
77+
for stmt in &document.statements {
78+
if let Statement::ParticipantDef(p) = stmt {
79+
collect_participant_names(&p.identifier, &mut declared);
80+
}
81+
}
82+
83+
// 2. Validate message targets only when participants are declared.
84+
if !declared.is_empty() {
85+
for stmt in &document.statements {
86+
if let Statement::Message(msg) = stmt {
87+
let MessageContent::WithTargets { left, right, .. } = &msg.content;
88+
if !left.is_empty() && !declared.contains(left) {
89+
return Err(SequenceResolverError::UndeclaredParticipant {
90+
name: left.clone(),
91+
role: "caller",
92+
});
93+
}
94+
if !right.is_empty() && !declared.contains(right) {
95+
return Err(SequenceResolverError::UndeclaredParticipant {
96+
name: right.clone(),
97+
role: "callee",
98+
});
99+
}
100+
}
101+
}
102+
}
103+
104+
// 3. Build the tree.
48105
let root_interactions = build_tree(&document.statements);
49106
Ok(SequenceTree {
50107
name: document.name.clone(),
@@ -58,7 +115,9 @@ mod sequence_resolver_tests {
58115
use super::*;
59116
use parser_core::common_ast::{Arrow, ArrowDecor, ArrowLine};
60117
use resolver_traits::DiagramResolver;
61-
use sequence_parser::syntax_ast::{Message, MessageContent, Statement};
118+
use sequence_parser::syntax_ast::{
119+
Message, MessageContent, ParticipantDef, ParticipantIdentifier, ParticipantType, Statement,
120+
};
62121

63122
fn solid_arrow() -> Arrow {
64123
Arrow {
@@ -169,4 +228,77 @@ mod sequence_resolver_tests {
169228

170229
assert_eq!(tree1.root_interactions.len(), tree2.root_interactions.len());
171230
}
231+
232+
fn make_participant(name: &str) -> Statement {
233+
Statement::ParticipantDef(ParticipantDef {
234+
participant_type: ParticipantType::Participant,
235+
identifier: ParticipantIdentifier::Id(name.to_string()),
236+
stereotype: None,
237+
})
238+
}
239+
240+
/// When participants are declared, all message targets must be among them.
241+
#[test]
242+
fn test_declared_participants_pass_validation() {
243+
let stmts = vec![
244+
make_participant("A"),
245+
make_participant("B"),
246+
make_call("A", "B", "doWork"),
247+
make_return("B", "A", "result"),
248+
];
249+
let mut resolver = SequenceResolver;
250+
let doc = SeqPumlDocument {
251+
name: Some("valid".to_string()),
252+
statements: stmts,
253+
};
254+
assert!(resolver.resolve(&doc).is_ok());
255+
}
256+
257+
/// An undeclared callee should cause an error.
258+
#[test]
259+
fn test_undeclared_callee_raises_error() {
260+
let stmts = vec![make_participant("A"), make_call("A", "B", "doWork")];
261+
let mut resolver = SequenceResolver;
262+
let doc = SeqPumlDocument {
263+
name: Some("bad_callee".to_string()),
264+
statements: stmts,
265+
};
266+
let err = resolver.resolve(&doc).unwrap_err();
267+
let msg = err.to_string();
268+
assert!(
269+
msg.contains("B"),
270+
"error should name the undeclared participant"
271+
);
272+
assert!(msg.contains("callee"), "error should indicate the role");
273+
}
274+
275+
/// An undeclared caller should cause an error.
276+
#[test]
277+
fn test_undeclared_caller_raises_error() {
278+
let stmts = vec![make_participant("B"), make_call("A", "B", "doWork")];
279+
let mut resolver = SequenceResolver;
280+
let doc = SeqPumlDocument {
281+
name: Some("bad_caller".to_string()),
282+
statements: stmts,
283+
};
284+
let err = resolver.resolve(&doc).unwrap_err();
285+
let msg = err.to_string();
286+
assert!(
287+
msg.contains("A"),
288+
"error should name the undeclared participant"
289+
);
290+
assert!(msg.contains("caller"), "error should indicate the role");
291+
}
292+
293+
/// When no participants are declared, messages are allowed freely (no validation).
294+
#[test]
295+
fn test_no_participants_declared_skips_validation() {
296+
let stmts = vec![make_call("X", "Y", "hello")];
297+
let mut resolver = SequenceResolver;
298+
let doc = SeqPumlDocument {
299+
name: Some("implicit".to_string()),
300+
statements: stmts,
301+
};
302+
assert!(resolver.resolve(&doc).is_ok());
303+
}
172304
}

0 commit comments

Comments
 (0)