diff --git a/plantuml/parser/integration_test/sequence_diagram/invalid_undeclared_participant/invalid_undeclared_participant.puml b/plantuml/parser/integration_test/sequence_diagram/implicit_participant_from_message_endpoint/implicit_participant_from_message_endpoint.puml similarity index 92% rename from plantuml/parser/integration_test/sequence_diagram/invalid_undeclared_participant/invalid_undeclared_participant.puml rename to plantuml/parser/integration_test/sequence_diagram/implicit_participant_from_message_endpoint/implicit_participant_from_message_endpoint.puml index 44532be1..09073796 100644 --- a/plantuml/parser/integration_test/sequence_diagram/invalid_undeclared_participant/invalid_undeclared_participant.puml +++ b/plantuml/parser/integration_test/sequence_diagram/implicit_participant_from_message_endpoint/implicit_participant_from_message_endpoint.puml @@ -11,7 +11,7 @@ ' SPDX-License-Identifier: Apache-2.0 ' ******************************************************************************* -@startuml invalid_undeclared_participant +@startuml implicit_participant_from_message_endpoint participant Client diff --git a/plantuml/parser/integration_test/sequence_diagram/implicit_participant_from_message_endpoint/output.json b/plantuml/parser/integration_test/sequence_diagram/implicit_participant_from_message_endpoint/output.json new file mode 100644 index 00000000..44c58530 --- /dev/null +++ b/plantuml/parser/integration_test/sequence_diagram/implicit_participant_from_message_endpoint/output.json @@ -0,0 +1,43 @@ +{ + "implicit_participant_from_message_endpoint.puml": { + "name": "implicit_participant_from_message_endpoint", + "participants": [ + { + "display_name": "Client", + "alias": null, + "participant_type": "Participant", + "source_location": { + "file": "", + "line": 16 + }, + "stereotype": null + }, + { + "display_name": "Service", + "alias": null, + "participant_type": "Participant", + "source_location": { + "file": "", + "line": 18 + }, + "stereotype": null + } + ], + "root_interactions": [ + { + "event": { + "Interaction": { + "caller": "Client", + "callee": "Service", + "method": "request()" + } + }, + "source_location": { + "file": "", + "line": 18 + }, + "branches_node": [] + } + ] + } +} diff --git a/plantuml/parser/integration_test/sequence_diagram/invalid_undeclared_participant/output.yaml b/plantuml/parser/integration_test/sequence_diagram/invalid_undeclared_participant/output.yaml deleted file mode 100644 index 837eb127..00000000 --- a/plantuml/parser/integration_test/sequence_diagram/invalid_undeclared_participant/output.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -invalid_undeclared_participant.puml: - error: - type: "UndeclaredParticipant" - fields: - name: "Service" - role: "callee" diff --git a/plantuml/parser/integration_test/sequence_diagram/official_lost_found_endpoint_variants/official_lost_found_endpoint_variants.puml b/plantuml/parser/integration_test/sequence_diagram/lost_found_endpoint_resolution/lost_found_endpoint_resolution.puml similarity index 66% rename from plantuml/parser/integration_test/sequence_diagram/official_lost_found_endpoint_variants/official_lost_found_endpoint_variants.puml rename to plantuml/parser/integration_test/sequence_diagram/lost_found_endpoint_resolution/lost_found_endpoint_resolution.puml index ddbc550e..30ae43d8 100644 --- a/plantuml/parser/integration_test/sequence_diagram/official_lost_found_endpoint_variants/official_lost_found_endpoint_variants.puml +++ b/plantuml/parser/integration_test/sequence_diagram/lost_found_endpoint_resolution/lost_found_endpoint_resolution.puml @@ -11,17 +11,9 @@ ' SPDX-License-Identifier: Apache-2.0 ' ******************************************************************************* -@startuml official_lost_found_endpoint_variants +@startuml lost_found_endpoint_resolution -participant Actor1 -participant Actor2 -participant Builder - -[o-> Actor1 : Incoming request -[-> Actor2 : Another incoming -[x-> Builder : Third incoming -Actor1 -->o] : Outgoing response -Actor2 -->] : Another outgoing -Actor2 -->x] : Third outgoing +[ -> Service : incoming +Service --> ] : outgoing @enduml diff --git a/plantuml/parser/integration_test/sequence_diagram/lost_found_endpoint_resolution/output.json b/plantuml/parser/integration_test/sequence_diagram/lost_found_endpoint_resolution/output.json new file mode 100644 index 00000000..8b660c88 --- /dev/null +++ b/plantuml/parser/integration_test/sequence_diagram/lost_found_endpoint_resolution/output.json @@ -0,0 +1,48 @@ +{ + "lost_found_endpoint_resolution.puml": { + "name": "lost_found_endpoint_resolution", + "participants": [ + { + "display_name": "Service", + "alias": null, + "participant_type": "Participant", + "source_location": { + "file": "", + "line": 16 + }, + "stereotype": null + } + ], + "root_interactions": [ + { + "event": { + "Interaction": { + "caller": "ExternalEndpoint", + "callee": "Service", + "method": "incoming" + } + }, + "source_location": { + "file": "", + "line": 16 + }, + "branches_node": [ + { + "event": { + "Return": { + "caller": "ExternalEndpoint", + "callee": "Service", + "return_content": "outgoing" + } + }, + "source_location": { + "file": "", + "line": 17 + }, + "branches_node": [] + } + ] + } + ] + } +} diff --git a/plantuml/parser/integration_test/sequence_diagram/official_lost_found_endpoint_variants/output.json b/plantuml/parser/integration_test/sequence_diagram/official_lost_found_endpoint_variants/output.json deleted file mode 100644 index 9eafe08a..00000000 --- a/plantuml/parser/integration_test/sequence_diagram/official_lost_found_endpoint_variants/output.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "official_lost_found_endpoint_variants.puml": { - "name": "official_lost_found_endpoint_variants", - "participants": [ - { - "display_name": "Actor1", - "alias": null, - "participant_type": "Participant", - "source_location": { - "file": "", - "line": 16 - }, - "stereotype": null - }, - { - "display_name": "Actor2", - "alias": null, - "participant_type": "Participant", - "source_location": { - "file": "", - "line": 17 - }, - "stereotype": null - }, - { - "display_name": "Builder", - "alias": null, - "participant_type": "Participant", - "source_location": { - "file": "", - "line": 18 - }, - "stereotype": null - } - ], - "root_interactions": [ - { - "event": { - "Interaction": { - "caller": "ExternalEndpoint", - "callee": "Actor1", - "method": "Incoming request" - } - }, - "source_location": { - "file": "", - "line": 20 - }, - "branches_node": [ - { - "event": { - "Interaction": { - "caller": "ExternalEndpoint", - "callee": "Actor2", - "method": "Another incoming" - } - }, - "source_location": { - "file": "", - "line": 21 - }, - "branches_node": [ - { - "event": { - "Interaction": { - "caller": "ExternalEndpoint", - "callee": "Builder", - "method": "Third incoming" - } - }, - "source_location": { - "file": "", - "line": 22 - }, - "branches_node": [ - { - "event": { - "Return": { - "caller": "ExternalEndpoint", - "callee": "Actor1", - "return_content": "Outgoing response" - } - }, - "source_location": { - "file": "", - "line": 23 - }, - "branches_node": [] - }, - { - "event": { - "Return": { - "caller": "ExternalEndpoint", - "callee": "Actor2", - "return_content": "Another outgoing" - } - }, - "source_location": { - "file": "", - "line": 24 - }, - "branches_node": [] - }, - { - "event": { - "Return": { - "caller": "ExternalEndpoint", - "callee": "Actor2", - "return_content": "Third outgoing" - } - }, - "source_location": { - "file": "", - "line": 25 - }, - "branches_node": [] - } - ] - } - ] - } - ] - } - ] - } -} diff --git a/plantuml/parser/integration_test/sequence_diagram/participant_identifier_examples/output.json b/plantuml/parser/integration_test/sequence_diagram/participant_identifier_examples/output.json index fff2a905..64a148f5 100644 --- a/plantuml/parser/integration_test/sequence_diagram/participant_identifier_examples/output.json +++ b/plantuml/parser/integration_test/sequence_diagram/participant_identifier_examples/output.json @@ -3,128 +3,63 @@ "name": "participant_identifier_examples", "participants": [ { - "display_name": "Quoted As Id", - "alias": "QuotedAsId", + "display_name": "Display Service", + "alias": "Service", "participant_type": "Participant", "source_location": { "file": "", - "line": 17 - }, - "stereotype": "component" - }, - { - "display_name": "Id As Quoted", - "alias": "IdAsQuoted", - "participant_type": "Participant", - "source_location": { - "file": "", - "line": 20 + "line": 16 }, "stereotype": "component" }, { - "display_name": "LeftId", - "alias": "RightId", - "participant_type": "Participant", - "source_location": { - "file": "", - "line": 23 - }, - "stereotype": "component" - }, - { - "display_name": "Quoted Only", + "display_name": "Caller", "alias": null, "participant_type": "Participant", "source_location": { "file": "", - "line": 26 + "line": 17 }, - "stereotype": "component" + "stereotype": null }, { - "display_name": "IdOnly", + "display_name": "Display Service", "alias": null, "participant_type": "Participant", "source_location": { "file": "", - "line": 29 + "line": 22 }, - "stereotype": "component" + "stereotype": null } ], "root_interactions": [ { "event": { "Interaction": { - "caller": "QuotedAsId", - "callee": "IdAsQuoted", - "method": "callQuotedAsId()" + "caller": "Caller", + "callee": "Service", + "method": "callByAlias()" } }, "source_location": { "file": "", - "line": 31 + "line": 19 }, "branches_node": [ { "event": { "Interaction": { - "caller": "IdAsQuoted", - "callee": "RightId", - "method": "callIdAsQuoted()" + "caller": "Service", + "callee": "Display Service", + "method": "callByDisplayName()" } }, "source_location": { "file": "", - "line": 33 + "line": 22 }, - "branches_node": [ - { - "event": { - "Interaction": { - "caller": "RightId", - "callee": "Quoted Only", - "method": "callIdAsId()" - } - }, - "source_location": { - "file": "", - "line": 35 - }, - "branches_node": [ - { - "event": { - "Interaction": { - "caller": "Quoted Only", - "callee": "IdOnly", - "method": "callQuoted()" - } - }, - "source_location": { - "file": "", - "line": 37 - }, - "branches_node": [ - { - "event": { - "Interaction": { - "caller": "IdOnly", - "callee": "QuotedAsId", - "method": "callId()" - } - }, - "source_location": { - "file": "", - "line": 39 - }, - "branches_node": [] - } - ] - } - ] - } - ] + "branches_node": [] } ] } diff --git a/plantuml/parser/integration_test/sequence_diagram/participant_identifier_examples/participant_identifier_examples.puml b/plantuml/parser/integration_test/sequence_diagram/participant_identifier_examples/participant_identifier_examples.puml index b27eb63d..db2971c2 100644 --- a/plantuml/parser/integration_test/sequence_diagram/participant_identifier_examples/participant_identifier_examples.puml +++ b/plantuml/parser/integration_test/sequence_diagram/participant_identifier_examples/participant_identifier_examples.puml @@ -13,29 +13,12 @@ @startuml participant_identifier_examples -' quoted_display_with_alias -participant "Quoted As Id" as QuotedAsId <> +participant "Display Service" as Service <> +participant Caller -' alias_with_quoted_display -participant IdAsQuoted as "Id As Quoted" <> - -' display_with_alias -participant LeftId as RightId <> - -' quoted_display -participant "Quoted Only" <> - -' alias_only -participant IdOnly <> - -QuotedAsId -> IdAsQuoted : callQuotedAsId() - -IdAsQuoted -> RightId : callIdAsQuoted() - -RightId -> "Quoted Only" : callIdAsId() - -"Quoted Only" -> IdOnly : callQuoted() - -IdOnly -> QuotedAsId : callId() +Caller -> Service : callByAlias() +' Note: "Display Service" is a new participant that is implicitly created by +' the message endpoint resolution(confirmed by Plantuml Preview). +Service -> "Display Service" : callByDisplayName() @enduml diff --git a/plantuml/parser/integration_test/src/test_error_view.rs b/plantuml/parser/integration_test/src/test_error_view.rs index a11f522f..c21f4cd9 100644 --- a/plantuml/parser/integration_test/src/test_error_view.rs +++ b/plantuml/parser/integration_test/src/test_error_view.rs @@ -345,13 +345,6 @@ impl ErrorView for ClassPumlResolverError { impl ErrorView for SequenceResolverError { fn project(&self, base_dir: &Path) -> ProjectedError { let _ = base_dir; - - match self { - SequenceResolverError::UndeclaredParticipant { name, role } => { - ProjectedError::new("UndeclaredParticipant") - .with_field("name", name.clone()) - .with_field("role", (*role).to_string()) - } - } + match *self {} } } diff --git a/plantuml/parser/puml_parser/src/grammar/sequence.pest b/plantuml/parser/puml_parser/src/grammar/sequence.pest index 46f0f467..4d59c2cf 100644 --- a/plantuml/parser/puml_parser/src/grammar/sequence.pest +++ b/plantuml/parser/puml_parser/src/grammar/sequence.pest @@ -168,30 +168,50 @@ sequence_arrow = { // Messages message = { - async_marker? ~ - message_participant? ~ sequence_arrow ~ message_participant? ~ - activation_marker? ~ sequence_description? + parallel_marker? ~ + message_endpoint ~ sequence_arrow ~ message_endpoint ~ + message_suffix? ~ sequence_description? } -async_marker = { "&" } -message_participant = { - lost_found_marker | - quoted_display_with_alias | - display_with_alias | - alias_with_quoted_display | - participant_ref | - quoted_string +parallel_marker = { "&" } +message_endpoint = { + lost_found_marker + | inline_participant +} + +inline_participant = { + quoted_display_with_alias + | alias_with_quoted_display + | quoted_display + | alias_only } -lost_found_marker = { "[" ~ ("o" | "x")? | ("o" | "x")? ~ ("[" | "]") } -participant_ref = { CNAME | quoted_string } -activation_marker = { "+"+ | "-"+ | "*"+ | "!"+ } +lost_found_marker = { + left_lost_found + | right_lost_found +} +// '[', '[o', '[x' +left_lost_found = { "[" ~ ("o" | "x")? } +// ']', 'o]', 'x]' +right_lost_found = { ("o" | "x")? ~ "]" } + +message_suffix = { + activate_suffix + | deactivate_suffix + | create_suffix + | destroy_suffix +} +activate_suffix = { "++" ~ color_spec? } +deactivate_suffix = { "--" } +create_suffix = { "**" } +destroy_suffix = { "!!" } // Short activation syntax activation_short = { participant_ref ~ ("++" | "--") } +participant_ref = { CNAME | quoted_string } // Return -return_cmd = { async_marker? ~ ^"return" ~ sequence_text_content? } +return_cmd = { parallel_marker? ~ ^"return" ~ sequence_text_content? } // Box box_start = { "box" ~ quoted_string? ~ color_spec? } @@ -199,7 +219,7 @@ box_end = { ^"end" ~ WHITESPACE? ~ ^"box" } // Group commands (alt, opt, loop, etc.) group_cmd = { - async_marker? ~ group_type ~ group_condition? + parallel_marker? ~ group_type ~ group_condition? } group_condition = @{ (!EOL ~ ANY)+ } diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs index d53a69e2..58bcda12 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs @@ -15,26 +15,9 @@ pub mod sequence_ast; mod sequence_parser; pub use sequence_ast::{ - ActivateCmd, Arrow, CreateCmd, DeactivateCmd, DestroyCmd, ExternalEndpoint, GroupCmd, - GroupType, Message, MessageContent, ParticipantDef, ParticipantIdentifier, ParticipantType, - SeqPumlDocument, Statement, + ActivateCmd, Arrow, CreateCmd, DeactivateCmd, DestroyCmd, GroupCmd, GroupType, Message, + MessageEndpoint, MessageSuffix, ParticipantIdentifier, ParticipantType, SeqPumlDocument, + Statement, }; pub use sequence_parser::{PumlSequenceParser, SequenceError}; - -/// Parse a PlantUML sequence diagram and return the document name and statements -/// This is a convenience function for backwards compatibility with tests -pub fn parse_sequence_diagram( - input: &str, -) -> Result<(Option, Vec), Box> { - use parser_core::DiagramParser; - use puml_utils::LogLevel; - use std::path::PathBuf; - use std::rc::Rc; - - let mut parser = PumlSequenceParser; - let dummy_path = Rc::new(PathBuf::from("")); - let document = parser.parse_file(&dummy_path, input, LogLevel::Error)?; - - Ok((document.name, document.statements)) -} diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs index 70cc2682..099233f7 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs @@ -14,7 +14,6 @@ use serde::{Deserialize, Serialize}; use source_location::SourceLocation; -use std::str::FromStr; pub use parser_core::common_ast::Arrow; @@ -27,6 +26,9 @@ pub struct SeqPumlDocument { // Statement types used during parsing #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +// Keep `Message` unboxed for now because the statement representation is +// expected to be revisited as the sequence parser/resolver model settles. +#[allow(clippy::large_enum_variant)] pub enum Statement { DestroyCmd(DestroyCmd), CreateCmd(CreateCmd), @@ -48,26 +50,6 @@ pub struct ParticipantDef { pub source_location: SourceLocation, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct ExternalEndpoint; - -impl FromStr for ExternalEndpoint { - type Err = (); - - fn from_str(value: &str) -> Result { - match value { - "[" | "[o" | "[x" | "]" | "o]" | "x]" => Ok(ExternalEndpoint), - _ => Err(()), - } - } -} - -impl ExternalEndpoint { - pub fn as_name(self) -> &'static str { - "ExternalEndpoint" - } -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ParticipantType { Participant, @@ -111,20 +93,26 @@ pub struct DeactivateCmd { // Messages (internal parsing structure) #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Message { - pub content: MessageContent, - #[serde(skip_serializing_if = "Option::is_none")] - pub activation_marker: Option, + pub left: MessageEndpoint, + pub arrow: Arrow, + pub right: MessageEndpoint, + pub suffix: Option, pub description: Option, pub source_location: SourceLocation, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum MessageContent { - WithTargets { - left: String, - arrow: Arrow, - right: String, - }, +pub enum MessageEndpoint { + Participant(ParticipantIdentifier), + LostFound(String), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum MessageSuffix { + Activate, // ++ + Deactivate, // -- + Create, // ** + Destroy, // !! } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs index bab79d0d..21487db0 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs @@ -243,28 +243,28 @@ impl PumlSequenceParser { pair: pest::iterators::Pair, source_location: SourceLocation, ) -> Result { - let mut left: Option = None; + let mut left: Option = None; let mut arrow: Option = None; - let mut right: Option = None; - let mut activation_marker: Option = None; + let mut right: Option = None; + let mut suffix: Option = None; let mut description: Option = None; for inner in pair.into_inner() { match inner.as_rule() { - Rule::message_participant => { - let participant = Self::extract_participant_ref(inner); + Rule::message_endpoint => { + let endpoint = Self::parse_message_endpoint(inner)?; // First participant goes to left, second to right if arrow.is_none() { - left = Some(participant); + left = Some(endpoint); } else { - right = Some(participant); + right = Some(endpoint); } } Rule::sequence_arrow => { arrow = Some(Self::parse_arrow(inner)?); } - Rule::activation_marker => { - activation_marker = Some(inner.as_str().to_string()); + Rule::message_suffix => { + suffix = Some(Self::parse_message_suffix(inner)); } Rule::sequence_description => { description = inner @@ -276,22 +276,73 @@ impl PumlSequenceParser { } } - let content = MessageContent::WithTargets { - left: left.unwrap_or_default(), + Ok(Message { + left: left.expect("message must contain left endpoint"), arrow: arrow.ok_or_else(|| { SequenceError::InvalidStatement("missing arrow in message".to_string()) })?, - right: right.unwrap_or_default(), - }; - - Ok(Message { - content, - activation_marker, + right: right.expect("message must contain right endpoint"), + suffix, description, source_location, }) } + fn parse_message_endpoint( + pair: pest::iterators::Pair, + ) -> Result { + let endpoint = pair + .into_inner() + .next() + .expect("message_endpoint must contain an endpoint"); + + Ok(match endpoint.as_rule() { + Rule::inline_participant => { + MessageEndpoint::Participant(Self::parse_participant_identifier(endpoint)?) + } + Rule::lost_found_marker => { + MessageEndpoint::LostFound(Self::parse_lost_found_marker(endpoint)) + } + _ => unreachable!( + "message_endpoint grammar produced unsupported value: {:?}", + endpoint.as_rule() + ), + }) + } + + fn parse_lost_found_marker(pair: pest::iterators::Pair) -> String { + let marker = pair + .into_inner() + .next() + .expect("lost_found_marker must contain a lost-found endpoint"); + + match marker.as_rule() { + Rule::left_lost_found | Rule::right_lost_found => marker.as_str().trim().to_string(), + _ => unreachable!( + "lost_found_marker grammar produced unsupported value: {:?}", + marker.as_rule() + ), + } + } + + fn parse_message_suffix(pair: pest::iterators::Pair) -> MessageSuffix { + let suffix = pair + .into_inner() + .next() + .expect("message_suffix must contain a suffix"); + + match suffix.as_rule() { + Rule::activate_suffix => MessageSuffix::Activate, + Rule::deactivate_suffix => MessageSuffix::Deactivate, + Rule::create_suffix => MessageSuffix::Create, + Rule::destroy_suffix => MessageSuffix::Destroy, + _ => unreachable!( + "message_suffix grammar produced unsupported value: {:?}", + suffix.as_rule() + ), + } + } + fn parse_arrow(pair: pest::iterators::Pair) -> Result { common_parse_arrow(pair) .map_err(|e| SequenceError::InvalidStatement(format!("invalid arrow: {}", e))) @@ -433,12 +484,6 @@ impl PumlSequenceParser { fn extract_participant_ref(pair: pest::iterators::Pair) -> String { match pair.as_rule() { - Rule::message_participant => pair - .into_inner() - .next() - .map(Self::extract_participant_ref) - .unwrap_or_default(), - Rule::participant_ref => { let fallback = pair.as_str().trim(); diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs b/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs index 971a1b88..556c0862 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs @@ -65,3 +65,13 @@ fn run_sequence_diagram_parser_case(case_name: &str) { fn test_participant_identifiers() { run_sequence_diagram_parser_case("participant_identifiers"); } + +#[test] +fn test_message_participants() { + run_sequence_diagram_parser_case("message_participants"); +} + +#[test] +fn test_message_lost_found() { + run_sequence_diagram_parser_case("message_lost_found"); +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/message_lost_found.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/message_lost_found.puml new file mode 100644 index 00000000..f3f46416 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/message_lost_found.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml message_lost_found + +[ -> Service : left lost bracket +[o -> Service ++ : left lost bracket o +[x -> Service ** : left lost bracket x +Worker -> ] : right lost bracket +Worker -> o] -- : right lost o bracket +Worker -> x] !! : right lost x bracket + +@enduml diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/output.json new file mode 100644 index 00000000..fc34b94d --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/output.json @@ -0,0 +1,181 @@ +{ + "message_lost_found.puml": { + "name": "message_lost_found", + "statements": [ + { + "Message": { + "left": { + "LostFound": "[" + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Service", + "alias": null + } + }, + "suffix": null, + "description": "left lost bracket", + "source_location": { + "file": "", + "line": 15 + } + } + }, + { + "Message": { + "left": { + "LostFound": "[o" + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Service", + "alias": null + } + }, + "suffix": "Activate", + "description": "left lost bracket o", + "source_location": { + "file": "", + "line": 16 + } + } + }, + { + "Message": { + "left": { + "LostFound": "[x" + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Service", + "alias": null + } + }, + "suffix": "Create", + "description": "left lost bracket x", + "source_location": { + "file": "", + "line": 17 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Worker", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "LostFound": "]" + }, + "suffix": null, + "description": "right lost bracket", + "source_location": { + "file": "", + "line": 18 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Worker", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "LostFound": "o]" + }, + "suffix": "Deactivate", + "description": "right lost o bracket", + "source_location": { + "file": "", + "line": 19 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Worker", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "LostFound": "x]" + }, + "suffix": "Destroy", + "description": "right lost x bracket", + "source_location": { + "file": "", + "line": 20 + } + } + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/message_participants/message_participants.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/message_participants/message_participants.puml new file mode 100644 index 00000000..9d522073 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/message_participants/message_participants.puml @@ -0,0 +1,24 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml message_participants + +"Quoted Display" as QuotedAlias -> Target ++#red : quoted display with alias +AliasQuoted as "Alias Quoted Display" -> Target ** : alias with quoted display +"Quoted Only" -> Target !! : quoted display +AliasOnly --> Target -- : alias only +"DisplayName" as DisplayAlias -> "Quoted Display" as QuotedAlias + +' Not supported by preview +' DisplayName as DisplayAlias -> Target -- : display with alias + +@enduml diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/message_participants/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/message_participants/output.json new file mode 100644 index 00000000..5ceec90a --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/message_participants/output.json @@ -0,0 +1,167 @@ +{ + "message_participants.puml": { + "name": "message_participants", + "statements": [ + { + "Message": { + "left": { + "Participant": { + "display_name": "Quoted Display", + "alias": "QuotedAlias" + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Target", + "alias": null + } + }, + "suffix": "Activate", + "description": "quoted display with alias", + "source_location": { + "file": "", + "line": 15 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Alias Quoted Display", + "alias": "AliasQuoted" + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Target", + "alias": null + } + }, + "suffix": "Create", + "description": "alias with quoted display", + "source_location": { + "file": "", + "line": 16 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Quoted Only", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Target", + "alias": null + } + }, + "suffix": "Destroy", + "description": "quoted display", + "source_location": { + "file": "", + "line": 17 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "AliasOnly", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Target", + "alias": null + } + }, + "suffix": "Deactivate", + "description": "alias only", + "source_location": { + "file": "", + "line": 18 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "DisplayName", + "alias": "DisplayAlias" + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Quoted Display", + "alias": "QuotedAlias" + } + }, + "suffix": null, + "description": null, + "source_location": { + "file": "", + "line": 19 + } + } + } + ] + } +} diff --git a/plantuml/parser/puml_resolver/src/sequence_diagram/BUILD b/plantuml/parser/puml_resolver/src/sequence_diagram/BUILD index 038b6306..e181a2ee 100644 --- a/plantuml/parser/puml_resolver/src/sequence_diagram/BUILD +++ b/plantuml/parser/puml_resolver/src/sequence_diagram/BUILD @@ -10,7 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") rust_library( name = "puml_resolver_sequence", @@ -38,19 +38,6 @@ rust_test( crate = ":puml_resolver_sequence", ) -# Job 2: Build hierarchical tree from syntax JSON -rust_binary( - name = "logic_parse", - srcs = ["src/logic_parse_main.rs"], - visibility = ["//visibility:public"], - deps = [ - ":puml_resolver_sequence", - "//plantuml/parser/puml_parser:sequence_diagram", - "@crates//:serde", - "@crates//:serde_json", - ], -) - rust_test( name = "sequence_resolver_it", srcs = ["tests/sequence_resolver_test.rs"], diff --git a/plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parse_main.rs b/plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parse_main.rs deleted file mode 100644 index 7db33158..00000000 --- a/plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parse_main.rs +++ /dev/null @@ -1,70 +0,0 @@ -// ******************************************************************************* -// Copyright (c) 2026 Contributors to the Eclipse Foundation -// -// See the NOTICE file(s) distributed with this work for additional -// information regarding copyright ownership. -// -// This program and the accompanying materials are made available under the -// terms of the Apache License Version 2.0 which is available at -// -// -// SPDX-License-Identifier: Apache-2.0 -// ******************************************************************************* - -//! Logic parser job: Build hierarchical tree from syntax JSON - -use sequence_parser::sequence_ast::Statement; -use sequence_resolver::logic_parser::build_tree; - -use std::env; -use std::fs; - -fn main() { - let args: Vec = env::args().collect(); - - if args.len() < 3 { - eprintln!("Usage: {} ", args[0]); - std::process::exit(1); - } - - let input_file = &args[1]; - let output_file = &args[2]; - - // Read the syntax JSON file - let json_content = match fs::read_to_string(input_file) { - Ok(content) => content, - Err(e) => { - eprintln!("Error reading file '{}': {}", input_file, e); - std::process::exit(1); - } - }; - - // Deserialize the statements - let statements: Vec = match serde_json::from_str(&json_content) { - Ok(stmts) => stmts, - Err(e) => { - eprintln!("Error parsing JSON: {}", e); - std::process::exit(1); - } - }; - - // Build the logic tree - let tree = build_tree(&statements); - - // Serialize to JSON - let json_output = match serde_json::to_string_pretty(&tree) { - Ok(json) => json, - Err(e) => { - eprintln!("Error serializing to JSON: {}", e); - std::process::exit(1); - } - }; - - // Write to output file - if let Err(e) = fs::write(output_file, json_output) { - eprintln!("Error writing to file '{}': {}", output_file, e); - std::process::exit(1); - } - - println!("✓ Logic tree built: {} nodes → {}", tree.len(), output_file); -} diff --git a/plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parser.rs b/plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parser.rs index 7617fa1c..ddd92617 100644 --- a/plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parser.rs +++ b/plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parser.rs @@ -15,11 +15,16 @@ use sequence_logic::*; use sequence_parser::*; -fn map_special_endpoint_name(value: String) -> String { - if let Ok(endpoint) = value.parse::() { - endpoint.as_name().to_string() - } else { - value +const EXTERNAL_ENDPOINT_NAME: &str = "ExternalEndpoint"; + +fn endpoint_name(endpoint: &MessageEndpoint) -> String { + match endpoint { + MessageEndpoint::Participant(identifier) => identifier + .alias + .as_deref() + .unwrap_or(&identifier.display_name) + .to_string(), + MessageEndpoint::LostFound(_) => EXTERNAL_ENDPOINT_NAME.to_string(), } } @@ -244,57 +249,47 @@ fn build_node(statements: &[Statement]) -> Option<(SequenceNode, usize)> { /// Convert a message statement to an Event (Interaction or Return) fn message_to_event(msg: &Message) -> Option { - match &msg.content { - MessageContent::WithTargets { left, arrow, right } => { - let method = msg.description.clone().unwrap_or_default(); - - // Check if arrow left decorator points left (reverse arrow like <--) - let is_reverse = arrow - .left - .as_ref() - .map(|d| d.raw.contains("<")) - .unwrap_or(false); - - // Determine actual caller and callee based on arrow direction - let (actual_from, actual_to) = if is_reverse { - // Arrow points left: from right participant to left participant - // "A <-- B" means B sends to A - (right.clone(), left.clone()) - } else { - // Arrow points right: from left participant to right participant - // "A -> B" means A sends to B - (left.clone(), right.clone()) - }; - - let actual_from = map_special_endpoint_name(actual_from); - let actual_to = map_special_endpoint_name(actual_to); - - // Check arrow type to determine Interaction vs Return - if is_return_arrow_from_arrow(arrow) { - // For returns: actual_from is the sender (callee), actual_to is the receiver (caller) - Some(Event::Return(Return { - caller: actual_to, - callee: actual_from, - return_content: method, - })) - } else { - Some(Event::Interaction(Interaction { - caller: actual_from, - callee: actual_to, - method, - })) - } - } // Note: MessageContent only has WithTargets. Incomplete arrows (missing - // left or right participant) are represented as WithTargets with an empty - // string for the missing side, so no separate variant is needed. + let method = msg.description.clone().unwrap_or_default(); + + // Check if arrow left decorator points left (reverse arrow like <--) + let is_reverse = msg + .arrow + .left + .as_ref() + .map(|d| d.raw.contains("<")) + .unwrap_or(false); + + // Determine actual caller and callee based on arrow direction. + let (actual_from, actual_to) = if is_reverse { + // Arrow points left: from right participant to left participant. + // "A <-- B" means B sends to A. + (endpoint_name(&msg.right), endpoint_name(&msg.left)) + } else { + // Arrow points right: from left participant to right participant. + // "A -> B" means A sends to B. + (endpoint_name(&msg.left), endpoint_name(&msg.right)) + }; + + // Check arrow type to determine Interaction vs Return. + if is_return_arrow_from_arrow(&msg.arrow) { + // For returns: actual_from is the sender (callee), actual_to is the receiver (caller). + Some(Event::Return(Return { + caller: actual_to, + callee: actual_from, + return_content: method, + })) + } else { + Some(Event::Interaction(Interaction { + caller: actual_from, + callee: actual_to, + method, + })) } } /// Check if a message represents a return arrow fn is_return_arrow(msg: &Message) -> bool { - match &msg.content { - MessageContent::WithTargets { arrow, .. } => is_return_arrow_from_arrow(arrow), - } + is_return_arrow_from_arrow(&msg.arrow) } /// Check if an arrow represents a return (dashed arrow) diff --git a/plantuml/parser/puml_resolver/src/sequence_diagram/src/sequence_resolver.rs b/plantuml/parser/puml_resolver/src/sequence_diagram/src/sequence_resolver.rs index ff7b38ae..f70be12e 100644 --- a/plantuml/parser/puml_resolver/src/sequence_diagram/src/sequence_resolver.rs +++ b/plantuml/parser/puml_resolver/src/sequence_diagram/src/sequence_resolver.rs @@ -13,13 +13,16 @@ use crate::logic_parser::build_tree; use resolver_traits::DiagramResolver; -use sequence_logic::{ParticipantType as LogicParticipantType, SequenceParticipant, SequenceTree}; +use sequence_logic::{ + ParticipantType as LogicParticipantType, SequenceParticipant, SequenceTree, SourceLocation, +}; use sequence_parser::sequence_ast::{ - ExternalEndpoint, MessageContent, ParticipantIdentifier, + MessageEndpoint, ParticipantDef, ParticipantIdentifier, ParticipantType as SyntaxParticipantType, Statement, }; use sequence_parser::SeqPumlDocument; use std::collections::HashSet; +use std::fmt; /// Resolver for sequence diagrams. /// @@ -30,23 +33,135 @@ pub struct SequenceResolver; /// Error type for `SequenceResolver`. #[derive(Debug)] -pub enum SequenceResolverError { - /// A message references a participant that was not declared in a - /// `participant` (or actor/boundary/…) statement. - UndeclaredParticipant { name: String, role: &'static str }, +pub enum SequenceResolverError {} + +impl fmt::Display for SequenceResolverError { + fn fmt(&self, _formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self {} + } +} + +impl std::error::Error for SequenceResolverError {} + +impl DiagramResolver for SequenceResolver { + type Document = SeqPumlDocument; + type Output = SequenceTree; + type Error = SequenceResolverError; + + fn resolve(&mut self, document: &SeqPumlDocument) -> Result { + let participants = build_participant_table(&document.statements); + let root_interactions = build_tree(&document.statements); + + Ok(SequenceTree { + name: document.name.clone(), + participants, + root_interactions, + }) + } } -impl std::fmt::Display for SequenceResolverError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SequenceResolverError::UndeclaredParticipant { name, role } => { - write!(f, "{role} '{name}' is not declared as a participant") - } +fn build_participant_table(statements: &[Statement]) -> Vec { + let mut resolved_names = HashSet::new(); + let mut participants = Vec::new(); + + add_explicit_participants(statements, &mut participants, &mut resolved_names); + add_implicit_participants(statements, &mut participants, &mut resolved_names); + + participants +} + +fn add_explicit_participants( + statements: &[Statement], + participants: &mut Vec, + resolved_names: &mut HashSet, +) { + for stmt in statements { + if let Statement::ParticipantDef(participant_def) = stmt { + add_participant( + participants, + resolved_names, + explicit_participant(participant_def), + ); } } } -impl std::error::Error for SequenceResolverError {} +fn add_implicit_participants( + statements: &[Statement], + participants: &mut Vec, + resolved_names: &mut HashSet, +) { + for stmt in statements { + if let Statement::Message(msg) = stmt { + add_endpoint_participant( + participants, + resolved_names, + &msg.left, + &msg.source_location, + ); + add_endpoint_participant( + participants, + resolved_names, + &msg.right, + &msg.source_location, + ); + } + } +} + +fn add_participant( + participants: &mut Vec, + resolved_names: &mut HashSet, + participant: SequenceParticipant, +) { + let identifier = ParticipantIdentifier { + display_name: participant.display_name.clone(), + alias: participant.alias.clone(), + }; + let reference_name = participant_reference_name(&identifier); + if reference_name.is_empty() || !resolved_names.insert(reference_name.to_string()) { + return; + } + participants.push(participant); +} + +fn add_endpoint_participant( + participants: &mut Vec, + resolved_names: &mut HashSet, + endpoint: &MessageEndpoint, + source_location: &SourceLocation, +) { + if let MessageEndpoint::Participant(identifier) = endpoint { + add_participant( + participants, + resolved_names, + implicit_participant(identifier, source_location), + ); + } +} + +fn explicit_participant(participant_def: &ParticipantDef) -> SequenceParticipant { + SequenceParticipant { + display_name: participant_def.identifier.display_name.clone(), + alias: participant_def.identifier.alias.clone(), + participant_type: map_parser_participant_type(&participant_def.participant_type), + source_location: participant_def.source_location.clone(), + stereotype: participant_def.stereotype.clone(), + } +} + +fn implicit_participant( + identifier: &ParticipantIdentifier, + source_location: &SourceLocation, +) -> SequenceParticipant { + SequenceParticipant { + display_name: identifier.display_name.clone(), + alias: identifier.alias.clone(), + participant_type: LogicParticipantType::Participant, + source_location: source_location.clone(), + stereotype: None, + } +} fn map_parser_participant_type(kind: &SyntaxParticipantType) -> LogicParticipantType { match kind { @@ -61,10 +176,6 @@ fn map_parser_participant_type(kind: &SyntaxParticipantType) -> LogicParticipant } } -fn is_special_endpoint_marker(name: &str) -> bool { - name.parse::().is_ok() -} - fn participant_reference_name(identifier: &ParticipantIdentifier) -> &str { identifier .alias @@ -72,65 +183,6 @@ fn participant_reference_name(identifier: &ParticipantIdentifier) -> &str { .unwrap_or(&identifier.display_name) } -impl DiagramResolver for SequenceResolver { - type Document = SeqPumlDocument; - type Output = SequenceTree; - type Error = SequenceResolverError; - - fn resolve(&mut self, document: &SeqPumlDocument) -> Result { - // 1. Collect declared participants. - let mut declared = HashSet::new(); - let mut participants = Vec::new(); - for stmt in &document.statements { - if let Statement::ParticipantDef(p) = stmt { - declared.insert(participant_reference_name(&p.identifier).to_string()); - participants.push(SequenceParticipant { - display_name: p.identifier.display_name.clone(), - alias: p.identifier.alias.clone(), - participant_type: map_parser_participant_type(&p.participant_type), - source_location: p.source_location.clone(), - stereotype: p.stereotype.clone(), - }); - } - } - - // 2. Validate message targets only when participants are declared. - if !declared.is_empty() { - for stmt in &document.statements { - if let Statement::Message(msg) = stmt { - let MessageContent::WithTargets { left, right, .. } = &msg.content; - if !left.is_empty() - && !is_special_endpoint_marker(left) - && !declared.contains(left) - { - return Err(SequenceResolverError::UndeclaredParticipant { - name: left.clone(), - role: "caller", - }); - } - if !right.is_empty() - && !is_special_endpoint_marker(right) - && !declared.contains(right) - { - return Err(SequenceResolverError::UndeclaredParticipant { - name: right.clone(), - role: "callee", - }); - } - } - } - } - - // 3. Build the tree. - let root_interactions = build_tree(&document.statements); - Ok(SequenceTree { - name: document.name.clone(), - participants, - root_interactions, - }) - } -} - #[cfg(test)] mod sequence_resolver_tests { use super::*; @@ -138,7 +190,7 @@ mod sequence_resolver_tests { use resolver_traits::DiagramResolver; use sequence_logic::SourceLocation; use sequence_parser::sequence_ast::{ - Message, MessageContent, ParticipantDef, ParticipantIdentifier, + Message, MessageEndpoint, ParticipantDef, ParticipantIdentifier, ParticipantType as SyntaxParticipantType, Statement, }; @@ -172,14 +224,19 @@ mod sequence_resolver_tests { SourceLocation::new("test.puml", 0) } + fn message_endpoint(name: &str) -> MessageEndpoint { + MessageEndpoint::Participant(ParticipantIdentifier { + display_name: name.to_string(), + alias: None, + }) + } + fn make_call(from: &str, to: &str, label: &str) -> Statement { Statement::Message(Message { - content: MessageContent::WithTargets { - left: from.to_string(), - arrow: solid_arrow(), - right: to.to_string(), - }, - activation_marker: None, + left: message_endpoint(from), + arrow: solid_arrow(), + right: message_endpoint(to), + suffix: None, description: Some(label.to_string()), source_location: dummy_source_location(), }) @@ -187,12 +244,10 @@ mod sequence_resolver_tests { fn make_return(from: &str, to: &str, label: &str) -> Statement { Statement::Message(Message { - content: MessageContent::WithTargets { - left: from.to_string(), - arrow: dashed_arrow(), - right: to.to_string(), - }, - activation_marker: None, + left: message_endpoint(from), + arrow: dashed_arrow(), + right: message_endpoint(to), + suffix: None, description: Some(label.to_string()), source_location: dummy_source_location(), }) @@ -284,9 +339,10 @@ mod sequence_resolver_tests { }) } - /// When participants are declared, all message targets must be among them. + /// Explicit participants remain in the symbol table and message references + /// to them do not create duplicates. #[test] - fn test_declared_participants_pass_validation() { + fn test_declared_participants_are_preserved_without_duplicates() { let stmts = vec![ make_participant("A"), make_participant("B"), @@ -298,11 +354,14 @@ mod sequence_resolver_tests { name: Some("valid".to_string()), statements: stmts, }; - assert!(resolver.resolve(&doc).is_ok()); + let tree = resolver.resolve(&doc).expect("must not fail"); + assert_eq!(tree.participants.len(), 2); + assert_eq!(tree.participants[0].display_name, "A"); + assert_eq!(tree.participants[1].display_name, "B"); } #[test] - fn test_aliased_participant_reference_passes_validation() { + fn test_aliased_participant_reference_does_not_create_duplicate() { let stmts = vec![ make_participant("A"), make_participant_with_alias("Display B", "B"), @@ -313,11 +372,14 @@ mod sequence_resolver_tests { name: Some("valid_alias".to_string()), statements: stmts, }; - assert!(resolver.resolve(&doc).is_ok()); + let tree = resolver.resolve(&doc).expect("must not fail"); + assert_eq!(tree.participants.len(), 2); + assert_eq!(tree.participants[1].display_name, "Display B"); + assert_eq!(tree.participants[1].alias.as_deref(), Some("B")); } #[test] - fn test_aliased_participant_display_name_reference_raises_error() { + fn test_aliased_participant_display_name_reference_creates_implicit_participant() { let stmts = vec![ make_participant("A"), make_participant_with_alias("Display B", "B"), @@ -328,58 +390,25 @@ mod sequence_resolver_tests { name: Some("invalid_display_reference".to_string()), statements: stmts, }; - let err = resolver.resolve(&doc).unwrap_err(); - let msg = err.to_string(); - assert!(msg.contains("Display B")); - assert!(msg.contains("callee")); - } - - /// An undeclared callee should cause an error. - #[test] - fn test_undeclared_callee_raises_error() { - let stmts = vec![make_participant("A"), make_call("A", "B", "doWork")]; - let mut resolver = SequenceResolver; - let doc = SeqPumlDocument { - name: Some("bad_callee".to_string()), - statements: stmts, - }; - let err = resolver.resolve(&doc).unwrap_err(); - let msg = err.to_string(); - assert!( - msg.contains("B"), - "error should name the undeclared participant" - ); - assert!(msg.contains("callee"), "error should indicate the role"); - } - - /// An undeclared caller should cause an error. - #[test] - fn test_undeclared_caller_raises_error() { - let stmts = vec![make_participant("B"), make_call("A", "B", "doWork")]; - let mut resolver = SequenceResolver; - let doc = SeqPumlDocument { - name: Some("bad_caller".to_string()), - statements: stmts, - }; - let err = resolver.resolve(&doc).unwrap_err(); - let msg = err.to_string(); - assert!( - msg.contains("A"), - "error should name the undeclared participant" - ); - assert!(msg.contains("caller"), "error should indicate the role"); + let tree = resolver.resolve(&doc).expect("must not fail"); + assert_eq!(tree.participants.len(), 3); + assert_eq!(tree.participants[2].display_name, "Display B"); + assert_eq!(tree.participants[2].alias, None); } - /// When no participants are declared, messages are allowed freely (no validation). + /// When no participants are declared, message endpoints form the participant table. #[test] - fn test_no_participants_declared_skips_validation() { + fn test_no_participants_declared_creates_implicit_participants() { let stmts = vec![make_call("X", "Y", "hello")]; let mut resolver = SequenceResolver; let doc = SeqPumlDocument { name: Some("implicit".to_string()), statements: stmts, }; - assert!(resolver.resolve(&doc).is_ok()); + let tree = resolver.resolve(&doc).expect("must not fail"); + assert_eq!(tree.participants.len(), 2); + assert_eq!(tree.participants[0].display_name, "X"); + assert_eq!(tree.participants[1].display_name, "Y"); } /// Resolver output nodes must preserve source_location provenance. @@ -390,22 +419,18 @@ mod sequence_resolver_tests { let stmts = vec![ Statement::Message(Message { - content: MessageContent::WithTargets { - left: "A".to_string(), - arrow: solid_arrow(), - right: "B".to_string(), - }, - activation_marker: None, + left: message_endpoint("A"), + arrow: solid_arrow(), + right: message_endpoint("B"), + suffix: None, description: Some("doWork".to_string()), source_location: call_location.clone(), }), Statement::Message(Message { - content: MessageContent::WithTargets { - left: "B".to_string(), - arrow: dashed_arrow(), - right: "A".to_string(), - }, - activation_marker: None, + left: message_endpoint("B"), + arrow: dashed_arrow(), + right: message_endpoint("A"), + suffix: None, description: Some("result".to_string()), source_location: return_location.clone(), }), diff --git a/plantuml/parser/puml_resolver/src/sequence_diagram/tests/sequence_resolver_test.rs b/plantuml/parser/puml_resolver/src/sequence_diagram/tests/sequence_resolver_test.rs index 065745c5..98539621 100644 --- a/plantuml/parser/puml_resolver/src/sequence_diagram/tests/sequence_resolver_test.rs +++ b/plantuml/parser/puml_resolver/src/sequence_diagram/tests/sequence_resolver_test.rs @@ -72,11 +72,11 @@ fn test_participant_identifier_examples() { } #[test] -fn test_official_lost_found_endpoint_variants() { - run_sequence_resolver_case("official_lost_found_endpoint_variants"); +fn test_lost_found_endpoint_resolution() { + run_sequence_resolver_case("lost_found_endpoint_resolution"); } #[test] -fn test_invalid_undeclared_participant() { - run_sequence_resolver_case("invalid_undeclared_participant"); +fn test_implicit_participant_from_message_endpoint() { + run_sequence_resolver_case("implicit_participant_from_message_endpoint"); }