From d32ecb0a585e65ab2544d4ea7864f4a6d7c503d5 Mon Sep 17 00:00:00 2001 From: Melody Ma Date: Fri, 17 Jul 2026 11:25:29 +0800 Subject: [PATCH] implement validation for public api and static design --- .../examples/seooc/design/public_api.puml | 7 +- .../examples/seooc/design/static_design.puml | 4 +- .../sample_fmea_failure_modes.trlc | 4 +- .../private/architectural_design.bzl | 10 +- bazel/rules/rules_score/providers.bzl | 1 + validation/core/BUILD | 2 + .../component_public_api/BUILD | 40 +++++ .../component_public_api_suite.rs | 75 ++++++++ .../negative_case_sensitive/BUILD | 38 ++++ .../negative_case_sensitive/expected.yaml | 16 ++ .../negative_case_sensitive/public_api.puml | 20 +++ .../static_design.puml | 27 +++ .../BUILD | 38 ++++ .../expected.yaml | 17 ++ .../public_api.puml | 20 +++ .../static_design.puml | 24 +++ .../negative_public_api_missing/BUILD | 38 ++++ .../negative_public_api_missing/expected.yaml | 16 ++ .../public_api.puml | 20 +++ .../static_design.puml | 30 ++++ .../negative_public_api_wrong_type/BUILD | 38 ++++ .../expected.yaml | 16 ++ .../public_api.puml | 20 +++ .../static_design.puml | 25 +++ .../positive_public_api_match/BUILD | 37 ++++ .../positive_public_api_match/expected.yaml | 13 ++ .../positive_public_api_match/public_api.puml | 20 +++ .../static_design.puml | 27 +++ .../core/integration_test/puml_fixture.bzl | 4 + .../core/src/models/class_diagram_models.rs | 28 +++ validation/core/src/models/mod.rs | 2 +- .../core/src/profiles/architectural_design.rs | 17 +- .../component_public_api_validator.rs | 165 ++++++++++++++++++ validation/core/src/validators/mod.rs | 2 + .../component_public_api_validator_test.rs | 145 +++++++++++++++ 35 files changed, 991 insertions(+), 15 deletions(-) create mode 100644 validation/core/integration_test/component_public_api/BUILD create mode 100644 validation/core/integration_test/component_public_api/component_public_api_suite.rs create mode 100644 validation/core/integration_test/component_public_api/negative_case_sensitive/BUILD create mode 100644 validation/core/integration_test/component_public_api/negative_case_sensitive/expected.yaml create mode 100644 validation/core/integration_test/component_public_api/negative_case_sensitive/public_api.puml create mode 100644 validation/core/integration_test/component_public_api/negative_case_sensitive/static_design.puml create mode 100644 validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/BUILD create mode 100644 validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/expected.yaml create mode 100644 validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/public_api.puml create mode 100644 validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/static_design.puml create mode 100644 validation/core/integration_test/component_public_api/negative_public_api_missing/BUILD create mode 100644 validation/core/integration_test/component_public_api/negative_public_api_missing/expected.yaml create mode 100644 validation/core/integration_test/component_public_api/negative_public_api_missing/public_api.puml create mode 100644 validation/core/integration_test/component_public_api/negative_public_api_missing/static_design.puml create mode 100644 validation/core/integration_test/component_public_api/negative_public_api_wrong_type/BUILD create mode 100644 validation/core/integration_test/component_public_api/negative_public_api_wrong_type/expected.yaml create mode 100644 validation/core/integration_test/component_public_api/negative_public_api_wrong_type/public_api.puml create mode 100644 validation/core/integration_test/component_public_api/negative_public_api_wrong_type/static_design.puml create mode 100644 validation/core/integration_test/component_public_api/positive_public_api_match/BUILD create mode 100644 validation/core/integration_test/component_public_api/positive_public_api_match/expected.yaml create mode 100644 validation/core/integration_test/component_public_api/positive_public_api_match/public_api.puml create mode 100644 validation/core/integration_test/component_public_api/positive_public_api_match/static_design.puml create mode 100644 validation/core/src/validators/component_public_api_validator.rs create mode 100644 validation/core/src/validators/test/component_public_api_validator_test.rs diff --git a/bazel/rules/rules_score/examples/seooc/design/public_api.puml b/bazel/rules/rules_score/examples/seooc/design/public_api.puml index e4bdcb66..61818294 100644 --- a/bazel/rules/rules_score/examples/seooc/design/public_api.puml +++ b/bazel/rules/rules_score/examples/seooc/design/public_api.puml @@ -13,9 +13,10 @@ @startuml -package "SampleLibraryAPI" as SampleLibraryAPI { - interface "GetNumber" as GetNumber - ' interface "SetNumber" as SetNumber +namespace safety_software_seooc_example { + interface "SampleLibraryAPI" as SampleLibraryAPI { + + GetNumber(): int + } } @enduml diff --git a/bazel/rules/rules_score/examples/seooc/design/static_design.puml b/bazel/rules/rules_score/examples/seooc/design/static_design.puml index 71a3bcf5..d6319e4c 100644 --- a/bazel/rules/rules_score/examples/seooc/design/static_design.puml +++ b/bazel/rules/rules_score/examples/seooc/design/static_design.puml @@ -24,8 +24,8 @@ package "Safety Software SEooC Example" as safety_software_seooc_example < SampleLibraryAPI +safety_software_seooc_example )-d- SampleLibraryAPI @enduml diff --git a/bazel/rules/rules_score/examples/seooc/safety_analysis/sample_fmea_failure_modes.trlc b/bazel/rules/rules_score/examples/seooc/safety_analysis/sample_fmea_failure_modes.trlc index c95ab6f2..627258f9 100644 --- a/bazel/rules/rules_score/examples/seooc/safety_analysis/sample_fmea_failure_modes.trlc +++ b/bazel/rules/rules_score/examples/seooc/safety_analysis/sample_fmea_failure_modes.trlc @@ -20,7 +20,7 @@ ScoreReq.FailureMode SampleFailureMode{ failureeffect = "The world as we know it will end" version = 1 safety = ScoreReq.Asil.B - interface = "SampleLibraryAPI.GetNumber" + interface = "safety_software_seooc_example.SampleLibraryAPI.GetNumber" } ScoreReq.FailureMode SampleFailureMode2{ @@ -29,5 +29,5 @@ ScoreReq.FailureMode SampleFailureMode2{ failureeffect = "Downstream consumers time out" version = 1 safety = ScoreReq.Asil.D - interface = "SampleLibraryAPI.GetNumber" + interface = "safety_software_seooc_example.SampleLibraryAPI.GetNumber" } diff --git a/bazel/rules/rules_score/private/architectural_design.bzl b/bazel/rules/rules_score/private/architectural_design.bzl index b8a1b69c..47c62ed2 100644 --- a/bazel/rules/rules_score/private/architectural_design.bzl +++ b/bazel/rules/rules_score/private/architectural_design.bzl @@ -42,7 +42,6 @@ def _run_puml_parser(ctx, puml_file): Args: ctx: Rule context puml_file: The .puml File object to parse - Returns: Tuple of (fbs_output, lobster_output) declared output Files. """ @@ -79,7 +78,6 @@ def _parse_puml_diagrams(ctx, files): Args: ctx: Rule context files: List of File objects - Returns: Tuple of (fbs_outputs, lobster_outputs) lists of generated Files. """ @@ -92,13 +90,14 @@ def _parse_puml_diagrams(ctx, files): lobster_outputs.append(lobster) return fbs_outputs, lobster_outputs -def _run_validation(ctx, component_fbs_files, sequence_fbs_files, internal_api_fbs_files): +def _run_validation(ctx, component_fbs_files, sequence_fbs_files, public_api_fbs_files, internal_api_fbs_files): """Run the architectural-design validation profile. Args: ctx: Rule context component_fbs_files: Component-diagram FlatBuffer files generated from this target's static inputs. sequence_fbs_files: Sequence-diagram FlatBuffer files generated from this target's dynamic inputs. + public_api_fbs_files: List of public-API FlatBuffer files generated from this target's public_api inputs. internal_api_fbs_files: List of internal-API FlatBuffer files generated from this target's internal_api inputs. Returns: Struct with file and name fields describing the validation log entry. @@ -111,9 +110,10 @@ def _run_validation(ctx, component_fbs_files, sequence_fbs_files, internal_api_f input_bundle = { "component_diagrams": [f.path for f in component_fbs_files], "sequence_diagrams": [f.path for f in sequence_fbs_files], + "public_api_diagrams": [f.path for f in public_api_fbs_files], "internal_api_diagrams": [f.path for f in internal_api_fbs_files], }, - inputs = component_fbs_files + sequence_fbs_files + internal_api_fbs_files, + inputs = component_fbs_files + sequence_fbs_files + public_api_fbs_files + internal_api_fbs_files, mnemonic = "ArchitecturalDesignValidate", maturity = ctx.attr.maturity, log_level = get_log_level(ctx), @@ -193,6 +193,7 @@ def _architectural_design_impl(ctx): ctx, static_fbs_list, dynamic_fbs_list, + public_api_fbs_list, internal_api_fbs_list, ) @@ -203,6 +204,7 @@ def _architectural_design_impl(ctx): ArchitecturalDesignInfo( static = static_fbs, dynamic = dynamic_fbs, + public_api = public_api_fbs, internal_api = internal_api_fbs, name = ctx.label.name, public_api_lobster_files = public_api_lobster, diff --git a/bazel/rules/rules_score/providers.bzl b/bazel/rules/rules_score/providers.bzl index 043060e8..735f7ad4 100644 --- a/bazel/rules/rules_score/providers.bzl +++ b/bazel/rules/rules_score/providers.bzl @@ -185,6 +185,7 @@ ArchitecturalDesignInfo = provider( fields = { "static": "Depset of FlatBuffers binaries for static architecture diagrams (class diagrams, component diagrams, etc.)", "dynamic": "Depset of FlatBuffers binaries for dynamic architecture diagrams (sequence diagrams, activity diagrams, etc.)", + "public_api": "Depset of FlatBuffers binaries for public API diagrams (class diagrams, etc.)", "internal_api": "Depset of FlatBuffers binaries for internal API diagrams (class diagrams, etc.)", "name": "Name of the architectural design target", "public_api_lobster_files": "Depset of .lobster traceability files generated from public_api diagrams.", diff --git a/validation/core/BUILD b/validation/core/BUILD index b02efb8b..415b5636 100644 --- a/validation/core/BUILD +++ b/validation/core/BUILD @@ -57,6 +57,7 @@ rust_library( "src/validators/bazel_component_validator.rs", "src/validators/class_design_implementation_validator.rs", "src/validators/component_internal_api_validator.rs", + "src/validators/component_public_api_validator.rs", "src/validators/component_sequence_validator.rs", "src/validators/mod.rs", "src/validators/sequence_internal_api_validator.rs", @@ -64,6 +65,7 @@ rust_library( "src/validators/shared/helpers.rs", "src/validators/shared/mod.rs", "src/validators/test/component_internal_api_validator_test.rs", + "src/validators/test/component_public_api_validator_test.rs", "src/validators/test/component_sequence_validator_test.rs", "src/validators/test/fixtures.rs", "src/validators/test/sequence_internal_api_validator_test.rs", diff --git a/validation/core/integration_test/component_public_api/BUILD b/validation/core/integration_test/component_public_api/BUILD new file mode 100644 index 00000000..95b8cb37 --- /dev/null +++ b/validation/core/integration_test/component_public_api/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("@rules_rust//rust:defs.bzl", "rust_test") + +filegroup( + name = "component_public_api_test_data", + srcs = [ + "//validation/core/integration_test/component_public_api/negative_case_sensitive:case_data", + "//validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship:case_data", + "//validation/core/integration_test/component_public_api/negative_public_api_missing:case_data", + "//validation/core/integration_test/component_public_api/negative_public_api_wrong_type:case_data", + "//validation/core/integration_test/component_public_api/positive_public_api_match:case_data", + ], +) + +rust_test( + name = "component_public_api_integration_test", + srcs = ["component_public_api_suite.rs"], + crate_root = "component_public_api_suite.rs", + data = [ + ":component_public_api_test_data", + ], + deps = [ + "//validation/core:validation_cli", + "//validation/core/integration_test:test_framework", + "@crates//:serde", + "@crates//:serde_json", + ], +) diff --git a/validation/core/integration_test/component_public_api/component_public_api_suite.rs b/validation/core/integration_test/component_public_api/component_public_api_suite.rs new file mode 100644 index 00000000..0a3de71c --- /dev/null +++ b/validation/core/integration_test/component_public_api/component_public_api_suite.rs @@ -0,0 +1,75 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +use test_framework::{ + assert_cli_result, collect_case_fbs_files, load_expected_yaml_fixture, run_validation_profile, + CliRunResult, +}; + +const SUITE_DIR: &str = "component_public_api"; + +fn run_case_from_cli( + case_dir: &str, + component_fbs_paths: &[String], + public_api_fbs_paths: &[String], +) -> CliRunResult { + run_validation_profile( + &format!("component_public_api_{case_dir}"), + "architectural-design", + serde_json::json!({ + "component_diagrams": component_fbs_paths, + "public_api_diagrams": public_api_fbs_paths, + }), + ) +} + +fn assert_case(case_dir: &str) { + let expected = load_expected_yaml_fixture(SUITE_DIR, case_dir); + let component_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "component"); + let public_api_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "public_api"); + + let result = if !component_fbs_paths.is_empty() && !public_api_fbs_paths.is_empty() { + run_case_from_cli(case_dir, &component_fbs_paths, &public_api_fbs_paths) + } else { + panic!( + "missing generated FBS fixtures for {case_dir}: expected component/*.fbs.bin and public_api/*.fbs.bin", + ); + }; + + assert_cli_result(case_dir, &expected, &result); +} + +#[test] +fn positive_public_api_match_suite_case() { + assert_case("positive_public_api_match"); +} + +#[test] +fn negative_public_api_missing_suite_case() { + assert_case("negative_public_api_missing"); +} + +#[test] +fn negative_public_api_wrong_type_suite_case() { + assert_case("negative_public_api_wrong_type"); +} + +#[test] +fn negative_case_sensitive_suite_case() { + assert_case("negative_case_sensitive"); +} + +#[test] +fn negative_public_api_lack_of_relationship_suite_case() { + assert_case("negative_public_api_lack_of_relationship"); +} diff --git a/validation/core/integration_test/component_public_api/negative_case_sensitive/BUILD b/validation/core/integration_test/component_public_api/negative_case_sensitive/BUILD new file mode 100644 index 00000000..071e5aec --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_case_sensitive/BUILD @@ -0,0 +1,38 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + maturity = "development", + public_api = ["public_api.puml"], + static = ["static_design.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/component_public_api/negative_case_sensitive/expected.yaml b/validation/core/integration_test/component_public_api/negative_case_sensitive/expected.yaml new file mode 100644 index 00000000..92248f8a --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_case_sensitive/expected.yaml @@ -0,0 +1,16 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: false +error_contains: | + Missing public APIs : "SampleLibraryAPI" + Action : Declare each public API interface in the public API class diagram or remove it from the component diagram diff --git a/validation/core/integration_test/component_public_api/negative_case_sensitive/public_api.puml b/validation/core/integration_test/component_public_api/negative_case_sensitive/public_api.puml new file mode 100644 index 00000000..ce7c7b0f --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_case_sensitive/public_api.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 public_api + +interface "Sample Library API" as SAMPLELIBRARYAPI <> { + +GetNumber(): int +} + +@enduml diff --git a/validation/core/integration_test/component_public_api/negative_case_sensitive/static_design.puml b/validation/core/integration_test/component_public_api/negative_case_sensitive/static_design.puml new file mode 100644 index 00000000..4adb079c --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_case_sensitive/static_design.puml @@ -0,0 +1,27 @@ +' ******************************************************************************* +' 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 static_design + +package "Sample Seooc" as sample_seooc <> { + component "Component Example" as component_example <> { + component "Unit 1" as unit_1 <> + } + + portout sample_seooc_public_api_port +} + +interface "SampleLibraryAPI" as SampleLibraryAPI +sample_seooc_public_api_port )-d- SampleLibraryAPI + +@enduml diff --git a/validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/BUILD b/validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/BUILD new file mode 100644 index 00000000..071e5aec --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/BUILD @@ -0,0 +1,38 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + maturity = "development", + public_api = ["public_api.puml"], + static = ["static_design.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/expected.yaml b/validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/expected.yaml new file mode 100644 index 00000000..a22f809e --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/expected.yaml @@ -0,0 +1,17 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: false +error_contains: | + Public API interface has no component relationship: + Public APIs : "SampleLibraryAPI" + Action : Connect each public API interface to the SEooC, or remove it from the static design diagram diff --git a/validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/public_api.puml b/validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/public_api.puml new file mode 100644 index 00000000..7fba8dbe --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/public_api.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 public_api + +interface "Sample Library API" as SampleLibraryAPI <> { + +GetNumber(): int +} + +@enduml diff --git a/validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/static_design.puml b/validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/static_design.puml new file mode 100644 index 00000000..74e81023 --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_public_api_lack_of_relationship/static_design.puml @@ -0,0 +1,24 @@ +' ******************************************************************************* +' 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 static_design + +package "Sample Seooc" as sample_seooc <> { + component "Component Example" as component_example <> { + component "Unit 1" as unit_1 <> + } +} + +interface "SampleLibraryAPI" as SampleLibraryAPI + +@enduml diff --git a/validation/core/integration_test/component_public_api/negative_public_api_missing/BUILD b/validation/core/integration_test/component_public_api/negative_public_api_missing/BUILD new file mode 100644 index 00000000..071e5aec --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_public_api_missing/BUILD @@ -0,0 +1,38 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + maturity = "development", + public_api = ["public_api.puml"], + static = ["static_design.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/component_public_api/negative_public_api_missing/expected.yaml b/validation/core/integration_test/component_public_api/negative_public_api_missing/expected.yaml new file mode 100644 index 00000000..03b8d59d --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_public_api_missing/expected.yaml @@ -0,0 +1,16 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: false +error_contains: | + Missing public APIs : "SampleLibraryAPI1", "SampleLibraryAPI2" + Action : Declare each public API interface in the public API class diagram or remove it from the component diagram diff --git a/validation/core/integration_test/component_public_api/negative_public_api_missing/public_api.puml b/validation/core/integration_test/component_public_api/negative_public_api_missing/public_api.puml new file mode 100644 index 00000000..07e87e49 --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_public_api_missing/public_api.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 public_api + +interface "Other API" as OtherAPI <> { + +GetNumber(): int +} + +@enduml diff --git a/validation/core/integration_test/component_public_api/negative_public_api_missing/static_design.puml b/validation/core/integration_test/component_public_api/negative_public_api_missing/static_design.puml new file mode 100644 index 00000000..9b7127b1 --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_public_api_missing/static_design.puml @@ -0,0 +1,30 @@ +' ******************************************************************************* +' 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 static_design + +package "Sample Seooc" as sample_seooc <> { + component "Component Example" as component_example <> { + component "Unit 1" as unit_1 <> + } + + portout sample_seooc_public_api_port +} + +interface "SampleLibraryAPI1" as SampleLibraryAPI1 +interface "SampleLibraryAPI2" as SampleLibraryAPI2 + +sample_seooc_public_api_port )-d- SampleLibraryAPI1 +sample_seooc_public_api_port )-d- SampleLibraryAPI2 + +@enduml diff --git a/validation/core/integration_test/component_public_api/negative_public_api_wrong_type/BUILD b/validation/core/integration_test/component_public_api/negative_public_api_wrong_type/BUILD new file mode 100644 index 00000000..071e5aec --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_public_api_wrong_type/BUILD @@ -0,0 +1,38 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + maturity = "development", + public_api = ["public_api.puml"], + static = ["static_design.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/component_public_api/negative_public_api_wrong_type/expected.yaml b/validation/core/integration_test/component_public_api/negative_public_api_wrong_type/expected.yaml new file mode 100644 index 00000000..92248f8a --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_public_api_wrong_type/expected.yaml @@ -0,0 +1,16 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: false +error_contains: | + Missing public APIs : "SampleLibraryAPI" + Action : Declare each public API interface in the public API class diagram or remove it from the component diagram diff --git a/validation/core/integration_test/component_public_api/negative_public_api_wrong_type/public_api.puml b/validation/core/integration_test/component_public_api/negative_public_api_wrong_type/public_api.puml new file mode 100644 index 00000000..985c32ea --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_public_api_wrong_type/public_api.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 public_api + +class "Sample Library API" as SampleLibraryAPI { + +GetNumber(): int +} + +@enduml diff --git a/validation/core/integration_test/component_public_api/negative_public_api_wrong_type/static_design.puml b/validation/core/integration_test/component_public_api/negative_public_api_wrong_type/static_design.puml new file mode 100644 index 00000000..ae7936ad --- /dev/null +++ b/validation/core/integration_test/component_public_api/negative_public_api_wrong_type/static_design.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' 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 static_design + +package "Sample Seooc" as sample_seooc <> { + component "Component Example" as component_example <> { + component "Unit 1" as unit_1 <> + } +} + +interface "SampleLibraryAPI" as SampleLibraryAPI +component_example )-d- SampleLibraryAPI + +@enduml diff --git a/validation/core/integration_test/component_public_api/positive_public_api_match/BUILD b/validation/core/integration_test/component_public_api/positive_public_api_match/BUILD new file mode 100644 index 00000000..c473da70 --- /dev/null +++ b/validation/core/integration_test/component_public_api/positive_public_api_match/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + public_api = ["public_api.puml"], + static = ["static_design.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/component_public_api/positive_public_api_match/expected.yaml b/validation/core/integration_test/component_public_api/positive_public_api_match/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/component_public_api/positive_public_api_match/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/component_public_api/positive_public_api_match/public_api.puml b/validation/core/integration_test/component_public_api/positive_public_api_match/public_api.puml new file mode 100644 index 00000000..d4d2db64 --- /dev/null +++ b/validation/core/integration_test/component_public_api/positive_public_api_match/public_api.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 public_api + +interface "Sample Library API" as SampleLibraryAPI <> { + +GetNumber(): int +} + +@enduml diff --git a/validation/core/integration_test/component_public_api/positive_public_api_match/static_design.puml b/validation/core/integration_test/component_public_api/positive_public_api_match/static_design.puml new file mode 100644 index 00000000..e3623ecb --- /dev/null +++ b/validation/core/integration_test/component_public_api/positive_public_api_match/static_design.puml @@ -0,0 +1,27 @@ +' ******************************************************************************* +' 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 static_design + +package "Sample Seooc" as sample_seooc <> { + component "Component Example" as component_example <> { + component "Unit 1" as unit_1 <> + } + + portout sample_seooc_public_api_port +} + +interface "Sample Library API" as SampleLibraryAPI +sample_seooc_public_api_port )-d- SampleLibraryAPI + +@enduml diff --git a/validation/core/integration_test/puml_fixture.bzl b/validation/core/integration_test/puml_fixture.bzl index cbb137fb..22f36cd6 100644 --- a/validation/core/integration_test/puml_fixture.bzl +++ b/validation/core/integration_test/puml_fixture.bzl @@ -24,6 +24,7 @@ def _collect_fbs_files(deps): files_by_category = { "component": [], "internal_api": [], + "public_api": [], "sequence": [], "unit_design_class": [], "unit_design_sequence": [], @@ -35,10 +36,12 @@ def _collect_fbs_files(deps): if ArchitecturalDesignInfo in dep: component_files = dep[ArchitecturalDesignInfo].static.to_list() internal_api_files = dep[ArchitecturalDesignInfo].internal_api.to_list() + public_api_files = dep[ArchitecturalDesignInfo].public_api.to_list() sequence_files = dep[ArchitecturalDesignInfo].dynamic.to_list() files_by_category["component"].extend(component_files) files_by_category["internal_api"].extend(internal_api_files) + files_by_category["public_api"].extend(public_api_files) files_by_category["sequence"].extend(sequence_files) if UnitDesignInfo in dep: @@ -90,6 +93,7 @@ def _provider_fbs_fixture_bundle_impl(ctx): _materialize_category("component") _materialize_category("internal_api") + _materialize_category("public_api") _materialize_category("sequence") _materialize_category("unit_design_class") _materialize_category("unit_design_sequence") diff --git a/validation/core/src/models/class_diagram_models.rs b/validation/core/src/models/class_diagram_models.rs index 7609f599..ef3a6c54 100644 --- a/validation/core/src/models/class_diagram_models.rs +++ b/validation/core/src/models/class_diagram_models.rs @@ -107,6 +107,34 @@ impl InternalApiIndex { } } +/// Indexed public-API data prepared for component/public-API validators. +pub struct PublicApiIndex { + api_index: BTreeMap, // +} + +impl PublicApiIndex { + /// Build a [`PublicApiIndex`] from public-API class diagram inputs. + pub fn build_index(diagrams: &[ClassDiagramInput]) -> Self { + let mut api_index: BTreeMap = BTreeMap::new(); + + for diagram in diagrams { + for entity in &diagram.entities { + if entity.entity_type != EntityType::Interface { + continue; + } + + api_index.insert(entity.name.clone(), entity.id.clone()); + } + } + + Self { api_index } + } + + pub fn api_names(&self) -> impl Iterator + '_ { + self.api_index.keys() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/validation/core/src/models/mod.rs b/validation/core/src/models/mod.rs index 59fa6c65..c518de95 100644 --- a/validation/core/src/models/mod.rs +++ b/validation/core/src/models/mod.rs @@ -25,7 +25,7 @@ use shared::EntityKey; pub use bazel_models::BazelInputEntry; pub use bazel_models::{BazelArchitecture, BazelInput}; pub use class_diagram_models::{ - ClassDiagramInputs, ClassEntityIndex, InternalApiIndex, InternalApiInterface, + ClassDiagramInputs, ClassEntityIndex, InternalApiIndex, InternalApiInterface, PublicApiIndex, }; pub use component_diagram_models::{ ComponentDiagramArchitecture, ComponentDiagramInputs, ComponentRelationType, ComponentType, diff --git a/validation/core/src/profiles/architectural_design.rs b/validation/core/src/profiles/architectural_design.rs index 7affe74c..d6f032e9 100644 --- a/validation/core/src/profiles/architectural_design.rs +++ b/validation/core/src/profiles/architectural_design.rs @@ -13,11 +13,12 @@ use crate::models::{ ClassDiagramInputs, ComponentDiagramArchitecture, ComponentDiagramInputs, InternalApiIndex, - SequenceDiagramIndex, SequenceDiagramInputs, + PublicApiIndex, SequenceDiagramIndex, SequenceDiagramInputs, }; use crate::readers::{ClassDiagramReader, ComponentDiagramReader, SequenceDiagramReader}; use crate::validators::{ - validate_component_internal_api, validate_component_sequence, validate_sequence_internal_api, + validate_component_internal_api, validate_component_public_api, validate_component_sequence, + validate_sequence_internal_api, }; use crate::ValidationResult; use serde::Deserialize; @@ -39,6 +40,7 @@ fn registered_validators<'a>( component: &'a Option, sequence: &'a Option, internal_api: &'a Option, + public_api: &'a Option, ) -> Vec> { vec![ Box::new(move || { @@ -49,6 +51,10 @@ fn registered_validators<'a>( let (component, internal_api) = (component.as_ref()?, internal_api.as_ref()?); Some(validate_component_internal_api(component, internal_api)) }), + Box::new(move || { + let (component, public_api) = (component.as_ref()?, public_api.as_ref()?); + Some(validate_component_public_api(component, public_api)) + }), Box::new(move || { let (sequence, internal_api) = (sequence.as_ref()?, internal_api.as_ref()?); Some(validate_sequence_internal_api( @@ -77,8 +83,13 @@ pub fn run(inputs: &ArchitecturalDesignInputs) -> Result { &mut result, |raw: ClassDiagramInputs, _result| InternalApiIndex::build_index(&raw), )?; + let public_api = read_and_convert::( + inputs.public_api_diagrams.as_slice(), + &mut result, + |raw: ClassDiagramInputs, _result| PublicApiIndex::build_index(&raw), + )?; - let validators = registered_validators(&component, &sequence, &internal_api); + let validators = registered_validators(&component, &sequence, &internal_api, &public_api); let mut ran_validator = false; for validator in validators { diff --git a/validation/core/src/validators/component_public_api_validator.rs b/validation/core/src/validators/component_public_api_validator.rs new file mode 100644 index 00000000..f793cd1e --- /dev/null +++ b/validation/core/src/validators/component_public_api_validator.rs @@ -0,0 +1,165 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Validation: compare public API references in the component diagram with +//! interfaces declared by the public API class diagram. + +use std::collections::BTreeSet; + +use super::shared::format_name_list; +use crate::models::{ComponentDiagramArchitecture, LogicComponentExt, PublicApiIndex}; +use crate::{Diagnostics, ValidationResult}; + +/// Run component-vs-public-API reference validation. +pub fn validate_component_public_api( + component_diagram: &ComponentDiagramArchitecture, + public_api_index: &PublicApiIndex, +) -> ValidationResult { + ComponentPublicApiValidator::new(component_diagram, public_api_index).run() +} + +struct ComponentPublicApiValidator { + /// Public API interfaces explicitly declared in the component diagram. + component_public_api_ids: BTreeSet, + /// Public API interfaces referenced by relationships from SEooC entities. + seooc_related_public_api_ids: BTreeSet, + /// Public API interfaces declared in the public API class diagram. + design_public_api_ids: BTreeSet, + result: ValidationResult, +} + +impl ComponentPublicApiValidator { + fn new( + component_diagram: &ComponentDiagramArchitecture, + public_api_index: &PublicApiIndex, + ) -> Self { + Self { + seooc_related_public_api_ids: collect_seooc_related_public_api_ids(component_diagram), + component_public_api_ids: collect_component_public_api_ids(component_diagram), + design_public_api_ids: public_api_index.api_names().cloned().collect(), + result: ValidationResult::default(), + } + } + + fn run(mut self) -> ValidationResult { + append_debug_log( + &mut self.result.diagnostics, + &self.component_public_api_ids, + &self.seooc_related_public_api_ids, + &self.design_public_api_ids, + ); + self.check_component_public_apis_declared_by_public_api(); + self.check_component_public_apis_have_relationship(); + self.result + } + + fn check_component_public_apis_declared_by_public_api(&mut self) { + let missing_public_apis: BTreeSet = self + .component_public_api_ids + .difference(&self.design_public_api_ids) + .cloned() + .collect(); + + if !missing_public_apis.is_empty() { + self.result + .add_failure(format_missing_public_api_error(&missing_public_apis)); + } + } + + fn check_component_public_apis_have_relationship(&mut self) { + let unrelated_public_apis: BTreeSet = self + .component_public_api_ids + .difference(&self.seooc_related_public_api_ids) + .cloned() + .collect(); + + if !unrelated_public_apis.is_empty() { + self.result + .add_failure(format_unrelated_public_api_error(&unrelated_public_apis)); + } + } +} + +fn append_debug_log( + diagnostics: &mut Diagnostics, + component_public_api_ids: &BTreeSet, + seooc_related_public_api_ids: &BTreeSet, + design_public_api_ids: &BTreeSet, +) { + diagnostics.debug(|| "Component public APIs checked against public API diagram:".to_string()); + for api_id in component_public_api_ids { + diagnostics.debug(|| format!(" {api_id}")); + } + + diagnostics.debug(|| "Component public APIs referenced by component relations:".to_string()); + for api_id in seooc_related_public_api_ids { + diagnostics.debug(|| format!(" {api_id}")); + } + + diagnostics.debug(|| "Public API entries available for component public APIs:".to_string()); + for api_id in design_public_api_ids { + diagnostics.debug(|| format!(" {api_id}")); + } +} + +fn collect_component_public_api_ids( + component_diagram: &ComponentDiagramArchitecture, +) -> BTreeSet { + component_diagram + .entities + .iter() + .filter(|entity| entity.is_interface() && entity.parent_id.is_none()) + .map(|entity| entity.id.clone()) + .collect() +} + +fn collect_seooc_related_public_api_ids( + component_diagram: &ComponentDiagramArchitecture, +) -> BTreeSet { + let interface_ids: BTreeSet = component_diagram + .entities + .iter() + .filter(|entity| entity.is_interface()) + .map(|entity| entity.id.clone()) + .collect(); + + component_diagram + .seooc_set + .values() + .flat_map(|entity| entity.relations.iter()) + .filter(|relation| interface_ids.contains(&relation.target)) + .map(|relation| relation.target.clone()) + .collect() +} + +fn format_missing_public_api_error(missing_public_apis: &BTreeSet) -> String { + format!( + "Public API consistency failure: Missing public API declaration:\n\ + Missing public APIs : {missing_public_apis}\n\ + Action : Declare each public API interface in the public API class diagram or remove it from the component diagram", + missing_public_apis = format_name_list(missing_public_apis), + ) +} + +fn format_unrelated_public_api_error(unrelated_public_apis: &BTreeSet) -> String { + format!( + "Public API consistency failure: Public API interface has no component relationship:\n\ + Public APIs : {public_apis}\n\ + Action : Connect each public API interface to the SEooC, or remove it from the static design diagram", + public_apis = format_name_list(unrelated_public_apis), + ) +} + +#[cfg(test)] +#[path = "test/component_public_api_validator_test.rs"] +mod tests; diff --git a/validation/core/src/validators/mod.rs b/validation/core/src/validators/mod.rs index 4ddefdba..190482fc 100644 --- a/validation/core/src/validators/mod.rs +++ b/validation/core/src/validators/mod.rs @@ -16,6 +16,7 @@ mod bazel_component_validator; mod class_design_implementation_validator; mod component_internal_api_validator; +mod component_public_api_validator; mod component_sequence_validator; mod sequence_internal_api_validator; mod shared; @@ -27,5 +28,6 @@ pub(crate) mod fixtures; pub use bazel_component_validator::validate_bazel_component; pub use class_design_implementation_validator::validate_class_design_implementation; pub use component_internal_api_validator::validate_component_internal_api; +pub use component_public_api_validator::validate_component_public_api; pub use component_sequence_validator::validate_component_sequence; pub use sequence_internal_api_validator::validate_sequence_internal_api; diff --git a/validation/core/src/validators/test/component_public_api_validator_test.rs b/validation/core/src/validators/test/component_public_api_validator_test.rs new file mode 100644 index 00000000..77e4737e --- /dev/null +++ b/validation/core/src/validators/test/component_public_api_validator_test.rs @@ -0,0 +1,145 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* +use super::super::fixtures::*; +use super::*; +use crate::models::{ + ComponentDiagramInputs, ComponentRelationType, EndpointRole, LogicComponent, PublicApiIndex, +}; +use crate::ValidationResult; +use class_diagram::ClassDiagram; + +fn seooc_with_public_api_relations(alias: &str, relation_targets: &[&str]) -> LogicComponent { + let mut seooc = unit_without_interfaces(alias); + seooc.stereotype = Some("SEooC".to_string()); + seooc.relations = relation_targets + .iter() + .map(|target| { + relation( + target, + ComponentRelationType::InterfaceBinding, + EndpointRole::Required, + ) + }) + .collect(); + seooc +} + +fn public_api_index(interfaces: Vec<(&str, Option<&str>)>) -> PublicApiIndex { + let diagrams = vec![ClassDiagram { + name: "public_api".to_string(), + entities: interfaces + .into_iter() + .map(|(interface_name, namespace)| class_interface(interface_name, namespace)) + .collect(), + }]; + + PublicApiIndex::build_index(&diagrams) +} + +fn validate( + component_diagrams: ComponentDiagramInputs, + public_api: &PublicApiIndex, +) -> ValidationResult { + let mut setup_result = ValidationResult::default(); + let component_arch = component_diagrams.to_diagram_architecture(&mut setup_result); + assert!( + setup_result.is_empty(), + "test fixture setup failed: {:?}", + setup_result.failures + ); + + validate_component_public_api(&component_arch, public_api) +} + +#[test] +fn passes_when_top_level_public_api_is_declared_and_related_from_seooc() { + let component_diagrams = component_diagram(vec![ + seooc_with_public_api_relations("sample_seooc", &["SampleLibraryAPI"]), + interface("SampleLibraryAPI"), + ]); + let public_api = public_api_index(vec![("SampleLibraryAPI", Some("sample_seooc"))]); + + let validation_result = validate(component_diagrams, &public_api); + + assert!(validation_result.failures.is_empty()); +} + +#[test] +fn reports_missing_component_public_api_declaration() { + let component_diagrams = component_diagram(vec![ + seooc_with_public_api_relations("sample_seooc", &["SampleLibraryAPI"]), + interface("SampleLibraryAPI"), + ]); + let public_api = public_api_index(vec![("OtherAPI", Some("sample_seooc"))]); + + let validation_result = validate(component_diagrams, &public_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0].contains("Missing public API declaration")); + assert!(validation_result.failures[0].contains("Missing public APIs : \"SampleLibraryAPI\"")); +} + +#[test] +fn reports_public_api_without_seooc_relationship() { + let component_diagrams = component_diagram(vec![ + seooc_with_public_api_relations("sample_seooc", &[]), + interface("SampleLibraryAPI"), + ]); + let public_api = public_api_index(vec![("SampleLibraryAPI", Some("sample_seooc"))]); + + let validation_result = validate(component_diagrams, &public_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] + .contains("Public API interface has no component relationship")); + assert!(validation_result.failures[0].contains("\"SampleLibraryAPI\"")); +} + +#[test] +fn ignores_component_relationships_when_checking_public_api() { + let mut component = unit_without_interfaces("component_example"); + component.stereotype = Some("component".to_string()); + component.relations = vec![relation( + "SampleLibraryAPI", + ComponentRelationType::InterfaceBinding, + EndpointRole::Required, + )]; + + let component_diagrams = component_diagram(vec![ + seooc_with_public_api_relations("sample_seooc", &[]), + component, + interface("SampleLibraryAPI"), + ]); + let public_api = public_api_index(vec![("SampleLibraryAPI", Some("component_example"))]); + + let validation_result = validate(component_diagrams, &public_api); + + assert_eq!(validation_result.failures.len(), 1); + assert!(validation_result.failures[0] + .contains("Public API interface has no component relationship")); + assert!(validation_result.failures[0].contains("\"SampleLibraryAPI\"")); +} + +#[test] +fn ignores_internal_interface_bindings_when_checking_public_api() { + let component_diagrams = component_diagram(vec![ + unit("u1", &["component_example.InternalInterface"], &[]), + unit_without_interfaces("component_example"), + interface_with_parent_id("InternalInterface", "component_example"), + ]); + let public_api = public_api_index(vec![]); + + let validation_result = validate(component_diagrams, &public_api); + + assert!(validation_result.failures.is_empty()); +}