Skip to content

Commit 7e709bb

Browse files
committed
Add docs for relationship semantics
1 parent 71868d6 commit 7e709bb

3 files changed

Lines changed: 82 additions & 33 deletions

File tree

plantuml/parser/puml_resolver/src/component_diagram/src/component_logic.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ pub struct LogicRelation {
6767
pub source_role: EndpointRole,
6868
}
6969

70-
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
70+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
7171
pub enum ComponentRelationType {
7272
#[default]
7373
#[serde(alias = "None")]
@@ -79,7 +79,7 @@ pub enum ComponentRelationType {
7979
InterfaceBinding,
8080
}
8181

82-
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
82+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
8383
pub enum EndpointRole {
8484
#[default]
8585
None,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
### Supported component relations
2+
3+
- Association (no direction):
4+
- `A -- B`
5+
- `A .. B`
6+
7+
- Dependency (directional):
8+
- `A --> B`
9+
- `B <-- A` (equivalent reverse-direction syntax)
10+
- `A ..> B`
11+
- `B <.. A` (equivalent reverse-direction syntax)
12+
13+
- Interface binding (component-left only):
14+
- Provided interface:
15+
- `Component )- Interface`
16+
- Required interface:
17+
- `Component -( Interface`
18+
19+
Note: Only component-to-interface binding forms are supported.
20+
21+
### Unsupported interface binding forms
22+
23+
The following forms are rejected:
24+
25+
- Interface )- Component
26+
- Interface -( Component
27+
28+
### Generic lollipop decorators
29+
30+
The following forms are resolved as plain associations and do not carry interface-binding semantics:
31+
- `Component --() Interface`
32+
- `Interface ()-- Component`
33+
34+
Note: Use canonical component-left forms such as `Component )- Interface` or `Component -( Interface` when you need interface binding behavior.
35+
36+
### Resolver constraints
37+
38+
When interface bindings are used:
39+
40+
- Exactly one endpoint must be an interface.
41+
- Interface-to-interface bindings are not allowed.
42+
- Interface-left decorator forms are rejected.
43+
- Port role (`portin`/`portout`) must be consistent with decorator role.

plantuml/parser/puml_resolver/src/component_diagram/src/component_resolver.rs

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ struct ArrowAnalysis {
3636
decor_role: Option<EndpointRole>,
3737
}
3838

39-
struct RelationValidationInput {
40-
relation: Relation,
39+
struct RelationValidationInput<'a> {
40+
relation: &'a Relation,
4141
has_interface_tokens: bool,
4242
src_is_interface: bool,
4343
tgt_is_interface: bool,
@@ -46,7 +46,7 @@ struct RelationValidationInput {
4646
src_port_role: Option<EndpointRole>,
4747
}
4848

49-
type RelationValidationRule = fn(&RelationValidationInput) -> Option<ElementResolverError>;
49+
type RelationValidationRule = fn(&RelationValidationInput<'_>) -> Option<ElementResolverError>;
5050

5151
#[derive(Default)]
5252
pub struct ElementResolver {
@@ -110,8 +110,11 @@ impl ElementResolver {
110110
// and whose parent element is a descendant of (or equal to) the current scope.
111111
let scope_prefix = scope.join(".");
112112
for (pfqn, parent_comp) in &self.port_parents {
113-
let parts: Vec<&str> = pfqn.split('.').collect();
114-
if parts.last() != Some(&port_local) {
113+
let pfqn_last = match pfqn.rfind('.') {
114+
Some(i) => &pfqn[i + 1..],
115+
None => pfqn,
116+
};
117+
if pfqn_last != port_local {
115118
continue;
116119
}
117120

@@ -169,18 +172,22 @@ impl ElementResolver {
169172
Some(scope.join("."))
170173
};
171174

172-
let children: Vec<String> = self
175+
let children: &[String] = self
173176
.child_elements_by_parent
174177
.get(&scope_key)
175-
.cloned()
176-
.unwrap_or_default();
178+
.map(Vec::as_slice)
179+
.unwrap_or(&[]);
177180

178181
for child_id in children {
179-
let Some(element) = self.elements.get(&child_id) else {
182+
let Some(element) = self.elements.get(child_id) else {
180183
continue;
181184
};
182185

183-
let Some(name) = element.alias.as_deref().or_else(|| element.name.as_deref()) else {
186+
let Some(name) = element
187+
.alias
188+
.as_deref()
189+
.or_else(|| element.name.as_deref())
190+
else {
184191
continue;
185192
};
186193

@@ -253,6 +260,8 @@ impl ElementResolver {
253260
.unwrap_or(false);
254261

255262
if ok {
263+
// Invariant: after aligning candidates to resolved endpoint identity, there should be at most one match.
264+
// If multiple matches remain, this indicates inconsistent resolver state or unexpected duplicate-alignment; fail fast with AmbiguousReference instead of silently picking one.
256265
if matched.is_some() {
257266
return Err(ElementResolverError::AmbiguousReference {
258267
reference: raw.to_string(),
@@ -504,7 +513,7 @@ impl ElementResolver {
504513
src_port_role: Option<EndpointRole>,
505514
) -> Result<(), ElementResolverError> {
506515
let input = RelationValidationInput {
507-
relation: relation.clone(),
516+
relation,
508517
has_interface_tokens,
509518
src_is_interface,
510519
tgt_is_interface,
@@ -531,7 +540,7 @@ impl ElementResolver {
531540
}
532541

533542
fn rule_require_exactly_one_interface_endpoint(
534-
input: &RelationValidationInput,
543+
input: &RelationValidationInput<'_>
535544
) -> Option<ElementResolverError> {
536545
if input.has_interface_tokens
537546
&& !input.src_is_interface
@@ -549,7 +558,7 @@ impl ElementResolver {
549558
}
550559

551560
fn rule_disallow_interface_to_interface(
552-
input: &RelationValidationInput,
561+
input: &RelationValidationInput<'_>
553562
) -> Option<ElementResolverError> {
554563
if input.has_interface_tokens
555564
&& input.src_is_interface
@@ -567,7 +576,7 @@ impl ElementResolver {
567576
}
568577

569578
fn rule_require_component_endpoint_for_binding(
570-
input: &RelationValidationInput,
579+
input: &RelationValidationInput<'_>
571580
) -> Option<ElementResolverError> {
572581
if input.has_interface_tokens && input.decor_role.is_some() {
573582
if !input.src_is_component || !input.tgt_is_interface {
@@ -582,7 +591,7 @@ impl ElementResolver {
582591
}
583592

584593
fn rule_disallow_generic_decor_with_direction(
585-
input: &RelationValidationInput,
594+
input: &RelationValidationInput<'_>
586595
) -> Option<ElementResolverError> {
587596
if input.has_interface_tokens
588597
&& input.decor_role.is_none()
@@ -598,9 +607,11 @@ impl ElementResolver {
598607
None
599608
}
600609

601-
fn rule_port_role_consistency(input: &RelationValidationInput) -> Option<ElementResolverError> {
610+
fn rule_port_role_consistency(
611+
input: &RelationValidationInput<'_>
612+
) -> Option<ElementResolverError> {
602613
if let (Some(port_role), Some(decor_role)) =
603-
(input.src_port_role.clone(), input.decor_role.clone())
614+
(input.src_port_role, input.decor_role)
604615
{
605616
if port_role != decor_role {
606617
return Some(ElementResolverError::InvalidRelationship {
@@ -641,35 +652,30 @@ impl ElementResolver {
641652
src_is_interface,
642653
tgt_is_interface,
643654
src_is_component,
644-
parsed_arrow.decor_role.clone(),
645-
src_port_role.clone(),
655+
parsed_arrow.decor_role,
656+
src_port_role,
646657
)?;
647658

648659
let relation_type = Self::infer_relation_type(&parsed_arrow);
649-
let src_role = src_port_role.clone();
650-
let tgt_role = tgt_port_role.clone();
651660

652661
let source_role = if relation_type == ComponentRelationType::InterfaceBinding {
662+
// Guard-only invariant check: InterfaceBinding should always carry a decorator role.
663+
// If this panics, resolver invariants have been broken by upstream logic changes.
653664
parsed_arrow
654665
.decor_role
655-
.clone()
656-
.or(src_role)
657-
.or(tgt_role)
658-
.unwrap_or(EndpointRole::None)
666+
.expect("Invariant: InterfaceBinding requires decorator role")
659667
} else {
660668
EndpointRole::None
661669
};
662670

663-
let (owner_fqn, target_fqn) = (src_fqn.clone(), tgt_fqn.clone());
664-
665-
let source_element = self.elements.get_mut(&owner_fqn).ok_or_else(|| {
671+
let source_element = self.elements.get_mut(&src_fqn).ok_or_else(|| {
666672
ElementResolverError::UnresolvedReference {
667-
reference: owner_fqn.clone(),
673+
reference: src_fqn.clone(),
668674
}
669675
})?;
670676

671677
let duplicate = source_element.relations.iter().any(|existing| {
672-
existing.target == target_fqn
678+
existing.target == tgt_fqn
673679
&& existing.relation_type == relation_type
674680
&& existing.source_role == source_role
675681
});
@@ -679,7 +685,7 @@ impl ElementResolver {
679685
}
680686

681687
source_element.relations.push(LogicRelation {
682-
target: target_fqn,
688+
target: tgt_fqn,
683689
annotation: relation.description.clone(),
684690
relation_type,
685691
source_role,

0 commit comments

Comments
 (0)