Skip to content

Commit 32ee3cb

Browse files
melodyoncodehoe-jo
authored andcommitted
serialize relationship in component diagram
1 parent 2ea117f commit 32ee3cb

6 files changed

Lines changed: 92 additions & 84 deletions

File tree

plantuml/parser/integration_test/src/test_error_view.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,6 @@ impl ErrorView for ElementResolverError {
240240
} => ProjectedError::new("AmbiguousReference")
241241
.with_field("reference", reference.clone())
242242
.with_field("candidates", candidates.join(", ")),
243-
244243
}
245244
}
246245
}

plantuml/parser/puml_resolver/src/component_diagram/src/component_relations.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
<!-- ----------------------------------------------------------------------------
2+
Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
4+
See the NOTICE file(s) distributed with this work for additional
5+
information regarding copyright ownership.
6+
7+
This program and the accompanying materials are made available under the
8+
terms of the Apache License Version 2.0 which is available at
9+
https://www.apache.org/licenses/LICENSE-2.0
10+
11+
SPDX-License-Identifier: Apache-2.0
12+
----------------------------------------------------------------------------- -->
13+
114
### Supported component relations
215

316
- Association (no direction):

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

Lines changed: 44 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,7 @@ impl ElementResolver {
183183
continue;
184184
};
185185

