diff --git a/bazel/rules/rules_score/examples/seooc/design/BUILD b/bazel/rules/rules_score/examples/seooc/design/BUILD index 86411807..90755164 100644 --- a/bazel/rules/rules_score/examples/seooc/design/BUILD +++ b/bazel/rules/rules_score/examples/seooc/design/BUILD @@ -17,6 +17,9 @@ load( architectural_design( name = "sample_seooc_design", + dynamic = [ + "dynamic_design.puml", + ], public_api = [ "public_api.puml", ], diff --git a/bazel/rules/rules_score/examples/seooc/design/dynamic_design.puml b/bazel/rules/rules_score/examples/seooc/design/dynamic_design.puml new file mode 100644 index 00000000..e9508716 --- /dev/null +++ b/bazel/rules/rules_score/examples/seooc/design/dynamic_design.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2025 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 + +participant "Unit 1" as unit_1 <> +participant "Unit 2" as unit_2 <> + +unit_1 -> unit_2 : callMethod1() +unit_2 -> unit_1 : callMethod2() + +@enduml diff --git a/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_class_diagram.puml b/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_class_diagram.puml index 4d8e2780..9a625d1a 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_class_diagram.puml +++ b/bazel/rules/rules_score/examples/seooc/unit_2/docs/unit_2_class_diagram.puml @@ -15,6 +15,8 @@ namespace unit_1 { class Foo { + {final} + -- + GetNumber() : uint8_t + SetNumber(value : uint8_t) : void } @@ -22,6 +24,11 @@ namespace unit_1 { namespace unit_2 { class Bar { + {final} + -- + - foo_ : unique_ptr + -- + + Bar(foo : unique_ptr) + AssertNumber() : bool } } diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index 94835674..7f18681b 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -629,13 +629,15 @@ def _collect_architecture_components(ctx): return all_components -def _run_validation(ctx, arch_json, static_fbs_files): +def _run_validation(ctx, arch_json, static_fbs_files, dynamic_fbs_files, unit_static_fbs_files): """Run the architecture verifier tool against a pre-built JSON file. Args: ctx: Rule context arch_json: The architecture JSON File object (already declared and written) - static_fbs_files: List of static FlatBuffer files to verify against + static_fbs_files: List of static component-diagram FlatBuffer files + dynamic_fbs_files: List of dynamic component-diagram FlatBuffer files + unit_static_fbs_files: List of static class-diagram FlatBuffer files Returns: validation_log File object @@ -645,7 +647,13 @@ def _run_validation(ctx, arch_json, static_fbs_files): validation_args = ctx.actions.args() validation_args.add("--architecture-json", arch_json) - validation_args.add_all("--component-fbs", static_fbs_files) + if static_fbs_files: + validation_args.add_all("--component-fbs", static_fbs_files) + if dynamic_fbs_files: + validation_args.add_all("--sequence-fbs", dynamic_fbs_files) + + # if unit_static_fbs_files: + # validation_args.add_all("--class-fbs", unit_static_fbs_files) validation_args.add("--output", validation_log) validation_args.add("--log-level", get_log_level(ctx)) if ctx.attr.maturity == "development": @@ -653,7 +661,7 @@ def _run_validation(ctx, arch_json, static_fbs_files): # ctx.actions.run will fail the build if validation_cli returns non-zero exit code ctx.actions.run( - inputs = [arch_json] + static_fbs_files, + inputs = [arch_json] + static_fbs_files + dynamic_fbs_files + unit_static_fbs_files, outputs = [validation_log], executable = ctx.executable._validation_cli, arguments = [validation_args], @@ -815,12 +823,20 @@ def _dependable_element_index_impl(ctx): # Collect static FlatBuffers from architectural_design targets (the expected # static architecture) and verify them against the current architecture. static_fbs_files = [] + dynamic_fbs_files = [] for ad in ctx.attr.architectural_design: if ArchitecturalDesignInfo in ad: static_fbs_files.extend(ad[ArchitecturalDesignInfo].static.to_list()) + dynamic_fbs_files.extend(ad[ArchitecturalDesignInfo].dynamic.to_list()) + + # Collect class-diagram FBS files produced by unit_design targets. + unit_static_fbs_files = [] + for unit_target in all_units.values(): + unit_info = unit_target[UnitInfo] + unit_static_fbs_files.extend(unit_info.unit_design_static_fbs.to_list()) # Run validation; build fails automatically on non-zero exit - validation_log = _run_validation(ctx, arch_json, static_fbs_files) + validation_log = _run_validation(ctx, arch_json, static_fbs_files, dynamic_fbs_files, unit_static_fbs_files) # Both outputs are included so validation always runs in a default build. # validation_log is also exposed in the debug output group for explicit access. diff --git a/plantuml/parser/puml_cli/src/main.rs b/plantuml/parser/puml_cli/src/main.rs index 47c72b89..847788dd 100644 --- a/plantuml/parser/puml_cli/src/main.rs +++ b/plantuml/parser/puml_cli/src/main.rs @@ -29,7 +29,7 @@ use puml_parser::{ use puml_resolver::{ ClassResolver, DiagramResolver, ElementResolver, LogicElement, SequenceResolver, SequenceTree, }; -use puml_serializer::{ClassSerializer, ComponentSerializer}; +use puml_serializer::{ClassSerializer, ComponentSerializer, SequenceSerializer}; use puml_utils::{write_fbs_to_file, write_json_to_file, LogLevel}; /// CLI wrapper for LogLevel that implements ValueEnum @@ -214,13 +214,8 @@ fn serialize_resolved_diagram(resolved_content: &ResolvedDiagram, source_file: & ResolvedDiagram::Class(resolved_content) => { ClassSerializer::serialize(resolved_content, source_file) } - ResolvedDiagram::Sequence(_) => { - log::warn!( - "Sequence diagram serialization is not yet implemented; \ - no output will be written for '{}'", - source_file - ); - vec![] + ResolvedDiagram::Sequence(resolved_content) => { + SequenceSerializer::serialize(resolved_content, source_file) } } } diff --git a/plantuml/parser/puml_serializer/src/fbs/BUILD b/plantuml/parser/puml_serializer/src/fbs/BUILD index 033eaff3..d62f4bf5 100644 --- a/plantuml/parser/puml_serializer/src/fbs/BUILD +++ b/plantuml/parser/puml_serializer/src/fbs/BUILD @@ -108,6 +108,7 @@ rust_library( ], visibility = [ "//plantuml/parser:__subpackages__", + "//validation/core:__subpackages__", ], deps = [ "@crates//:flatbuffers", diff --git a/plantuml/parser/puml_serializer/src/serialize/sequence_serializer.rs b/plantuml/parser/puml_serializer/src/serialize/sequence_serializer.rs index 3e009420..a336b49e 100644 --- a/plantuml/parser/puml_serializer/src/serialize/sequence_serializer.rs +++ b/plantuml/parser/puml_serializer/src/serialize/sequence_serializer.rs @@ -13,42 +13,33 @@ use flatbuffers::FlatBufferBuilder; use sequence_fbs::sequence_metamodel as fb; -use sequence_logic::{ConditionType, Event, SequenceNode}; +use sequence_logic::{ConditionType, Event, SequenceNode, SequenceTree}; pub struct SequenceSerializer; impl SequenceSerializer { - pub fn serialize( - nodes: &[SequenceNode], - name: Option<&str>, - source_files: &[String], - version: Option<&str>, - ) -> Vec { + pub fn serialize(diagram: &SequenceTree, source_file: &str) -> Vec { let mut builder = FlatBufferBuilder::new(); - let name_offset = name.map(|n| builder.create_string(n)); + let name_offset = diagram.name.as_deref().map(|n| builder.create_string(n)); - let node_offsets: Vec<_> = nodes + let node_offsets: Vec<_> = diagram + .root_interactions .iter() .map(|node| Self::serialize_node(&mut builder, node)) .collect(); let nodes_offset = builder.create_vector(&node_offsets); - let source_offsets: Vec<_> = source_files - .iter() - .map(|s| builder.create_string(s)) - .collect(); + let source_offsets = [builder.create_string(source_file)]; let source_files_offset = builder.create_vector(&source_offsets); - let version_offset = version.map(|v| builder.create_string(v)); - let root = fb::SequenceDiagram::create( &mut builder, &fb::SequenceDiagramArgs { name: name_offset, root_interactions: Some(nodes_offset), source_files: Some(source_files_offset), - version: version_offset, + version: None, }, ); diff --git a/validation/core/BUILD b/validation/core/BUILD index 413edfce..54bc5806 100644 --- a/validation/core/BUILD +++ b/validation/core/BUILD @@ -21,11 +21,13 @@ rust_library( "src/models/component_diagram_models.rs", "src/models/error_models.rs", "src/models/mod.rs", + "src/models/sequence_diagram_models.rs", "src/models/shared.rs", "src/readers/bazel_reader.rs", "src/readers/class_diagram_reader.rs", "src/readers/component_diagram_reader.rs", "src/readers/mod.rs", + "src/readers/sequence_diagram_reader.rs", "src/validators/bazel_component_validator.rs", "src/validators/component_class_validator.rs", "src/validators/component_sequence_validator.rs", @@ -36,6 +38,9 @@ rust_library( deps = [ "//plantuml/parser/puml_serializer/src/fbs:class_fbs", "//plantuml/parser/puml_serializer/src/fbs:component_fbs", + "//plantuml/parser/puml_serializer/src/fbs:sequence_fbs", + "//tools/metamodel:class_diagram", + "//tools/metamodel:sequence_diagram", "@crates//:flatbuffers", "@crates//:serde", "@crates//:serde_json", diff --git a/validation/core/README.md b/validation/core/README.md index 19eb5c43..e80e4f58 100644 --- a/validation/core/README.md +++ b/validation/core/README.md @@ -25,12 +25,14 @@ The package contains two public targets: ## What It Validates -The current implementation supports two validation flows: +The current implementation supports three validation flows: 1. `BazelComponent`: compares the indexed Bazel build graph with the indexed PlantUML component-diagram structure. 2. `ComponentClass`: compares component-diagram unit aliases with enclosing namespace IDs observed in class diagrams. +3. `ComponentSequence`: compares component-diagram unit aliases with + caller/callee participants observed in sequence diagrams (exact match). The CLI builds a `ValidationContext` from the provided inputs, infers which of these flows are executable, and runs all compatible validators in one pass. @@ -59,6 +61,8 @@ The CLI accepts the following input families: - `--architecture-json`: Bazel architecture export consumed by `BazelReader` - `--component-fbs`: one or more component-diagram FlatBuffers files consumed by `ComponentDiagramReader` +- `--sequence-fbs`: one or more sequence-diagram FlatBuffers files consumed by + `SequenceDiagramReader` - `--class-fbs`: one or more class-diagram FlatBuffers files consumed by `ClassDiagramReader` @@ -66,8 +70,9 @@ The current inference rules are: - `--architecture-json` + `--component-fbs` enables `BazelComponent` - `--component-fbs` + `--class-fbs` enables `ComponentClass` +- `--component-fbs` + `--sequence-fbs` enables `ComponentSequence` -If both combinations are present, both validators are executed. +If multiple combinations are present, all compatible validators are executed. ## Run @@ -83,6 +88,7 @@ Run it directly: bazel run //validation/core:validation_cli -- \ --architecture-json path/to/architecture.json \ --component-fbs path/to/component.fbs.bin \ + --sequence-fbs path/to/sequence.fbs.bin \ --class-fbs path/to/class.fbs.bin \ --output path/to/validation.log ``` diff --git a/validation/core/docs/assets/validation_core_flow.puml b/validation/core/docs/assets/validation_core_flow.puml index d0ea2511..aa63ce89 100644 --- a/validation/core/docs/assets/validation_core_flow.puml +++ b/validation/core/docs/assets/validation_core_flow.puml @@ -17,9 +17,11 @@ title Validation Core Runtime Flow participant "CLI" as cli participant "BazelReader" as bazel_reader participant "ComponentDiagramReader" as component_reader +participant "SequenceDiagramReader" as sequence_reader participant "ClassDiagramReader" as class_reader participant "ValidationContext" as context participant "validate_bazel_component()" as bazel_validator +participant "validate_component_sequence()" as sequence_validator participant "validate_component_class()" as class_validator participant "Errors" as errors @@ -38,6 +40,12 @@ opt component fbs provided cli -> cli: to_diagram_architecture(&mut base_errors) end +opt sequence fbs provided + cli -> sequence_reader: read(paths) + sequence_reader --> cli: SequenceDiagramInputs + cli -> cli: to_sequence_diagram_index(&mut base_errors) +end + opt class fbs provided cli -> class_reader: read(paths) class_reader --> cli: ClassDiagramInputs @@ -53,6 +61,12 @@ opt bazel + component available cli -> errors: merge_errors(...) end +opt component + sequence available + cli -> sequence_validator: validate_component_sequence(..., Errors::default()) + sequence_validator --> cli: Errors + cli -> errors: merge_errors(...) +end + opt component + class available cli -> class_validator: validate_component_class(..., Errors::default()) class_validator --> cli: Errors diff --git a/validation/core/docs/assets/validation_core_overview.puml b/validation/core/docs/assets/validation_core_overview.puml index 2462fd3e..5d179962 100644 --- a/validation/core/docs/assets/validation_core_overview.puml +++ b/validation/core/docs/assets/validation_core_overview.puml @@ -29,12 +29,15 @@ package "validation/core" { package "readers" { class "BazelReader" class "ComponentDiagramReader" + class "SequenceDiagramReader" class "ClassDiagramReader" } package "models" { class "BazelArchitecture" class "ComponentDiagramArchitecture" + class "SequenceDiagramInputs" + class "SequenceDiagramIndex" class "ClassDiagramInputs" class "ClassDiagramIndex" class "Errors" @@ -50,27 +53,33 @@ package "validation/core" { +base_errors: Errors +bazel: Option +component: Option + +sequence: Option +class: Option } } cli --> BazelReader : reads architecture json cli --> ComponentDiagramReader : reads component fbs +cli --> SequenceDiagramReader : reads sequence fbs cli --> ClassDiagramReader : reads class fbs BazelReader --> BazelArchitecture ComponentDiagramReader --> ComponentDiagramArchitecture +SequenceDiagramReader --> SequenceDiagramInputs ClassDiagramReader --> ClassDiagramInputs +cli --> SequenceDiagramIndex : to_sequence_diagram_index(...) cli --> ClassDiagramIndex : to_class_diagram_index(...) cli --> context : builds context --> Errors context --> BazelArchitecture context --> ComponentDiagramArchitecture +context --> SequenceDiagramIndex context --> ClassDiagramIndex cli --> "validate_bazel_component()" : when bazel + component exist cli --> "validate_component_class()" : when component + class exist +cli --> "validate_component_sequence()" : when component + sequence exist "validate_bazel_component()" --> BazelArchitecture "validate_bazel_component()" --> ComponentDiagramArchitecture @@ -80,6 +89,10 @@ cli --> "validate_component_class()" : when component + class exist "validate_component_class()" --> ClassDiagramIndex "validate_component_class()" --> Errors +"validate_component_sequence()" --> ComponentDiagramArchitecture +"validate_component_sequence()" --> SequenceDiagramIndex +"validate_component_sequence()" --> Errors + note bottom of cli CLI owns orchestration: - input loading diff --git a/validation/core/src/lib.rs b/validation/core/src/lib.rs index 07134101..3926f2e1 100644 --- a/validation/core/src/lib.rs +++ b/validation/core/src/lib.rs @@ -16,20 +16,21 @@ //! This crate contains the shared models, readers, and validators used by the //! CLI entrypoints for architecture and design verification. -pub mod models; -pub mod readers; -pub mod validators; +mod models; +mod readers; +mod validators; pub use models::{ - BazelArchitecture, BazelInput, BazelInputEntry, ClassDiagramEntityInput, ClassDiagramIndex, - ClassDiagramInput, ClassDiagramInputs, ClassDiagramRelationshipInput, - ComponentDiagramArchitecture, ComponentDiagramInput, ComponentDiagramInputs, EntityKey, Errors, + BazelArchitecture, BazelInput, ClassDiagramIndex, ClassDiagramInputs, + ComponentDiagramArchitecture, ComponentDiagramInputs, Errors, SequenceDiagramIndex, + SequenceDiagramInputs, }; -pub use readers::{BazelReader, ClassDiagramReader, ComponentDiagramReader, Reader}; +pub use readers::{ + BazelReader, ClassDiagramReader, ComponentDiagramReader, Reader, SequenceDiagramReader, +}; pub use validators::{ - validate_bazel_component, validate_component_class, validate_component_sequence, - BazelComponentValidator, ComponentClassValidator, RequiredInput, SelectedValidator, - ValidatorSpec, ALL_VALIDATORS, + validate_bazel_component, validate_component_class, validate_component_sequence, RequiredInput, + SelectedValidator, ALL_VALIDATORS, }; diff --git a/validation/core/src/main.rs b/validation/core/src/main.rs index 8894e14d..32dafb12 100644 --- a/validation/core/src/main.rs +++ b/validation/core/src/main.rs @@ -23,10 +23,11 @@ use std::process; use clap::{Parser, ValueEnum}; use env_logger::Builder; use validation::{ - validate_bazel_component, validate_component_class, BazelArchitecture, BazelInput, BazelReader, - ClassDiagramIndex, ClassDiagramInputs, ClassDiagramReader, ComponentDiagramArchitecture, - ComponentDiagramInputs, ComponentDiagramReader, Errors, Reader, RequiredInput, - SelectedValidator, ValidatorSpec, ALL_VALIDATORS, + validate_bazel_component, validate_component_class, validate_component_sequence, + BazelArchitecture, BazelInput, BazelReader, ClassDiagramIndex, ClassDiagramInputs, + ClassDiagramReader, ComponentDiagramArchitecture, ComponentDiagramInputs, + ComponentDiagramReader, Errors, Reader, RequiredInput, SelectedValidator, SequenceDiagramIndex, + SequenceDiagramInputs, SequenceDiagramReader, ALL_VALIDATORS, }; /// CLI-visible log level (mirrors the parser/linker convention). @@ -62,6 +63,9 @@ struct Args { #[arg(long = "component-fbs", num_args = 1..)] component_fbs: Option>, + #[arg(long = "sequence-fbs", num_args = 1..)] + sequence_fbs: Option>, + #[arg(long = "class-fbs", num_args = 1..)] class_fbs: Option>, @@ -81,6 +85,7 @@ struct Args { struct ValidationCliInputs { architecture_json: Option, component_fbs: Vec, + sequence_fbs: Vec, class_fbs: Vec, } @@ -88,6 +93,7 @@ struct ValidationContext { base_errors: Errors, bazel: Option, component: Option, + sequence: Option, class: Option, } @@ -96,9 +102,30 @@ impl ValidationContext { match input { RequiredInput::Bazel => self.bazel.is_some(), RequiredInput::Component => self.component.is_some(), + RequiredInput::Sequence => self.sequence.is_some(), RequiredInput::Class => self.class.is_some(), } } + + fn run_validator(&self, validator: SelectedValidator) -> Errors { + match validator { + SelectedValidator::BazelComponent => validate_bazel_component( + self.bazel.as_ref().unwrap(), + self.component.as_ref().unwrap(), + Errors::default(), + ), + SelectedValidator::ComponentClass => validate_component_class( + self.component.as_ref().unwrap(), + self.class.as_ref().unwrap(), + Errors::default(), + ), + SelectedValidator::ComponentSequence => validate_component_sequence( + self.component.as_ref().unwrap(), + self.sequence.as_ref().unwrap(), + Errors::default(), + ), + } + } } fn read_and_convert( @@ -121,6 +148,7 @@ fn run(args: Args) -> Result<(), String> { let inputs = ValidationCliInputs { architecture_json: args.architecture_json, component_fbs: args.component_fbs.unwrap_or_default(), + sequence_fbs: args.sequence_fbs.unwrap_or_default(), class_fbs: args.class_fbs.unwrap_or_default(), }; @@ -144,7 +172,7 @@ fn resolve_validators(context: &ValidationContext) -> Result Errors { - match validator { - SelectedValidator::BazelComponent => { - let (bazel, component) = bazel_component_refs(context) - .expect("BazelComponent validator requires Bazel and component inputs"); - validate_bazel_component(bazel, component, Errors::default()) - } - SelectedValidator::ComponentClass => { - let (component, class) = component_class_refs(context) - .expect("ComponentClass validator requires component and class inputs"); - validate_component_class(component, class, Errors::default()) - } - } -} - fn build_validation_context(inputs: ValidationCliInputs) -> Result { let mut errors = Errors::default(); let bazel = match inputs.architecture_json.as_deref() { @@ -197,32 +210,26 @@ fn build_validation_context(inputs: ValidationCliInputs) -> Result( + inputs.sequence_fbs.as_slice(), + &mut errors, + |raw: SequenceDiagramInputs, errs| raw.to_sequence_diagram_index(errs), + )?; let class = read_and_convert::( inputs.class_fbs.as_slice(), &mut errors, - |raw: ClassDiagramInputs, errs| raw.to_class_diagram_index(errs), + |raw: ClassDiagramInputs, errs| ClassDiagramIndex::build_index(&raw, errs), )?; Ok(ValidationContext { base_errors: errors, bazel, component, + sequence, class, }) } -fn bazel_component_refs( - context: &ValidationContext, -) -> Option<(&BazelArchitecture, &ComponentDiagramArchitecture)> { - Some((context.bazel.as_ref()?, context.component.as_ref()?)) -} - -fn component_class_refs( - context: &ValidationContext, -) -> Option<(&ComponentDiagramArchitecture, &ClassDiagramIndex)> { - Some((context.component.as_ref()?, context.class.as_ref()?)) -} - fn merge_errors(target: &mut Errors, incoming: Errors) { target.messages.extend(incoming.messages); if !incoming.debug_output.is_empty() { diff --git a/validation/core/src/models/class_diagram_models.rs b/validation/core/src/models/class_diagram_models.rs index f5279da8..1b8e69fd 100644 --- a/validation/core/src/models/class_diagram_models.rs +++ b/validation/core/src/models/class_diagram_models.rs @@ -15,72 +15,34 @@ use std::collections::BTreeSet; -use super::Errors; - -/// A single class-diagram entity such as a class, struct, enum, or interface. -#[derive(Debug, Clone, PartialEq)] -pub struct ClassDiagramEntityInput { - pub id: String, - pub name: Option, - pub alias: Option, - pub parent_id: Option, - pub entity_type: String, - pub stereotypes: Vec, - pub template_params: Vec, - pub source_file: Option, - pub source_line: u32, -} +use class_diagram::ClassDiagram as ClassDiagramInput; -/// A relationship edge between two class-diagram entities. -#[derive(Debug, Clone, PartialEq)] -pub struct ClassDiagramRelationshipInput { - pub source: String, - pub target: String, - pub relation_type: String, - pub label: Option, - pub stereotype: Option, - pub source_multiplicity: Option, - pub target_multiplicity: Option, - pub source_role: Option, - pub target_role: Option, -} - -/// One parsed class diagram, including entities, containers, and -/// relationships. -#[derive(Debug, Clone, PartialEq)] -pub struct ClassDiagramInput { - pub name: String, - pub entities: Vec, - pub relationships: Vec, - pub source_files: Vec, - pub version: Option, -} +use super::Errors; /// Collection of class diagrams loaded from one or more FlatBuffer files. -#[derive(Debug, Clone, PartialEq)] -pub struct ClassDiagramInputs { - pub diagrams: Vec, +pub type ClassDiagramInputs = Vec; + +/// Indexed class-diagram data prepared for validators. +pub struct ClassDiagramIndex { + observed_enclosing_namespace_ids: BTreeSet, } -impl ClassDiagramInputs { +impl ClassDiagramIndex { /// Build a [`ClassDiagramIndex`] from class diagram inputs. - pub fn to_class_diagram_index(&self, _errors: &mut Errors) -> ClassDiagramIndex { - let observed_namespace_names = self - .diagrams + pub fn build_index(diagrams: &[ClassDiagramInput], _errors: &mut Errors) -> Self { + let observed_enclosing_namespace_ids = diagrams .iter() .flat_map(|diagram| diagram.entities.iter()) - .filter_map(|entity| entity.parent_id.clone()) - .filter(|parent_id| !parent_id.is_empty()) + .filter_map(|entity| entity.enclosing_namespace_id.clone()) + .filter(|namespace_id| !namespace_id.is_empty()) .collect(); - ClassDiagramIndex { - observed_namespace_names, + Self { + observed_enclosing_namespace_ids, } } -} -/// Indexed names derived from class-diagram entities. -#[derive(Clone)] -pub struct ClassDiagramIndex { - pub observed_namespace_names: BTreeSet, + pub fn enclosing_namespace_ids(&self) -> &BTreeSet { + &self.observed_enclosing_namespace_ids + } } diff --git a/validation/core/src/models/mod.rs b/validation/core/src/models/mod.rs index 303940ab..d48405c8 100644 --- a/validation/core/src/models/mod.rs +++ b/validation/core/src/models/mod.rs @@ -18,15 +18,19 @@ mod bazel_models; mod class_diagram_models; mod component_diagram_models; mod error_models; +mod sequence_diagram_models; mod shared; -pub use bazel_models::{BazelArchitecture, BazelInput, BazelInputEntry}; -pub use class_diagram_models::{ - ClassDiagramEntityInput, ClassDiagramIndex, ClassDiagramInput, ClassDiagramInputs, - ClassDiagramRelationshipInput, -}; +use shared::EntityKey; + +#[cfg(test)] +pub use bazel_models::BazelInputEntry; +pub use bazel_models::{BazelArchitecture, BazelInput}; +pub use class_diagram_models::{ClassDiagramIndex, ClassDiagramInputs}; pub use component_diagram_models::{ ComponentDiagramArchitecture, ComponentDiagramInput, ComponentDiagramInputs, }; pub use error_models::Errors; -pub use shared::EntityKey; +pub use sequence_diagram_models::{ + SequenceDiagramIndex, SequenceDiagramInput, SequenceDiagramInputs, +}; diff --git a/validation/core/src/models/sequence_diagram_models.rs b/validation/core/src/models/sequence_diagram_models.rs new file mode 100644 index 00000000..d185df6c --- /dev/null +++ b/validation/core/src/models/sequence_diagram_models.rs @@ -0,0 +1,88 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Models for sequence-diagram FlatBuffer inputs used by design verification. + +use std::collections::BTreeSet; + +use sequence_logic::{Event, SequenceNode, SequenceTree}; + +use super::Errors; + +/// One parsed sequence diagram from a FlatBuffer file. +pub struct SequenceDiagramInput { + pub tree: SequenceTree, + pub source_files: Vec, + pub version: Option, +} + +/// Collection of sequence diagrams loaded from one or more FlatBuffer files. +pub struct SequenceDiagramInputs { + pub diagrams: Vec, +} + +impl SequenceDiagramInputs { + /// Build a [`SequenceDiagramIndex`] from sequence diagram inputs. + pub fn to_sequence_diagram_index(&self, _errors: &mut Errors) -> SequenceDiagramIndex { + SequenceDiagramIndex::from_diagrams(&self.diagrams) + } +} + +/// Indexed sequence-diagram data prepared for validators. +pub struct SequenceDiagramIndex { + used_participants: BTreeSet, +} + +impl SequenceDiagramIndex { + fn from_diagrams(diagrams: &[SequenceDiagramInput]) -> Self { + let mut used_participants = BTreeSet::new(); + + for diagram in diagrams { + for node in &diagram.tree.root_interactions { + collect_used_participants(node, &mut used_participants); + } + } + + Self { used_participants } + } + + pub fn used_participants(&self) -> &BTreeSet { + &self.used_participants + } +} + +fn collect_used_participants(node: &SequenceNode, out: &mut BTreeSet) { + match &node.event { + Event::Interaction(interaction) => { + if !interaction.caller.is_empty() { + out.insert(interaction.caller.clone()); + } + if !interaction.callee.is_empty() { + out.insert(interaction.callee.clone()); + } + } + Event::Return(ret) => { + if !ret.caller.is_empty() { + out.insert(ret.caller.clone()); + } + if !ret.callee.is_empty() { + out.insert(ret.callee.clone()); + } + } + Event::Condition(_) => {} + } + + for child in &node.branches_node { + collect_used_participants(child, out); + } +} diff --git a/validation/core/src/readers/class_diagram_reader.rs b/validation/core/src/readers/class_diagram_reader.rs index b9a7004c..07c9fbac 100644 --- a/validation/core/src/readers/class_diagram_reader.rs +++ b/validation/core/src/readers/class_diagram_reader.rs @@ -17,73 +17,243 @@ use std::fs; use class_fbs::class_metamodel as fb_class; -use crate::models::{ - ClassDiagramEntityInput, ClassDiagramInput, ClassDiagramInputs, ClassDiagramRelationshipInput, -}; +use crate::models::ClassDiagramInputs; use crate::readers::Reader; +use class_diagram::{ + ClassDiagram, EntityType, EnumLiteral, FunctionArgument, MemberVariable, Method, + MethodModifier, RelationType, Relationship, SimpleEntity, TypeAlias, Visibility, +}; pub struct ClassDiagramReader; -impl ClassDiagramReader { - /// Read all class-diagram files and convert them into validation-friendly - /// Rust models. - pub fn read(paths: &[String]) -> Result { +fn collect_strings( + values: Option>>, +) -> Option> { + values.map(|items| items.iter().map(|value| value.to_string()).collect()) +} + +fn read_type_aliases(entity: fb_class::SimpleEntity<'_>) -> Vec { + entity + .type_aliases() + .map(|values| { + values + .iter() + .map(|value| TypeAlias { + alias: value.alias().to_string(), + original_type: value.original_type().to_string(), + }) + .collect::>() + }) + .unwrap_or_default() +} + +fn read_variables( + entity: fb_class::SimpleEntity<'_>, + path: &str, +) -> Result, String> { + entity + .variables() + .map(|values| { + values + .iter() + .map(|value| { + Ok(MemberVariable { + name: value.name().to_string(), + data_type: value.data_type().map(|s| s.to_string()), + visibility: map_visibility( + value.visibility(), + &format!("{path}:entity:{}:variable:{}", entity.id(), value.name()), + )?, + is_static: value.is_static(), + is_const: value.is_const(), + }) + }) + .collect::, String>>() + }) + .transpose() + .map(|values| values.unwrap_or_default()) +} + +fn read_method( + method: fb_class::Method<'_>, + entity: fb_class::SimpleEntity<'_>, + path: &str, +) -> Result { + let parameters = method + .parameters() + .map(|params| { + params + .iter() + .map(|param| FunctionArgument { + name: param.name().to_string(), + param_type: param.param_type().map(|s| s.to_string()), + is_variadic: param.is_variadic(), + }) + .collect::>() + }) + .unwrap_or_default(); + + let template_parameters = collect_strings(method.template_parameters()); + + let modifiers = method + .modifiers() + .map(|mods| { + mods.iter() + .map(|modifier| { + map_method_modifier( + modifier, + &format!("{path}:entity:{}:method:{}", entity.id(), method.name()), + ) + }) + .collect::, String>>() + }) + .transpose()? + .unwrap_or_default(); + + Ok(Method { + name: method.name().to_string(), + return_type: method.return_type().map(|s| s.to_string()), + visibility: map_visibility( + method.visibility(), + &format!("{path}:entity:{}:method:{}", entity.id(), method.name()), + )?, + parameters, + template_parameters, + modifiers, + }) +} + +fn read_methods(entity: fb_class::SimpleEntity<'_>, path: &str) -> Result, String> { + entity + .methods() + .map(|values| { + values + .iter() + .map(|method| read_method(method, entity, path)) + .collect::, String>>() + }) + .transpose() + .map(|values| values.unwrap_or_default()) +} + +fn read_enum_literals(entity: fb_class::SimpleEntity<'_>) -> Vec { + entity + .enum_literals() + .map(|values| { + values + .iter() + .map(|value| EnumLiteral { + name: value.name().to_string(), + value: value.value().map(|s| s.to_string()), + }) + .collect::>() + }) + .unwrap_or_default() +} + +fn read_entity_relationships( + entity: fb_class::SimpleEntity<'_>, + path: &str, +) -> Result, String> { + entity + .relationships() + .map(|values| { + values + .iter() + .map(|rel| { + read_relationship(rel, &format!("{path}:entity:{}:relationship", entity.id())) + }) + .collect::, String>>() + }) + .transpose() + .map(|values| values.unwrap_or_default()) +} + +fn read_entity(entity: fb_class::SimpleEntity<'_>, path: &str) -> Result { + Ok(SimpleEntity { + id: entity.id().to_string(), + name: entity.name().to_string(), + enclosing_namespace_id: entity.enclosing_namespace_id().map(|s| s.to_string()), + entity_type: map_entity_type( + entity.entity_type(), + &format!("{path}:entity:{}", entity.id()), + )?, + type_aliases: read_type_aliases(entity), + variables: read_variables(entity, path)?, + methods: read_methods(entity, path)?, + template_parameters: collect_strings(entity.template_parameters()), + enum_literals: read_enum_literals(entity), + relationships: read_entity_relationships(entity, path)?, + source_file: entity.source_file().map(|s| s.to_string()), + source_line: if entity.source_line() == 0 { + None + } else { + Some(entity.source_line()) + }, + }) +} + +fn read_entities( + diagram: fb_class::ClassDiagram<'_>, + path: &str, +) -> Result, String> { + diagram + .entities() + .map(|raw_entities| { + raw_entities + .iter() + .map(|entity| read_entity(entity, path)) + .collect::, String>>() + }) + .transpose() + .map(|values| values.unwrap_or_default()) +} + +fn read_relationship( + rel: fb_class::Relationship<'_>, + context: &str, +) -> Result { + Ok(Relationship { + source: rel.source().to_string(), + target: rel.target().to_string(), + relation_type: map_relation_type(rel.relation_type(), context)?, + source_multiplicity: rel.source_multiplicity().map(|s| s.to_string()), + target_multiplicity: rel.target_multiplicity().map(|s| s.to_string()), + }) +} + +impl Reader for ClassDiagramReader { + type Input = [String]; + type Raw = ClassDiagramInputs; + type Error = String; + + fn read(input: &Self::Input) -> Result { let mut diagrams = Vec::new(); - for path in paths { + for path in input { let data = fs::read(path).map_err(|e| format!("Failed to read {path}: {e}"))?; let diagram = flatbuffers::root::(&data) .map_err(|e| format!("Failed to parse class FlatBuffer {path}: {e}"))?; - let mut entities = Vec::new(); - if let Some(raw_entities) = diagram.entities() { - for entity in raw_entities.iter() { - // Rehydrate repeated FlatBuffer string vectors into owned - // Rust values so validators can work without borrow/lifetime - // coupling to the underlying buffer. - let template_params = entity - .template_parameters() - .map(|values| values.iter().map(|p| p.to_string()).collect::>()) - .unwrap_or_default(); - - entities.push(ClassDiagramEntityInput { - id: entity.id().to_string(), - name: Some(entity.name().to_string()), - alias: None, - parent_id: entity.enclosing_namespace_id().map(|s| s.to_string()), - entity_type: format!("{:?}", entity.entity_type()), - stereotypes: Vec::new(), - template_params, - source_file: entity.source_file().map(|s| s.to_string()), - source_line: entity.source_line(), - }); - } - } - - let mut relationships = Vec::new(); - if let Some(raw_rels) = diagram.relationships() { - for rel in raw_rels.iter() { - relationships.push(ClassDiagramRelationshipInput { - source: rel.source().to_string(), - target: rel.target().to_string(), - relation_type: format!("{:?}", rel.relation_type()), - label: None, - stereotype: None, - source_multiplicity: rel.source_multiplicity().map(|s| s.to_string()), - target_multiplicity: rel.target_multiplicity().map(|s| s.to_string()), - source_role: None, - target_role: None, - }); - } - } - - let source_files = diagram - .source_files() - .map(|values| values.iter().map(|f| f.to_string()).collect::>()) + let entities = read_entities(diagram, path)?; + + let relationships = diagram + .relationships() + .map(|rels| { + rels.iter() + .enumerate() + .map(|(index, rel)| { + read_relationship(rel, &format!("{path}:diagram_relationship[{index}]")) + }) + .collect::, String>>() + }) + .transpose()? .unwrap_or_default(); - diagrams.push(ClassDiagramInput { + let source_files = collect_strings(diagram.source_files()).unwrap_or_default(); + + diagrams.push(ClassDiagram { name: diagram.name().to_string(), entities, relationships, @@ -92,16 +262,58 @@ impl ClassDiagramReader { }); } - Ok(ClassDiagramInputs { diagrams }) + Ok(diagrams) } } -impl Reader for ClassDiagramReader { - type Input = [String]; - type Raw = ClassDiagramInputs; - type Error = String; +fn unsupported_enum(context: &str, label: &str, value: T) -> String { + format!("{context}: unsupported {label} {value:?}") +} - fn read(input: &Self::Input) -> Result { - ClassDiagramReader::read(input) +fn map_entity_type(value: fb_class::EntityType, context: &str) -> Result { + match value { + fb_class::EntityType::Class => Ok(EntityType::Class), + fb_class::EntityType::Struct => Ok(EntityType::Struct), + fb_class::EntityType::Interface => Ok(EntityType::Interface), + fb_class::EntityType::AbstractClass => Ok(EntityType::AbstractClass), + fb_class::EntityType::Enum => Ok(EntityType::Enum), + _ => Err(unsupported_enum(context, "entity_type", value)), + } +} + +fn map_visibility(value: fb_class::Visibility, context: &str) -> Result { + match value { + fb_class::Visibility::Public => Ok(Visibility::Public), + fb_class::Visibility::Private => Ok(Visibility::Private), + fb_class::Visibility::Protected => Ok(Visibility::Protected), + _ => Err(unsupported_enum(context, "visibility", value)), + } +} + +fn map_relation_type(value: fb_class::RelationType, context: &str) -> Result { + match value { + fb_class::RelationType::Inheritance => Ok(RelationType::Inheritance), + fb_class::RelationType::Implementation => Ok(RelationType::Implementation), + fb_class::RelationType::Composition => Ok(RelationType::Composition), + fb_class::RelationType::Aggregation => Ok(RelationType::Aggregation), + fb_class::RelationType::Association => Ok(RelationType::Association), + fb_class::RelationType::Dependency => Ok(RelationType::Dependency), + _ => Err(unsupported_enum(context, "relation_type", value)), + } +} + +fn map_method_modifier( + value: fb_class::MethodModifier, + context: &str, +) -> Result { + match value { + fb_class::MethodModifier::Static => Ok(MethodModifier::Static), + fb_class::MethodModifier::Virtual => Ok(MethodModifier::Virtual), + fb_class::MethodModifier::Abstract => Ok(MethodModifier::Abstract), + fb_class::MethodModifier::Override => Ok(MethodModifier::Override), + fb_class::MethodModifier::Constructor => Ok(MethodModifier::Constructor), + fb_class::MethodModifier::Destructor => Ok(MethodModifier::Destructor), + fb_class::MethodModifier::Noexcept => Ok(MethodModifier::Noexcept), + _ => Err(unsupported_enum(context, "method_modifier", value)), } } diff --git a/validation/core/src/readers/mod.rs b/validation/core/src/readers/mod.rs index 3f7b4a98..46a68cca 100644 --- a/validation/core/src/readers/mod.rs +++ b/validation/core/src/readers/mod.rs @@ -16,6 +16,7 @@ mod bazel_reader; mod class_diagram_reader; mod component_diagram_reader; +mod sequence_diagram_reader; pub trait InputPresence { fn is_present(&self) -> bool; @@ -48,3 +49,4 @@ pub trait Reader { pub use bazel_reader::BazelReader; pub use class_diagram_reader::ClassDiagramReader; pub use component_diagram_reader::ComponentDiagramReader; +pub use sequence_diagram_reader::SequenceDiagramReader; diff --git a/validation/core/src/readers/sequence_diagram_reader.rs b/validation/core/src/readers/sequence_diagram_reader.rs new file mode 100644 index 00000000..344d33aa --- /dev/null +++ b/validation/core/src/readers/sequence_diagram_reader.rs @@ -0,0 +1,164 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Reader for sequence-diagram FlatBuffer exports used by design verification. + +use std::fs; + +use sequence_fbs::sequence_metamodel as fb_sequence; +use sequence_logic::{ + Condition, ConditionType, Event, Interaction, Return, SequenceNode, SequenceTree, +}; + +use crate::models::{SequenceDiagramInput, SequenceDiagramInputs}; +use crate::readers::Reader; + +pub struct SequenceDiagramReader; + +impl Reader for SequenceDiagramReader { + type Input = [String]; + type Raw = SequenceDiagramInputs; + type Error = String; + + fn read(input: &Self::Input) -> Result { + let mut diagrams = Vec::new(); + + for path in input { + let data = fs::read(path).map_err(|e| format!("Failed to read {path}: {e}"))?; + + let diagram = flatbuffers::root::(&data) + .map_err(|e| format!("Failed to parse sequence FlatBuffer {path}: {e}"))?; + + let root_interactions = if let Some(nodes) = diagram.root_interactions() { + let mut parsed_nodes = Vec::with_capacity(nodes.len()); + for (index, node) in nodes.iter().enumerate() { + parsed_nodes.push( + read_node(node, &format!("{path}:root[{index}]")) + .map_err(|e| format!("Failed to parse sequence node: {e}"))?, + ); + } + parsed_nodes + } else { + Vec::new() + }; + + let source_files = diagram + .source_files() + .map(|values| values.iter().map(|f| f.to_string()).collect::>()) + .unwrap_or_default(); + + diagrams.push(SequenceDiagramInput { + tree: SequenceTree { + name: diagram.name().map(|s| s.to_string()), + root_interactions, + }, + source_files, + version: diagram.version().map(|s| s.to_string()), + }); + } + + Ok(SequenceDiagramInputs { diagrams }) + } +} + +fn read_node(node: fb_sequence::SequenceNode<'_>, node_path: &str) -> Result { + let event = match node.event_type() { + fb_sequence::Event::Interaction => { + let interaction = node.event_as_interaction().ok_or_else(|| { + format!( + "{node_path}: event_type is Interaction, but interaction payload is missing" + ) + })?; + Event::Interaction(Interaction { + caller: interaction.caller().to_string(), + callee: interaction.callee().to_string(), + method: interaction + .method() + .map(|s| s.to_string()) + .unwrap_or_default(), + }) + } + fb_sequence::Event::Return => { + let ret = node.event_as_return().ok_or_else(|| { + format!("{node_path}: event_type is Return, but return payload is missing") + })?; + Event::Return(Return { + caller: ret.caller().to_string(), + callee: ret.callee().to_string(), + return_content: ret + .return_content() + .map(|s| s.to_string()) + .unwrap_or_default(), + }) + } + fb_sequence::Event::Condition => { + let condition = node.event_as_condition().ok_or_else(|| { + format!("{node_path}: event_type is Condition, but condition payload is missing") + })?; + Event::Condition(Condition { + condition_type: map_condition_type(condition.condition_type(), node_path)?, + condition_value: condition + .condition_value() + .map(|s| s.to_string()) + .unwrap_or_default(), + }) + } + fb_sequence::Event::NONE => { + return Err(format!("{node_path}: event_type is NONE")); + } + _ => { + return Err(format!( + "{node_path}: unsupported event_type {:?}", + node.event_type() + )); + } + }; + + let branches_node = if let Some(children) = node.branches_node() { + let mut parsed_children = Vec::with_capacity(children.len()); + for (index, child) in children.iter().enumerate() { + parsed_children.push(read_node(child, &format!("{node_path}.branches[{index}]"))?); + } + parsed_children + } else { + Vec::new() + }; + + Ok(SequenceNode { + event, + branches_node, + }) +} + +fn map_condition_type( + value: fb_sequence::ConditionType, + node_path: &str, +) -> Result { + match value { + fb_sequence::ConditionType::Opt => Ok(ConditionType::Opt), + fb_sequence::ConditionType::Alt => Ok(ConditionType::Alt), + fb_sequence::ConditionType::Loop => Ok(ConditionType::Loop), + fb_sequence::ConditionType::Par => Ok(ConditionType::Par), + fb_sequence::ConditionType::Par2 => Ok(ConditionType::Par2), + fb_sequence::ConditionType::Break => Ok(ConditionType::Break), + fb_sequence::ConditionType::Critical => Ok(ConditionType::Critical), + fb_sequence::ConditionType::Else => Ok(ConditionType::Else), + fb_sequence::ConditionType::Also => Ok(ConditionType::Also), + fb_sequence::ConditionType::End => Ok(ConditionType::End), + fb_sequence::ConditionType::Group => Ok(ConditionType::Group), + _ => Err(format!( + "{node_path}: unsupported condition_type {:?}", + value + )), + } +} diff --git a/validation/core/src/validators/bazel_component_validator.rs b/validation/core/src/validators/bazel_component_validator.rs index 6ee48981..ac3130eb 100644 --- a/validation/core/src/validators/bazel_component_validator.rs +++ b/validation/core/src/validators/bazel_component_validator.rs @@ -56,7 +56,7 @@ impl<'a> BazelComponentValidator<'a> { /// /// The debug log is always built and stored in `errors.debug_output`. pub fn run(mut self) -> Errors { - self.errors.debug_output = self.build_debug_log(); + self.errors.debug_output.push_str(&self.build_debug_log()); self.check_seooc(); self.check_components(); self.check_units(); diff --git a/validation/core/src/validators/component_class_validator.rs b/validation/core/src/validators/component_class_validator.rs index 174f7f63..c88b368e 100644 --- a/validation/core/src/validators/component_class_validator.rs +++ b/validation/core/src/validators/component_class_validator.rs @@ -11,8 +11,8 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* -//! Validation: compare unit names from component diagrams with namespace names -//! found in class diagrams. +//! Validation: compare component-diagram unit IDs with enclosing +//! namespace IDs found in class diagrams. use std::collections::BTreeSet; @@ -26,36 +26,36 @@ pub fn validate_component_class( errors: Errors, ) -> Errors { ComponentClassValidator::new( - build_expected_unit_names(component_diagram), - &class_diagram.observed_namespace_names, + build_expected_unit_ids(component_diagram), + class_diagram.enclosing_namespace_ids(), errors, ) .run() } -/// Verifies naming consistency between component-diagram units and -/// class-diagram namespaces. -pub struct ComponentClassValidator<'a> { - expected_unit_names: BTreeSet, - observed_namespace_names: &'a BTreeSet, +/// Verifies consistency between component-diagram unit IDs and +/// class-diagram enclosing namespace IDs. +struct ComponentClassValidator<'a> { + expected_unit_ids: BTreeSet, + observed_namespace_ids: &'a BTreeSet, errors: Errors, } impl<'a> ComponentClassValidator<'a> { fn new( - expected_unit_names: BTreeSet, - observed_namespace_names: &'a BTreeSet, + expected_unit_ids: BTreeSet, + observed_namespace_ids: &'a BTreeSet, errors: Errors, ) -> Self { Self { - expected_unit_names, - observed_namespace_names, + expected_unit_ids, + observed_namespace_ids, errors, } } /// Run the consistency check and return accumulated errors. pub fn run(mut self) -> Errors { - self.errors.debug_output = self.build_debug_log(); + self.errors.debug_output.push_str(&self.build_debug_log()); self.check_unit_naming_consistency(); self.errors } @@ -63,70 +63,88 @@ impl<'a> ComponentClassValidator<'a> { fn build_debug_log(&self) -> String { let mut log = String::new(); - log.push_str("DEBUG: Expected unit aliases from component diagrams:\n"); - for name in &self.expected_unit_names { - log.push_str(&format!(" {name}\n")); + log.push_str("DEBUG: Expected unit IDs from component diagrams:\n"); + for unit_id in &self.expected_unit_ids { + log.push_str(&format!(" {unit_id}\n")); } - log.push_str("DEBUG: Observed namespace IDs from class diagrams:\n"); - for name in self.observed_namespace_names { - log.push_str(&format!(" {name}\n")); + log.push_str("DEBUG: Observed enclosing namespace IDs from class diagrams:\n"); + for namespace_id in self.observed_namespace_ids { + log.push_str(&format!(" {namespace_id}\n")); } log } fn check_unit_naming_consistency(&mut self) { - // Present in component diagrams but missing as namespaces in class - // diagrams. - for missing_name in self - .expected_unit_names - .difference(self.observed_namespace_names) - { + // Every expected unit ID must end with at least one observed namespace + // ID. + for expected_unit_id in &self.expected_unit_ids { + let has_matching_suffix = + self.observed_namespace_ids + .iter() + .any(|observed_namespace_id| { + has_boundary_suffix(expected_unit_id, observed_namespace_id) + }); + + if has_matching_suffix { + continue; + } + self.errors.push(format!( - "Naming consistency violation: missing unit namespace in class diagrams:\n\ - Expected unit name: \"{}\"\n\ - Source : Component diagram unit identifiers\n\ - Action : Add/rename class-diagram namespace to match the unit name", - missing_name + "Naming consistency violation: no enclosing namespace ID suffix match for component unit ID:\n\ + Expected unit ID : \"{}\"\n\ + Source : Component diagram unit IDs\n\ + Action : Add/rename class-diagram enclosing namespace ID so it matches a suffix of this unit ID", + expected_unit_id )); } - // Present as class-diagram namespaces but not declared as component - // units. - for extra_name in self - .observed_namespace_names - .difference(&self.expected_unit_names) - { + // Every observed namespace ID must be a suffix of at least one + // expected unit ID. + for observed_namespace_id in self.observed_namespace_ids { + let has_matching_suffix = self.expected_unit_ids.iter().any(|expected_unit_id| { + has_boundary_suffix(expected_unit_id, observed_namespace_id) + }); + + if has_matching_suffix { + continue; + } + self.errors.push(format!( - "Naming consistency violation: unexpected class-diagram unit namespace:\n\ - Namespace name : \"{}\"\n\ - Source : Unit class diagrams\n\ - Action : Rename namespace to an existing component-diagram unit identifier", - extra_name + "Naming consistency violation: enclosing namespace ID is not a suffix of any component unit ID:\n\ + Namespace ID : \"{}\"\n\ + Source : Class-diagram enclosing namespace IDs\n\ + Action : Rename namespace ID or component unit ID so the namespace ID becomes a suffix of a unit ID", + observed_namespace_id )); } } } -fn build_expected_unit_names(component_diagram: &ComponentDiagramArchitecture) -> BTreeSet { - // Unit aliases define expected logical names directly. Parent hierarchy is +fn has_boundary_suffix(full_id: &str, suffix: &str) -> bool { + full_id == suffix + || (full_id.len() > suffix.len() + && full_id.ends_with(suffix) + && full_id.as_bytes()[full_id.len() - suffix.len() - 1] == b'.') +} + +fn build_expected_unit_ids(component_diagram: &ComponentDiagramArchitecture) -> BTreeSet { + // Unit IDs define expected logical names directly. Parent hierarchy is // intentionally ignored. component_diagram .entities .iter() .filter(|entity| entity.is_unit()) - .filter_map(|entity| entity.alias.clone()) + .map(|entity| entity.id.clone()) .collect() } #[cfg(test)] mod tests { use super::*; - use crate::models::{ - ClassDiagramEntityInput, ClassDiagramInput, ClassDiagramInputs, ComponentDiagramInput, - ComponentDiagramInputs, - }; + use crate::models::{ClassDiagramInputs, ComponentDiagramInput, ComponentDiagramInputs}; + use class_diagram::{ClassDiagram, EntityType, SimpleEntity}; fn component_diagrams(units: &[&str]) -> ComponentDiagramInputs { ComponentDiagramInputs { @@ -159,29 +177,30 @@ mod tests { } fn class_diagrams(namespaces: &[&str]) -> ClassDiagramInputs { - ClassDiagramInputs { - diagrams: vec![ClassDiagramInput { - name: "diagram".to_string(), - entities: namespaces - .iter() - .enumerate() - .map(|(index, parent_id)| ClassDiagramEntityInput { - id: format!("entity_{index}"), - name: Some(format!("entity_{index}")), - alias: None, - parent_id: Some((*parent_id).to_string()), - entity_type: "Class".to_string(), - stereotypes: Vec::new(), - template_params: Vec::new(), - source_file: None, - source_line: 0, - }) - .collect(), - relationships: Vec::new(), - source_files: Vec::new(), - version: None, - }], - } + vec![ClassDiagram { + name: "diagram".to_string(), + entities: namespaces + .iter() + .enumerate() + .map(|(index, namespace_id)| SimpleEntity { + id: format!("entity_{index}"), + name: format!("entity_{index}"), + enclosing_namespace_id: Some((*namespace_id).to_string()), + entity_type: EntityType::Class, + type_aliases: Vec::new(), + variables: Vec::new(), + methods: Vec::new(), + template_parameters: None, + enum_literals: Vec::new(), + relationships: Vec::new(), + source_file: None, + source_line: None, + }) + .collect(), + relationships: Vec::new(), + source_files: Vec::new(), + version: None, + }] } fn run_component_class_validation( @@ -190,37 +209,11 @@ mod tests { ) -> Errors { let mut errors = Errors::default(); let component_arch = component_diagrams.to_diagram_architecture(&mut errors); - let class_index = class_diagrams.to_class_diagram_index(&mut errors); + let class_index = ClassDiagramIndex::build_index(class_diagrams.as_slice(), &mut errors); validate_component_class(&component_arch, &class_index, errors) } - fn class_diagrams_from_entity_parent_ids(parent_ids: &[&str]) -> ClassDiagramInputs { - ClassDiagramInputs { - diagrams: vec![ClassDiagramInput { - name: "diagram".to_string(), - entities: parent_ids - .iter() - .enumerate() - .map(|(index, parent_id)| ClassDiagramEntityInput { - id: format!("entity_{index}"), - name: Some(format!("entity_{index}")), - alias: None, - parent_id: Some((*parent_id).to_string()), - entity_type: "Class".to_string(), - stereotypes: Vec::new(), - template_params: Vec::new(), - source_file: None, - source_line: 0, - }) - .collect(), - relationships: Vec::new(), - source_files: Vec::new(), - version: None, - }], - } - } - #[test] fn naming_consistency_passes_for_exact_match() { let component_diagrams = component_diagrams(&["unit_1", "Unit_2"]); @@ -244,12 +237,14 @@ mod tests { let missing_count = errors .messages .iter() - .filter(|message| message.contains("missing unit namespace in class diagrams")) + .filter(|message| { + message.contains("no enclosing namespace ID suffix match for component unit ID") + }) .count(); let unexpected_count = errors .messages .iter() - .filter(|message| message.contains("unexpected class-diagram unit namespace")) + .filter(|message| message.contains("is not a suffix of any component unit ID")) .count(); assert_eq!(missing_count, 2); @@ -257,63 +252,118 @@ mod tests { } #[test] - fn units_without_alias_are_skipped() { - let component_diagrams = ComponentDiagramInputs { - entities: vec![ - ComponentDiagramInput { - id: "unit_with_alias".to_string(), - alias: Some("unit_with_alias".to_string()), - parent_id: None, - stereotype: Some("unit".to_string()), - }, - ComponentDiagramInput { - id: "unit_without_alias".to_string(), - alias: None, - parent_id: None, - stereotype: Some("unit".to_string()), - }, - ], - }; - - let class_diagrams = class_diagrams(&["unit_with_alias"]); + fn entity_enclosing_namespace_ids_are_used_as_observed_namespaces() { + let component_diagrams = component_diagrams(&["unit_1"]); + let class_diagrams = class_diagrams(&["unit_1"]); let errors = run_component_class_validation(&component_diagrams, &class_diagrams); assert!( errors.is_empty(), - "Expected pass when unit without alias is ignored, got: {:?}", + "Expected pass when entity parent IDs match unit aliases, got: {:?}", errors.messages ); } #[test] - fn entity_parent_ids_are_used_as_observed_namespaces() { - let component_diagrams = component_diagrams(&["unit_1"]); - let class_diagrams = class_diagrams_from_entity_parent_ids(&["unit_1"]); + fn parent_unit_aliases_are_not_prefixed_into_expected_names() { + let component_diagrams = component_diagrams_with_hierarchy(&[ + ("component_1", Some("component_1"), None, "component"), + ( + "component_1.parent", + Some("parent"), + Some("component_1"), + "unit", + ), + ( + "component_1.parent.child", + Some("child"), + Some("component_1.parent"), + "unit", + ), + ( + "component_1.parent.child.leaf", + Some("leaf"), + Some("component_1.parent.child"), + "unit", + ), + ]); + let class_diagrams = class_diagrams(&["parent", "child", "leaf"]); let errors = run_component_class_validation(&component_diagrams, &class_diagrams); + assert!( errors.is_empty(), - "Expected pass when entity parent IDs match unit aliases, got: {:?}", + "Expected pass when namespace IDs match unit ID suffixes on boundaries, got: {:?}", errors.messages ); } #[test] - fn parent_unit_aliases_are_not_prefixed_into_expected_names() { + fn suffix_matching_passes_when_namespace_ids_match_unit_id_suffixes() { let component_diagrams = component_diagrams_with_hierarchy(&[ - ("component_1", Some("component_1"), None, "component"), - ("unit_parent", Some("parent"), Some("component_1"), "unit"), - ("unit_child", Some("child"), Some("unit_parent"), "unit"), - ("unit_leaf", Some("leaf"), Some("unit_child"), "unit"), + ("module_a.subsystem.unit_1", Some("u1"), None, "unit"), + ("module_b.unit_2", Some("u2"), None, "unit"), ]); - let class_diagrams = class_diagrams_from_entity_parent_ids(&["parent", "child", "leaf"]); + let class_diagrams = class_diagrams(&["unit_1", "unit_2"]); let errors = run_component_class_validation(&component_diagrams, &class_diagrams); - assert!( errors.is_empty(), - "Expected pass when only direct unit aliases are compared, got: {:?}", + "Expected pass when namespace IDs are suffixes of unit IDs, got: {:?}", errors.messages ); } + + #[test] + fn reports_missing_when_expected_unit_id_has_no_suffix_match() { + let component_diagrams = component_diagrams_with_hierarchy(&[( + "module_a.subsystem.unit_1", + Some("u1"), + None, + "unit", + )]); + let class_diagrams = class_diagrams(&["unit_2"]); + + let errors = run_component_class_validation(&component_diagrams, &class_diagrams); + assert!(!errors.is_empty()); + assert_eq!(errors.messages.len(), 2); + assert!(errors.messages.iter().any(|message| { + message.contains("no enclosing namespace ID suffix match for component unit ID") + && message.contains("module_a.subsystem.unit_1") + })); + } + + #[test] + fn reports_unexpected_when_namespace_is_not_suffix_of_any_unit_id() { + let component_diagrams = component_diagrams_with_hierarchy(&[( + "module_a.subsystem.unit_1", + Some("u1"), + None, + "unit", + )]); + let class_diagrams = class_diagrams(&["unit_1", "orphan"]); + + let errors = run_component_class_validation(&component_diagrams, &class_diagrams); + assert!(!errors.is_empty()); + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages.iter().any(|message| { + message.contains("is not a suffix of any component unit ID") + && message.contains("Namespace ID : \"orphan\"") + })); + } + + #[test] + fn partial_suffix_without_namespace_boundary_does_not_match() { + let component_diagrams = component_diagrams_with_hierarchy(&[( + "module_a.subsystem.unit_1", + Some("u1"), + None, + "unit", + )]); + let class_diagrams = class_diagrams(&["it_1"]); + + let errors = run_component_class_validation(&component_diagrams, &class_diagrams); + assert!(!errors.is_empty()); + assert_eq!(errors.messages.len(), 2); + } } diff --git a/validation/core/src/validators/component_sequence_validator.rs b/validation/core/src/validators/component_sequence_validator.rs index 7a961c29..104389d3 100644 --- a/validation/core/src/validators/component_sequence_validator.rs +++ b/validation/core/src/validators/component_sequence_validator.rs @@ -11,13 +11,236 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* -//! Sequence verification entrypoint placeholder. +//! Validation: compare component-diagram unit IDs with sequence-diagram +//! used participant IDs. -use crate::models::Errors; +use std::collections::BTreeSet; -/// Sequence validation placeholder. -pub fn validate_component_sequence() -> Errors { - let mut errors = Errors::default(); - errors.push("Sequence validation is not implemented yet".to_string()); - errors +use crate::models::{ComponentDiagramArchitecture, Errors, SequenceDiagramIndex}; + +/// Run component-vs-sequence naming validation. +pub fn validate_component_sequence( + component_diagram: &ComponentDiagramArchitecture, + sequence_diagram: &SequenceDiagramIndex, + errors: Errors, +) -> Errors { + ComponentSequenceValidator::new( + build_expected_unit_aliases(component_diagram), + sequence_diagram.used_participants(), + errors, + ) + .run() +} + +struct ComponentSequenceValidator<'a> { + expected_unit_aliases: BTreeSet, + observed_participants: &'a BTreeSet, + errors: Errors, +} + +impl<'a> ComponentSequenceValidator<'a> { + fn new( + expected_unit_aliases: BTreeSet, + observed_participants: &'a BTreeSet, + errors: Errors, + ) -> Self { + Self { + expected_unit_aliases, + observed_participants, + errors, + } + } + + fn run(mut self) -> Errors { + self.errors.debug_output = self.build_debug_log(); + self.check_consistency(); + self.errors + } + + fn build_debug_log(&self) -> String { + let mut log = String::new(); + + log.push_str("DEBUG: Expected unit aliases from component diagrams:\n"); + for alias in &self.expected_unit_aliases { + log.push_str(&format!(" {alias}\n")); + } + + log.push_str("DEBUG: Observed participants from sequence diagrams:\n"); + for participant in self.observed_participants { + log.push_str(&format!(" {participant}\n")); + } + + log + } + + fn check_consistency(&mut self) { + for alias in &self.expected_unit_aliases { + if !self.observed_participants.contains(alias) { + self.errors.push(format!( + "Naming consistency violation: component unit alias not found in sequence participants:\n\ + Unit alias : \"{alias}\"\n\ + Source : Component diagram unit aliases\n\ + Action : Add a matching sequence participant for this unit alias", + )); + } + } + + for participant in self.observed_participants { + if !self.expected_unit_aliases.contains(participant) { + self.errors.push(format!( + "Naming consistency violation: sequence participant not found in component unit aliases:\n\ + Participant : \"{participant}\"\n\ + Source : Sequence diagram participants\n\ + Action : Add a matching component unit alias or remove this participant", + )); + } + } + } +} + +fn build_expected_unit_aliases( + component_diagram: &ComponentDiagramArchitecture, +) -> BTreeSet { + component_diagram + .entities + .iter() + .filter(|entity| entity.is_unit()) + .filter_map(|entity| entity.alias.clone()) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::{ + ComponentDiagramInput, ComponentDiagramInputs, SequenceDiagramInput, SequenceDiagramInputs, + }; + use sequence_logic::{Event, Interaction, SequenceNode, SequenceTree}; + + fn component_diagrams(aliases: &[&str]) -> ComponentDiagramInputs { + ComponentDiagramInputs { + entities: aliases + .iter() + .map(|alias| ComponentDiagramInput { + id: format!("some_id.{alias}"), + alias: Some((*alias).to_string()), + parent_id: None, + stereotype: Some("unit".to_string()), + }) + .collect(), + } + } + + fn sequence_diagrams(participants: &[&str]) -> SequenceDiagramInputs { + SequenceDiagramInputs { + diagrams: vec![SequenceDiagramInput { + tree: SequenceTree { + name: Some("seq".to_string()), + root_interactions: participants + .iter() + .map(|participant| SequenceNode { + event: Event::Interaction(Interaction { + caller: (*participant).to_string(), + callee: (*participant).to_string(), + method: String::new(), + }), + branches_node: Vec::new(), + }) + .collect(), + }, + source_files: Vec::new(), + version: None, + }], + } + } + + #[test] + fn passes_when_aliases_and_participants_are_identical() { + let component_diagrams = component_diagrams(&["unit_1", "unit_2"]); + let sequence_diagrams = sequence_diagrams(&["unit_1", "unit_2"]); + + let mut errors = Errors::default(); + let component_arch = component_diagrams.to_diagram_architecture(&mut errors); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); + + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); + assert!(errors.is_empty()); + } + + #[test] + fn reports_missing_and_extra() { + let component_diagrams = component_diagrams(&["unit_1", "unit_2", "unit_3"]); + let sequence_diagrams = sequence_diagrams(&["unit_2", "unit_4"]); + + let mut errors = Errors::default(); + let component_arch = component_diagrams.to_diagram_architecture(&mut errors); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); + + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); + + assert!(!errors.is_empty()); + assert_eq!(errors.messages.len(), 3); + + let missing_count = errors + .messages + .iter() + .filter(|msg| msg.contains("unit alias not found in sequence participants")) + .count(); + let unexpected_count = errors + .messages + .iter() + .filter(|msg| msg.contains("sequence participant not found in component unit aliases")) + .count(); + + assert_eq!(missing_count, 2); + assert_eq!(unexpected_count, 1); + } + + #[test] + fn units_without_alias_are_ignored() { + let component_diagrams = ComponentDiagramInputs { + entities: vec![ComponentDiagramInput { + id: "module_a.unit_1".to_string(), + alias: None, + parent_id: None, + stereotype: Some("unit".to_string()), + }], + }; + let sequence_diagrams = sequence_diagrams(&[]); + + let mut errors = Errors::default(); + let component_arch = component_diagrams.to_diagram_architecture(&mut errors); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); + + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); + assert!(errors.is_empty()); + } + + #[test] + fn reports_alias_missing_from_participants() { + let component_diagrams = component_diagrams(&["u1", "u2"]); + let sequence_diagrams = sequence_diagrams(&["u1"]); + + let mut errors = Errors::default(); + let component_arch = component_diagrams.to_diagram_architecture(&mut errors); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); + + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0].contains("\"u2\"")); + } + + #[test] + fn reports_participant_not_in_aliases() { + let component_diagrams = component_diagrams(&["u1"]); + let sequence_diagrams = sequence_diagrams(&["u1", "orphan"]); + + let mut errors = Errors::default(); + let component_arch = component_diagrams.to_diagram_architecture(&mut errors); + let sequence_index = sequence_diagrams.to_sequence_diagram_index(&mut errors); + + let errors = validate_component_sequence(&component_arch, &sequence_index, errors); + assert_eq!(errors.messages.len(), 1); + assert!(errors.messages[0].contains("\"orphan\"")); + } } diff --git a/validation/core/src/validators/mod.rs b/validation/core/src/validators/mod.rs index f4d401fd..924bd357 100644 --- a/validation/core/src/validators/mod.rs +++ b/validation/core/src/validators/mod.rs @@ -13,15 +13,16 @@ //! Validator entrypoints for architecture checks. -pub mod bazel_component_validator; -pub mod component_class_validator; -pub mod component_sequence_validator; +mod bazel_component_validator; +mod component_class_validator; +mod component_sequence_validator; /// Typed inputs that a validator may require to run. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum RequiredInput { Bazel, Component, + Sequence, Class, } @@ -30,36 +31,31 @@ pub enum RequiredInput { pub enum SelectedValidator { BazelComponent, ComponentClass, + ComponentSequence, } -pub const ALL_VALIDATORS: [SelectedValidator; 2] = [ +pub const ALL_VALIDATORS: [SelectedValidator; 3] = [ SelectedValidator::BazelComponent, SelectedValidator::ComponentClass, + SelectedValidator::ComponentSequence, ]; -/// Validator metadata and execution contract used by orchestrators. -pub trait ValidatorSpec { - fn required_inputs(self) -> &'static [RequiredInput]; +impl SelectedValidator { + pub fn required_inputs(self) -> &'static [RequiredInput] { + match self { + Self::BazelComponent => &[RequiredInput::Bazel, RequiredInput::Component], + Self::ComponentClass => &[RequiredInput::Component, RequiredInput::Class], + Self::ComponentSequence => &[RequiredInput::Component, RequiredInput::Sequence], + } + } - fn can_run(self, is_available: impl Fn(RequiredInput) -> bool) -> bool - where - Self: Sized, - { + pub fn can_run(self, is_available: impl Fn(RequiredInput) -> bool) -> bool { self.required_inputs() .iter() .all(|input| is_available(*input)) } } -impl ValidatorSpec for SelectedValidator { - fn required_inputs(self) -> &'static [RequiredInput] { - match self { - SelectedValidator::BazelComponent => &[RequiredInput::Bazel, RequiredInput::Component], - SelectedValidator::ComponentClass => &[RequiredInput::Component, RequiredInput::Class], - } - } -} - -pub use bazel_component_validator::{validate_bazel_component, BazelComponentValidator}; -pub use component_class_validator::{validate_component_class, ComponentClassValidator}; +pub use bazel_component_validator::validate_bazel_component; +pub use component_class_validator::validate_component_class; pub use component_sequence_validator::validate_component_sequence;