diff --git a/bazel/rules/rules_score/docs/user_guide/architectural_design.md b/bazel/rules/rules_score/docs/user_guide/architectural_design.md index 69960f62..dac7b09c 100644 --- a/bazel/rules/rules_score/docs/user_guide/architectural_design.md +++ b/bazel/rules/rules_score/docs/user_guide/architectural_design.md @@ -71,6 +71,48 @@ package "MySeooc" as MySeooc <> { @enduml ``` +#### Valid PlantUML Definitions + +The validator identifies elements by their **stereotype**, not by the PlantUML keyword used. Both `package` and `component` keywords are accepted at each level. + +| Stereotype | Valid PlantUML keywords | Meaning | Bazel rule | +|---|---|---|---| +| `<>` | `package`, `component` | Safety Element out of Context boundary | `dependable_element` | +| `<>` | `component`, `package` | Architectural component | `component` | +| `<>` | `component`, `package` | Leaf implementation unit | `unit` | + +#### Ports and Interface Bindings + +Elements with stereotype `<>` or `<>` may declare ports and bind them to interfaces. This documents which external interfaces the element requires or provides. + +```text +@startuml MySeooc_StaticDesign + +package "MySeooc" as MySeooc <> { + component "KvsComponent" as KvsComponent <> { + component "KeyValueStore" as KeyValueStore <> + } + + portin " " as p_storage ' required interface port + portout " " as p_api ' provided interface port +} + +interface "score::storage" as storage +interface "kvsapi" as kvsapi + +p_storage -( storage : requires +p_api )- kvsapi : provides + +@enduml +``` + +**Rules:** + +- `portin` / `portout` are declared inside the `<>` or `<>` element. +- `-(` binds a required (incoming) interface; `)-` binds a provided (outgoing) interface. +- The `--()` lollipop syntax (e.g. `port --() Interface`) is treated as a plain association and does **not** carry interface-binding semantics. +- Plain `package` without a stereotype cannot carry interface bindings. + ### Bazel The PlantUML diagrams capture *intended* structure; the Bazel rules model the *actual* implementation. Using the same example as the diagram above — SEooC `MySeooc` containing component `KvsComponent` with units `KeyValueStore` and `StorageBackend` — the three rules work together like this: diff --git a/plantuml/parser/integration_test/component_diagram/invalid_interface_binding_non_component/output.yaml b/plantuml/parser/integration_test/component_diagram/invalid_interface_binding_non_component/output.yaml index ebd4252e..5fe1a24f 100644 --- a/plantuml/parser/integration_test/component_diagram/invalid_interface_binding_non_component/output.yaml +++ b/plantuml/parser/integration_test/component_diagram/invalid_interface_binding_non_component/output.yaml @@ -16,4 +16,4 @@ invalid_interface_binding_non_component.puml: fields: from: "ActorA" to: "IService" - reason: "Decorator binding only allows Component on the left and Interface on the right" + 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_decorator/output.yaml b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_decorator/output.yaml index 31993808..2c935e9a 100644 --- a/plantuml/parser/integration_test/component_diagram/invalid_interface_left_decorator/output.yaml +++ b/plantuml/parser/integration_test/component_diagram/invalid_interface_left_decorator/output.yaml @@ -16,4 +16,4 @@ invalid_interface_left_decorator.puml: fields: from: "IProvided" to: "A" - reason: "Decorator binding only allows Component on the left and Interface on the right" + 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_package_no_stereotype_binding/invalid_package_no_stereotype_binding.puml b/plantuml/parser/integration_test/component_diagram/invalid_package_no_stereotype_binding/invalid_package_no_stereotype_binding.puml new file mode 100644 index 00000000..23ae2866 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_package_no_stereotype_binding/invalid_package_no_stereotype_binding.puml @@ -0,0 +1,21 @@ +' ******************************************************************************* +' 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 "IService" as IService +} + +Pkg )- IService : invalid + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/invalid_package_no_stereotype_binding/output.yaml b/plantuml/parser/integration_test/component_diagram/invalid_package_no_stereotype_binding/output.yaml new file mode 100644 index 00000000..c26762dc --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/invalid_package_no_stereotype_binding/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_package_no_stereotype_binding.puml: + error: + type: "InvalidRelationship" + fields: + from: "Pkg" + to: "IService" + 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/package_component_interface_binding/output.json b/plantuml/parser/integration_test/component_diagram/package_component_interface_binding/output.json new file mode 100644 index 00000000..7daac04e --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/package_component_interface_binding/output.json @@ -0,0 +1,44 @@ +{ + "package_component_interface_binding.puml": { + "MyComponent": { + "id": "MyComponent", + "name": "MyComponent", + "alias": "MyComponent", + "parent_id": null, + "comp_type": "Package", + "stereotype": "component", + "relations": [ + { + "target": "IRequired", + "annotation": "requires", + "relation_type": "InterfaceBinding", + "source_role": "Required" + }, + { + "target": "IProvided", + "annotation": "provides", + "relation_type": "InterfaceBinding", + "source_role": "Provided" + } + ] + }, + "IRequired": { + "id": "IRequired", + "name": "IRequired", + "alias": "IRequired", + "parent_id": null, + "comp_type": "Interface", + "stereotype": null, + "relations": [] + }, + "IProvided": { + "id": "IProvided", + "name": "IProvided", + "alias": "IProvided", + "parent_id": null, + "comp_type": "Interface", + "stereotype": null, + "relations": [] + } + } +} diff --git a/plantuml/parser/integration_test/component_diagram/package_component_interface_binding/package_component_interface_binding.puml b/plantuml/parser/integration_test/component_diagram/package_component_interface_binding/package_component_interface_binding.puml new file mode 100644 index 00000000..ccdb0a52 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/package_component_interface_binding/package_component_interface_binding.puml @@ -0,0 +1,26 @@ +' ******************************************************************************* +' 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 "MyComponent" as MyComponent <> { + portin " " as p_in + portout " " as p_out +} + +interface "IRequired" as IRequired +interface "IProvided" as IProvided + +p_in -( IRequired : requires +p_out )- IProvided : provides + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/package_seooc_interface_binding/output.json b/plantuml/parser/integration_test/component_diagram/package_seooc_interface_binding/output.json new file mode 100644 index 00000000..60e7acfb --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/package_seooc_interface_binding/output.json @@ -0,0 +1,44 @@ +{ + "package_seooc_interface_binding.puml": { + "MySeooc": { + "id": "MySeooc", + "name": "MySeooc", + "alias": "MySeooc", + "parent_id": null, + "comp_type": "Package", + "stereotype": "SEooC", + "relations": [ + { + "target": "IRequired", + "annotation": "requires", + "relation_type": "InterfaceBinding", + "source_role": "Required" + }, + { + "target": "IProvided", + "annotation": "provides", + "relation_type": "InterfaceBinding", + "source_role": "Provided" + } + ] + }, + "IRequired": { + "id": "IRequired", + "name": "IRequired", + "alias": "IRequired", + "parent_id": null, + "comp_type": "Interface", + "stereotype": null, + "relations": [] + }, + "IProvided": { + "id": "IProvided", + "name": "IProvided", + "alias": "IProvided", + "parent_id": null, + "comp_type": "Interface", + "stereotype": null, + "relations": [] + } + } +} diff --git a/plantuml/parser/integration_test/component_diagram/package_seooc_interface_binding/package_seooc_interface_binding.puml b/plantuml/parser/integration_test/component_diagram/package_seooc_interface_binding/package_seooc_interface_binding.puml new file mode 100644 index 00000000..d3973e94 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/package_seooc_interface_binding/package_seooc_interface_binding.puml @@ -0,0 +1,26 @@ +' ******************************************************************************* +' 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 "MySeooc" as MySeooc <> { + portin " " as p_in + portout " " as p_out +} + +interface "IRequired" as IRequired +interface "IProvided" as IProvided + +p_in -( IRequired : requires +p_out )- IProvided : 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 50e5cd1f..6f5cc9cd 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 @@ -53,4 +53,5 @@ When interface bindings are used: - Exactly one endpoint must be an interface. - Interface-to-interface bindings are not allowed. - Interface-left decorator forms are rejected. +- Only a `component` element or a `package` with stereotype `<>` or `<>` is accepted on the left side. - Port role (`portin`/`portout`) must be consistent with decorator role. 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 e643e9f8..47cccd08 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 @@ -65,7 +65,7 @@ struct RelationValidationInput<'a> { has_interface_tokens: bool, src_is_interface: bool, tgt_is_interface: bool, - src_is_component: bool, + src_is_component_role: bool, decor_role: Option, src_port_role: Option, } @@ -471,9 +471,15 @@ impl ComponentResolver { }); } - let decor_role = if line == "-" && left == ")" && middle.is_empty() && right.is_empty() { + // A lollipop line may carry a direction hint, which adds a second dash + // segment: `)-u-` or `-u-(`. The line field then contains `"--"` instead + // 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 line == "-" + } else if is_lollipop_line && left.is_empty() && ((middle == "(" && right.is_empty()) || (middle.is_empty() && right == "(")) { @@ -570,13 +576,13 @@ impl ComponentResolver { ) -> Option { if input.has_interface_tokens && input.decor_role.is_some() - && (!input.src_is_component || !input.tgt_is_interface) + && (!input.src_is_component_role || !input.tgt_is_interface) { return Some(ComponentResolverError::InvalidRelationship { from: input.relation.lhs.clone(), to: input.relation.rhs.clone(), reason: - "Decorator binding only allows Component on the left and Interface on the right" + "Decorator binding requires a Component or component-stereotyped element on the left and Interface on the right" .to_string(), }); } @@ -636,6 +642,13 @@ impl ComponentResolver { let src_is_interface = matches!(src_type, Some(ComponentType::Interface)); let tgt_is_interface = matches!(tgt_type, Some(ComponentType::Interface)); let src_is_component = matches!(src_type, Some(ComponentType::Component)); + let src_is_package = matches!(src_type, Some(ComponentType::Package)); + let src_stereotype = self + .elements + .get(&src_fqn) + .and_then(|e| e.stereotype.as_deref()); + let src_is_component_role = src_is_component + || (src_is_package && matches!(src_stereotype, Some("SEooC") | Some("component"))); let validation_input = RelationValidationInput { relation, @@ -643,7 +656,7 @@ impl ComponentResolver { || parsed_arrow.has_required_token, src_is_interface, tgt_is_interface, - src_is_component, + src_is_component_role, decor_role: parsed_arrow.decor_role, src_port_role, }; 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 dcb29fb5..92a6a42f 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 @@ -224,6 +224,21 @@ fn test_port_target_no_decor_no_mismatch() { run_component_resolver_case("port_target_no_decor_no_mismatch"); } +#[test] +fn test_package_seooc_interface_binding() { + run_component_resolver_case("package_seooc_interface_binding"); +} + +#[test] +fn test_package_component_interface_binding() { + run_component_resolver_case("package_component_interface_binding"); +} + +#[test] +fn test_invalid_package_no_stereotype_binding() { + run_component_resolver_case("invalid_package_no_stereotype_binding"); +} + #[test] fn test_deployment_diagram() { run_deployment_resolver_case("deployment_diagram_it"); diff --git a/validation/core/docs/specifications/bazel_component.md b/validation/core/docs/specifications/bazel_component.md index 10d52a62..5a02753a 100644 --- a/validation/core/docs/specifications/bazel_component.md +++ b/validation/core/docs/specifications/bazel_component.md @@ -108,6 +108,40 @@ immediate enclosing component alias as parent. | Missing unit in PlantUML | Unit Consistency | | Extra unit in PlantUML | Unit Consistency | +## PlantUML Stereotype Reference + +The validator identifies elements by their **stereotype**, not by the PlantUML keyword. Both `package` and `component` keywords are accepted for each role. + +| Stereotype | Valid PlantUML keywords | Meaning | Bazel rule | +|---|---|---|---| +| `<>` | `package`, `component` | Safety Element out of Context boundary; may own `portin`/`portout` ports | `dependable_element` | +| `<>` | `component`, `package` | Architectural component; may own `portin`/`portout` ports | `component` | +| `<>` | `component`, `package` | Leaf implementation unit | `unit` | + +### Port and Interface Binding + +Elements with stereotype `<>` or `<>` may declare ports and bind them to interfaces: + +```text +package "MySeooc" as MySeooc <> { + portin " " as p_in ' required interface port + portout " " as p_out ' provided interface port +} + +interface "IRequired" as IRequired +interface "IProvided" as IProvided + +p_in -( IRequired : requires ' required binding +p_out )- IProvided : provides ' provided binding +``` + +**Rules:** + +- `portin` / `portout` must be declared inside the `<>` or `<>` element. +- Use `-(` for required (incoming) and `)-` for provided (outgoing) interface bindings. +- Plain `package` **without** a stereotype cannot carry interface bindings. +- Elements with other stereotypes (e.g. `actor`, `database`) are not valid on the left side of a binding. + ## Debug Output The validator emits debug output containing: diff --git a/validation/core/src/readers/component_diagram_reader.rs b/validation/core/src/readers/component_diagram_reader.rs index e4530efa..89f542fa 100644 --- a/validation/core/src/readers/component_diagram_reader.rs +++ b/validation/core/src/readers/component_diagram_reader.rs @@ -26,12 +26,30 @@ pub struct ComponentDiagramReader; impl ComponentDiagramReader { /// Read all `Component` and `Package` entities from the given FlatBuffers /// binary files. + /// + /// Files that carry a known non-component file identifier (e.g. `CLSD` for + /// class diagrams or `SEQD` for sequence diagrams) are silently skipped so + /// that callers can pass all architectural-design FlatBuffers without + /// pre-filtering by diagram type. pub fn read(paths: &[String]) -> Result { let mut out = Vec::new(); for path in paths { let data = fs::read(path).map_err(|e| format!("Failed to read {path}: {e}"))?; + // FlatBuffers stores the file identifier at bytes 4-7 when one is + // present. Component diagrams are written without an identifier + // (builder.finish(root, None)), so bytes 4-7 are regular data. + // Class diagrams ("CLSD") and sequence diagrams ("SEQD") carry an + // explicit identifier. Skip such files here; they are not + // component diagrams and must not be parsed with the component schema. + if data.len() >= 8 { + let file_id = &data[4..8]; + if file_id == b"CLSD" || file_id == b"SEQD" { + continue; + } + } + let graph = flatbuffers::root::(&data) .map_err(|e| format!("Failed to parse FlatBuffer {path}: {e}"))?;