186-
let Some(name) = element
187-
.alias
188-
.as_deref()
189-
.or_else(|| element.name.as_deref())
190-
else {
186+
let Some(name) = element.alias.as_deref().or(element.name.as_deref()) else {
191187
continue;
192188
};
193189

@@ -208,7 +204,8 @@ impl ElementResolver {
208204
if parts.len() == 1 {
209205
for i in (0..=self.scope.len()).rev() {
210206
let outer_scope = &self.scope[..i];
211-
let matches = self.collect_matching_port_fqns_in_scope_or_children(outer_scope, raw);
207+
let matches =
208+
self.collect_matching_port_fqns_in_scope_or_children(outer_scope, raw);
212209
if !matches.is_empty() {
213210
// Keep nearest-scope matches only for simple names.
214211
candidates.extend(matches);
@@ -254,7 +251,8 @@ impl ElementResolver {
254251

255252
for pfqn in &candidates {
256253
let ok = pfqn == resolved
257-
|| self.port_parents
254+
|| self
255+
.port_parents
258256
.get(pfqn)
259257
.map(|p| p == resolved)
260258
.unwrap_or(false);
@@ -342,8 +340,7 @@ impl ElementResolver {
342340

343341
// 2) lexical port lookup (collapsed to parent component)
344342
if let Some(res) = self.walk_scopes_nearest_first(|scope| {
345-
let ports =
346-
self.collect_matching_port_fqns_in_scope_or_children(scope, name);
343+
let ports = self.collect_matching_port_fqns_in_scope_or_children(scope, name);
347344

348345
if ports.is_empty() {
349346
return Ok(None);
@@ -366,32 +363,22 @@ impl ElementResolver {
366363
let global: Vec<String> = self
367364
.elements
368365
.values()
369-
.filter(|e| {
370-
e.alias.as_deref() == Some(name)
371-
|| e.name.as_deref() == Some(name)
372-
})
366+
.filter(|e| e.alias.as_deref() == Some(name) || e.name.as_deref() == Some(name))
373367
.map(|e| e.id.clone())
374368
.collect::<std::collections::BTreeSet<_>>()
375369
.into_iter()
376370
.collect();
377371

378-
Ok(Self::pick_unique(global, raw)?)
372+
Self::pick_unique(global, raw)
379373
}
380374

381-
fn resolve_relative(
382-
&self,
383-
parts: &[&str],
384-
) -> Result<Option<String>, ElementResolverError> {
385-
let matches =
386-
self.collect_element_fqns_in_scope_or_children(&self.scope, parts);
375+
fn resolve_relative(&self, parts: &[&str]) -> Result<Option<String>, ElementResolverError> {
376+
let matches = self.collect_element_fqns_in_scope_or_children(&self.scope, parts);
387377

388-
Ok(Self::pick_unique(matches, &parts.join("."))?)
378+
Self::pick_unique(matches, &parts.join("."))
389379
}
390380

391-
fn walk_scopes_nearest_first<F>(
392-
&self,
393-
mut f: F,
394-
) -> Result<Option<String>, ElementResolverError>
381+
fn walk_scopes_nearest_first<F>(&self, mut f: F) -> Result<Option<String>, ElementResolverError>
395382
where
396383
F: FnMut(&[String]) -> Result<Option<String>, ElementResolverError>,
397384
{
@@ -504,24 +491,8 @@ impl ElementResolver {
504491

505492
fn validate_relation_constraints(
506493
&self,
507-
relation: &Relation,
508-
has_interface_tokens: bool,
509-
src_is_interface: bool,
510-
tgt_is_interface: bool,
511-
src_is_component: bool,
512-
decor_role: Option<EndpointRole>,
513-
src_port_role: Option<EndpointRole>,
494+
input: &RelationValidationInput<'_>,
514495
) -> Result<(), ElementResolverError> {
515-
let input = RelationValidationInput {
516-
relation,
517-
has_interface_tokens,
518-
src_is_interface,
519-
tgt_is_interface,
520-
src_is_component,
521-
decor_role,
522-
src_port_role,
523-
};
524-
525496
let rules: [RelationValidationRule; 5] = [
526497
Self::rule_require_exactly_one_interface_endpoint,
527498
Self::rule_disallow_interface_to_interface,
@@ -531,7 +502,7 @@ impl ElementResolver {
531502
];
532503

533504
for rule in rules {
534-
if let Some(err) = rule(&input) {
505+
if let Some(err) = rule(input) {
535506
return Err(err);
536507
}
537508
}
@@ -540,58 +511,53 @@ impl ElementResolver {
540511
}
541512

542513
fn rule_require_exactly_one_interface_endpoint(
543-
input: &RelationValidationInput<'_>
514+
input: &RelationValidationInput<'_>,
544515
) -> Option<ElementResolverError> {
545-
if input.has_interface_tokens
546-
&& !input.src_is_interface
547-
&& !input.tgt_is_interface
548-
{
516+
if input.has_interface_tokens && !input.src_is_interface && !input.tgt_is_interface {
549517
return Some(ElementResolverError::InvalidRelationship {
550518
from: input.relation.lhs.clone(),
551519
to: input.relation.rhs.clone(),
552-
reason:
553-
"Interface decorators '-(' and ')-' require exactly one Interface endpoint"
554-
.to_string(),
520+
reason: "Interface decorators '-(' and ')-' require exactly one Interface endpoint"
521+
.to_string(),
555522
});
556523
}
557524
None
558525
}
559526

560527
fn rule_disallow_interface_to_interface(
561-
input: &RelationValidationInput<'_>
528+
input: &RelationValidationInput<'_>,
562529
) -> Option<ElementResolverError> {
563-
if input.has_interface_tokens
564-
&& input.src_is_interface
565-
&& input.tgt_is_interface
566-
{
530+
if input.has_interface_tokens && input.src_is_interface && input.tgt_is_interface {
567531
return Some(ElementResolverError::InvalidRelationship {
568532
from: input.relation.lhs.clone(),
569533
to: input.relation.rhs.clone(),
570-
reason:
571-
"Interface decorators '-(' and ')-' are not allowed between two interfaces"
572-
.to_string(),
534+
reason: "Interface decorators '-(' and ')-' are not allowed between two interfaces"
535+
.to_string(),
573536
});
574537
}
575538
None
576539
}
577540

578541
fn rule_require_component_endpoint_for_binding(
579-
input: &RelationValidationInput<'_>
542+
input: &RelationValidationInput<'_>,
580543
) -> Option<ElementResolverError> {
581-
if input.has_interface_tokens && input.decor_role.is_some() {
582-
if !input.src_is_component || !input.tgt_is_interface {
583-
return Some(ElementResolverError::InvalidRelationship {
584-
from: input.relation.lhs.clone(),
585-
to: input.relation.rhs.clone(),
586-
reason: "Decorator binding only allows Component on the left and Interface on the right".to_string(),
587-
});
588-
}
544+
if input.has_interface_tokens
545+
&& input.decor_role.is_some()
546+
&& (!input.src_is_component || !input.tgt_is_interface)
547+
{
548+
return Some(ElementResolverError::InvalidRelationship {
549+
from: input.relation.lhs.clone(),
550+
to: input.relation.rhs.clone(),
551+
reason:
552+
"Decorator binding only allows Component on the left and Interface on the right"
553+
.to_string(),
554+
});
589555
}
590556
None
591557
}
592558

593559
fn rule_disallow_generic_decor_with_direction(
594-
input: &RelationValidationInput<'_>
560+
input: &RelationValidationInput<'_>,
595561
) -> Option<ElementResolverError> {
596562
if input.has_interface_tokens
597563
&& input.decor_role.is_none()
@@ -608,11 +574,9 @@ impl ElementResolver {
608574
}
609575

610576
fn rule_port_role_consistency(
611-
input: &RelationValidationInput<'_>
577+
input: &RelationValidationInput<'_>,
612578
) -> Option<ElementResolverError> {
613-
if let (Some(port_role), Some(decor_role)) =
614-
(input.src_port_role, input.decor_role)
615-
{
579+
if let (Some(port_role), Some(decor_role)) = (input.src_port_role, input.decor_role) {
616580
if port_role != decor_role {
617581
return Some(ElementResolverError::InvalidRelationship {
618582
from: input.relation.lhs.clone(),
@@ -646,15 +610,18 @@ impl ElementResolver {
646610
let tgt_is_interface = matches!(tgt_type, Some(ElementType::Interface));
647611
let src_is_component = matches!(src_type, Some(ElementType::Component));
648612

649-
self.validate_relation_constraints(
613+
let validation_input = RelationValidationInput {
650614
relation,
651-
parsed_arrow.has_provided_token || parsed_arrow.has_required_token,
615+
has_interface_tokens: parsed_arrow.has_provided_token
616+
|| parsed_arrow.has_required_token,
652617
src_is_interface,
653618
tgt_is_interface,
654619
src_is_component,
655-
parsed_arrow.decor_role,
620+
decor_role: parsed_arrow.decor_role,
656621
src_port_role,
657-
)?;
622+
};
623+
624+
self.validate_relation_constraints(&validation_input)?;
658625

659626
let relation_type = Self::infer_relation_type(&parsed_arrow);
660627

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ mod component_logic;
1515
mod component_resolver;
1616

1717
pub use component_logic::{
18-
ComponentResolverError, ComponentType, ElementResolverError, ElementType, LogicComponent,
19-
LogicElement,
18+
ComponentRelationType, ComponentResolverError, ComponentType, ElementResolverError,
19+
ElementType, EndpointRole, LogicComponent, LogicElement,
2020
};
2121
pub use component_resolver::{ComponentResolver, ElementResolver};

plantuml/parser/puml_serializer/src/fbs/component.fbs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,23 @@
1313

1414
namespace component;
1515

16+
enum ComponentRelationType:byte {
17+
Association,
18+
Dependency,
19+
InterfaceBinding
20+
}
21+
22+
enum EndpointRole:byte {
23+
None,
24+
Provided,
25+
Required
26+
}
27+
1628
table LogicRelation {
1729
target:string;
1830
annotation:string;
19-
relation_type:string;
31+
relation_type:ComponentRelationType = Association;
32+
source_role:EndpointRole = None;
2033
}
2134

2235
enum ComponentType:byte {

plantuml/parser/puml_serializer/src/serialize/component_serializer.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use flatbuffers::FlatBufferBuilder;
1515
use std::collections::HashMap;
1616

1717
use component_fbs::component as fb;
18-
use component_resolver::{ElementType, LogicElement};
18+
use component_resolver::{ComponentRelationType, ElementType, EndpointRole, LogicElement};
1919

2020
pub struct ComponentSerializer;
2121

@@ -34,14 +34,14 @@ impl ComponentSerializer {
3434
for r in &element.relations {
3535
let target_offset = builder.create_string(&r.target);
3636
let annotation_offset = r.annotation.as_ref().map(|s| builder.create_string(s));
37-
let relation_type_offset = builder.create_string(&r.relation_type);
3837

3938
let rel = fb::LogicRelation::create(
4039
&mut builder,
4140
&fb::LogicRelationArgs {
4241
target: Some(target_offset),
4342
annotation: annotation_offset,
44-
relation_type: Some(relation_type_offset),
43+
relation_type: Self::convert_relation_type(r.relation_type),
44+
source_role: Self::convert_endpoint_role(r.source_role),
4545
},
4646
);
4747
relation_offsets.push(rel);
@@ -136,4 +136,20 @@ impl ComponentSerializer {
136136
ElementType::Usecase => fb::ComponentType::Usecase,
137137
}
138138
}
139+
140+
fn convert_relation_type(relation_type: ComponentRelationType) -> fb::ComponentRelationType {
141+
match relation_type {
142+
ComponentRelationType::Association => fb::ComponentRelationType::Association,
143+
ComponentRelationType::Dependency => fb::ComponentRelationType::Dependency,
144+
ComponentRelationType::InterfaceBinding => fb::ComponentRelationType::InterfaceBinding,
145+
}
146+
}
147+
148+
fn convert_endpoint_role(source_role: EndpointRole) -> fb::EndpointRole {
149+
match source_role {
150+
EndpointRole::None => fb::EndpointRole::None,
151+
EndpointRole::Provided => fb::EndpointRole::Provided,
152+
EndpointRole::Required => fb::EndpointRole::Required,
153+
}
154+
}
139155
}

0 commit comments

Comments
 (0)