diff --git a/plantuml/parser/integration_test/component_diagram/relation_invalid_arrow_parsed_as_association/relation_invalid_arrow_parsed_as_association.puml b/plantuml/parser/integration_test/component_diagram/invalid_component_left_inverse_generic_lollipop/invalid_component_left_inverse_generic_lollipop.puml similarity index 87% rename from plantuml/parser/integration_test/component_diagram/relation_invalid_arrow_parsed_as_association/relation_invalid_arrow_parsed_as_association.puml rename to plantuml/parser/integration_test/component_diagram/invalid_component_left_inverse_generic_lollipop/invalid_component_left_inverse_generic_lollipop.puml index fbf1e346..117c682d 100644 --- a/plantuml/parser/integration_test/component_diagram/relation_invalid_arrow_parsed_as_association/relation_invalid_arrow_parsed_as_association.puml +++ b/plantuml/parser/integration_test/component_diagram/invalid_component_left_inverse_generic_lollipop/invalid_component_left_inverse_generic_lollipop.puml @@ -10,13 +10,13 @@ ' ' SPDX-License-Identifier: Apache-2.0 ' ******************************************************************************* -@startuml +@startuml invalid_component_left_inverse_generic_lollipop.puml package "Pkg" as Pkg { component "Component A" as ComponentA <> interface "IService" as IService - ComponentA --() IService : association + ComponentA ()-- IService : invalid } @enduml diff --git a/plantuml/parser/integration_test/component_diagram/invalid_component_left_inverse_generic_lollipop/output.yaml b/plantuml/parser/integration_test/component_diagram/invalid_component_left_inverse_generic_lollipop/output.yaml new file mode 100644 index 00000000..d9351aa8 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_component_left_inverse_generic_lollipop/output.yaml @@ -0,0 +1,19 @@ +# ******************************************************************************* +# 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_component_left_inverse_generic_lollipop.puml: + error: + type: "InvalidRelationship" + fields: + from: "ComponentA" + to: "IService" + reason: "Unsupported interface decorator syntax: only ')-' and '--()' (Provided) plus '-(' (Required) are supported; '()--' is rejected" diff --git a/plantuml/parser/integration_test/component_diagram/invalid_interface_decor_between_components/output.yaml b/plantuml/parser/integration_test/component_diagram/invalid_interface_decor_between_components/output.yaml index cbe08bee..2be4bac5 100644 --- a/plantuml/parser/integration_test/component_diagram/invalid_interface_decor_between_components/output.yaml +++ b/plantuml/parser/integration_test/component_diagram/invalid_interface_decor_between_components/output.yaml @@ -16,4 +16,4 @@ invalid_interface_decor_between_components.puml: fields: from: "A" to: "B" - reason: "Interface decorators '-(' and ')-' require exactly one Interface endpoint" + reason: "Interface decorators '-(', ')-', and '--()' require exactly one Interface endpoint" diff --git a/plantuml/parser/integration_test/component_diagram/invalid_interface_decor_between_interfaces/output.yaml b/plantuml/parser/integration_test/component_diagram/invalid_interface_decor_between_interfaces/output.yaml index b2891821..2c2c15ec 100644 --- a/plantuml/parser/integration_test/component_diagram/invalid_interface_decor_between_interfaces/output.yaml +++ b/plantuml/parser/integration_test/component_diagram/invalid_interface_decor_between_interfaces/output.yaml @@ -16,4 +16,4 @@ invalid_interface_decor_between_interfaces.puml: fields: from: "ILeft" to: "IRight" - reason: "Interface decorators '-(' and ')-' are not allowed between two interfaces" + reason: "Interface decorators '-(', ')-', and '--()' are not allowed between two interfaces" diff --git a/plantuml/parser/integration_test/component_diagram/invalid_interface_left_generic_lollipop/invalid_interface_left_generic_lollipop.puml b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_generic_lollipop/invalid_interface_left_generic_lollipop.puml new file mode 100644 index 00000000..b5c1ee15 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_generic_lollipop/invalid_interface_left_generic_lollipop.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 invalid_interface_left_generic_lollipop.puml + +package "Pkg" as Pkg { + interface "IProvided" as IProvided + component "A" as A <> + + IProvided --() A : invalid +} + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/invalid_interface_left_generic_lollipop/output.yaml b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_generic_lollipop/output.yaml new file mode 100644 index 00000000..11af695d --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_generic_lollipop/output.yaml @@ -0,0 +1,19 @@ +# ******************************************************************************* +# 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_interface_left_generic_lollipop.puml: + error: + type: "InvalidRelationship" + fields: + from: "IProvided" + to: "A" + reason: "Decorator binding requires a Component or component-stereotyped element on the left and Interface on the right" diff --git a/plantuml/parser/integration_test/component_diagram/invalid_interface_left_inverse_generic_lollipop/invalid_interface_left_inverse_generic_lollipop.puml b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_inverse_generic_lollipop/invalid_interface_left_inverse_generic_lollipop.puml new file mode 100644 index 00000000..dcb2e363 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_inverse_generic_lollipop/invalid_interface_left_inverse_generic_lollipop.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 invalid_interface_left_inverse_generic_lollipop.puml + +package "Pkg" as Pkg { + interface "IProvided" as IProvided + component "A" as A <> + + IProvided ()-- A : invalid +} + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/invalid_interface_left_inverse_generic_lollipop/output.yaml b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_inverse_generic_lollipop/output.yaml new file mode 100644 index 00000000..f5eaf198 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_inverse_generic_lollipop/output.yaml @@ -0,0 +1,19 @@ +# ******************************************************************************* +# 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_interface_left_inverse_generic_lollipop.puml: + error: + type: "InvalidRelationship" + fields: + from: "IProvided" + to: "A" + reason: "Unsupported interface decorator syntax: only ')-' and '--()' (Provided) plus '-(' (Required) are supported; '()--' is rejected" diff --git a/plantuml/parser/integration_test/component_diagram/invalid_interface_left_required_decorator/invalid_interface_left_required_decorator.puml b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_required_decorator/invalid_interface_left_required_decorator.puml new file mode 100644 index 00000000..e565c214 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_required_decorator/invalid_interface_left_required_decorator.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 + +package "Pkg" as Pkg { + interface "IReq" as IReq + component "A" as A <> + + IReq -( A : invalid +} + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/invalid_interface_left_required_decorator/output.yaml b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_required_decorator/output.yaml new file mode 100644 index 00000000..50ac5795 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_required_decorator/output.yaml @@ -0,0 +1,19 @@ +# ******************************************************************************* +# 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_interface_left_required_decorator.puml: + error: + type: "InvalidRelationship" + fields: + from: "IReq" + to: "A" + reason: "Decorator binding requires a Component or component-stereotyped element on the left and Interface on the right" diff --git a/plantuml/parser/integration_test/component_diagram/relation_invalid_arrow_parsed_as_association/output.json b/plantuml/parser/integration_test/component_diagram/relation_generic_lollipop_provided_to_component/output.json similarity index 86% rename from plantuml/parser/integration_test/component_diagram/relation_invalid_arrow_parsed_as_association/output.json rename to plantuml/parser/integration_test/component_diagram/relation_generic_lollipop_provided_to_component/output.json index 275736a5..7e3dbe5e 100644 --- a/plantuml/parser/integration_test/component_diagram/relation_invalid_arrow_parsed_as_association/output.json +++ b/plantuml/parser/integration_test/component_diagram/relation_generic_lollipop_provided_to_component/output.json @@ -1,5 +1,5 @@ { - "relation_invalid_arrow_parsed_as_association.puml": { + "relation_generic_lollipop_provided_to_component.puml": { "Pkg.ComponentA": { "id": "Pkg.ComponentA", "name": "Component A", @@ -10,9 +10,9 @@ "relations": [ { "target": "Pkg.IService", - "annotation": "association", - "relation_type": "Association", - "source_role": "None", + "annotation": "provides", + "relation_type": "InterfaceBinding", + "source_role": "Provided", "source_location": { "file": "", "line": 19 diff --git a/plantuml/parser/integration_test/component_diagram/relation_generic_lollipop_provided_to_component/relation_generic_lollipop_provided_to_component.puml b/plantuml/parser/integration_test/component_diagram/relation_generic_lollipop_provided_to_component/relation_generic_lollipop_provided_to_component.puml new file mode 100644 index 00000000..b011f907 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_generic_lollipop_provided_to_component/relation_generic_lollipop_provided_to_component.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 relation_generic_lollipop_provided_to_component.puml + +package "Pkg" as Pkg { + component "Component A" as ComponentA <> + interface "IService" as IService + + ComponentA --() IService : provides +} + +@enduml diff --git a/plantuml/parser/puml_resolver/src/component_diagram/src/component_relations.md b/plantuml/parser/puml_resolver/src/component_diagram/src/component_relations.md index 6f5cc9cd..0564c8fb 100644 --- a/plantuml/parser/puml_resolver/src/component_diagram/src/component_relations.md +++ b/plantuml/parser/puml_resolver/src/component_diagram/src/component_relations.md @@ -26,6 +26,7 @@ - Interface binding (component-left only): - Provided interface: - `Component )- Interface` + - `Component --() Interface` - Required interface: - `Component -( Interface` @@ -36,15 +37,19 @@ The following forms are rejected: - Interface )- Component + - accepted by PlantUML Preview, but rejected by the resolver - Interface -( Component + - accepted by PlantUML Preview, but rejected by the resolver +- Interface ()-- Component + - invalid in PlantUML Preview and rejected by the resolver +- Component ()-- Interface + - invalid in PlantUML Preview and rejected by the resolver +- Interface --() Component + - accepted by PlantUML Preview, but rejected by the resolver ### Generic lollipop decorators -The following forms are resolved as plain associations and do not carry interface-binding semantics: - - `Component --() Interface` - - `Interface ()-- Component` - -Note: Use canonical component-left forms such as `Component )- Interface` or `Component -( Interface` when you need interface binding behavior. +The generic lollipop form `Component --() Interface` is resolved as a provided interface binding. ### Resolver constraints diff --git a/plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs b/plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs index 7de1cdd7..58ec644d 100644 --- a/plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs +++ b/plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs @@ -452,14 +452,14 @@ impl ComponentResolver { } // Supported relation syntaxes: - // - Interface binding: `)-`, `-(` + // - Interface binding: `)-`, `-(`, `--()` // - Directed: `-->`, `<--`, `..>`, `<..` // - Undirected: `--`, `..` fn parse_arrow(relation: &Relation) -> Result { let (left, right, middle) = Self::arrow_parts(&relation.arrow); let line = relation.arrow.line.raw.as_str(); - let has_provided_token = left == ")"; + let has_provided_token = left == ")" || left == "()" || middle == "()"; let has_required_token = middle == "(" || right == "("; if has_provided_token && has_required_token { @@ -476,13 +476,17 @@ impl ComponentResolver { // of `"-"`. Direction is visual-only and does not affect semantics. let is_lollipop_line = line.chars().all(|c| c == '-') && !line.is_empty(); - let decor_role = if is_lollipop_line && left == ")" && middle.is_empty() && right.is_empty() - { - Some(EndpointRole::Provided) - } else if is_lollipop_line + let is_canonical_provided = + is_lollipop_line && left == ")" && middle.is_empty() && right.is_empty(); + let is_generic_lollipop_provided = + is_lollipop_line && left.is_empty() && middle == "()" && right.is_empty(); + let is_required = is_lollipop_line && left.is_empty() - && ((middle == "(" && right.is_empty()) || (middle.is_empty() && right == "(")) - { + && ((middle == "(" && right.is_empty()) || (middle.is_empty() && right == "(")); + + let decor_role = if is_canonical_provided || is_generic_lollipop_provided { + Some(EndpointRole::Provided) + } else if is_required { Some(EndpointRole::Required) } else { None @@ -550,7 +554,7 @@ impl ComponentResolver { return Some(ComponentResolverError::InvalidRelationship { from: input.relation.lhs.clone(), to: input.relation.rhs.clone(), - reason: "Interface decorators '-(' and ')-' require exactly one Interface endpoint" + reason: "Interface decorators '-(', ')-', and '--()' require exactly one Interface endpoint" .to_string(), }); } @@ -564,7 +568,7 @@ impl ComponentResolver { return Some(ComponentResolverError::InvalidRelationship { from: input.relation.lhs.clone(), to: input.relation.rhs.clone(), - reason: "Interface decorators '-(' and ')-' are not allowed between two interfaces" + reason: "Interface decorators '-(', ')-', and '--()' are not allowed between two interfaces" .to_string(), }); } @@ -599,13 +603,12 @@ impl ComponentResolver { return Some(ComponentResolverError::InvalidRelationship { from: input.relation.lhs.clone(), to: input.relation.rhs.clone(), - reason: "Unsupported interface decorator syntax: only ')-' (Provided) and '-(' (Required) are supported" + reason: "Unsupported interface decorator syntax: only ')-' and '--()' (Provided) plus '-(' (Required) are supported; '()--' is rejected" .to_string(), }); } None } - fn rule_port_role_consistency( input: &RelationValidationInput<'_>, ) -> Option { diff --git a/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs b/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs index da96037e..8d048ab9 100644 --- a/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs +++ b/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs @@ -95,8 +95,8 @@ fn test_relation_simple_name_alias() { } #[test] -fn test_relation_invalid_arrow_parsed_as_association() { - run_component_resolver_case("relation_invalid_arrow_parsed_as_association"); +fn test_relation_generic_lollipop_provided_to_component() { + run_component_resolver_case("relation_generic_lollipop_provided_to_component"); } #[test] @@ -139,6 +139,26 @@ fn test_invalid_interface_left_decorator() { run_component_resolver_case("invalid_interface_left_decorator"); } +#[test] +fn test_invalid_interface_left_required_decorator() { + run_component_resolver_case("invalid_interface_left_required_decorator"); +} + +#[test] +fn test_invalid_interface_left_generic_lollipop() { + run_component_resolver_case("invalid_interface_left_generic_lollipop"); +} + +#[test] +fn test_invalid_interface_left_inverse_generic_lollipop() { + run_component_resolver_case("invalid_interface_left_inverse_generic_lollipop"); +} + +#[test] +fn test_invalid_component_left_inverse_generic_lollipop() { + run_component_resolver_case("invalid_component_left_inverse_generic_lollipop"); +} + #[test] fn test_invalid_interface_binding_non_component() { run_component_resolver_case("invalid_interface_binding_non_component");