Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<component>>
interface "IService" as IService

ComponentA --() IService : association
ComponentA ()-- IService : invalid
}

@enduml
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
@@ -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 <<component>>

IProvided --() A : invalid
}

@enduml
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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 <<component>>

IProvided ()-- A : invalid
}

@enduml
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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 <<component>>

IReq -( A : invalid
}

@enduml
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <<component>>
interface "IService" as IService

ComponentA --() IService : provides
}

@enduml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- Interface binding (component-left only):
- Provided interface:
- `Component )- Interface`
- `Component --() Interface`
- Required interface:
- `Component -( Interface`

Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,14 @@ impl ComponentResolver {
}

// Supported relation syntaxes:
// - Interface binding: `)-`, `-(`
// - Interface binding: `)-`, `-(`, `--()`
// - Directed: `-->`, `<--`, `..>`, `<..`
// - Undirected: `--`, `..`
fn parse_arrow(relation: &Relation) -> Result<ArrowAnalysis, ComponentResolverError> {
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 {
Expand All @@ -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
Expand Down Expand Up @@ -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(),
});
}
Expand All @@ -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(),
});
}
Expand Down Expand Up @@ -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<ComponentResolverError> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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");
Expand Down
Loading