diff --git a/src/class_diagram/generators/plantuml/class_diagram_generator.cc b/src/class_diagram/generators/plantuml/class_diagram_generator.cc index 971cc021..f30800a4 100644 --- a/src/class_diagram/generators/plantuml/class_diagram_generator.cc +++ b/src/class_diagram/generators/plantuml/class_diagram_generator.cc @@ -479,14 +479,30 @@ void generator::generate(const package &p, std::ostream &ostr) const } else if (dynamic_cast(subpackage.get()) != nullptr) { if (m_model.should_include(*subpackage)) { - generate_alias(dynamic_cast(*subpackage), ostr); - generate(dynamic_cast(*subpackage), ostr); + auto together_group = + m_config.get_together_group(subpackage->full_name(false)); + if (together_group) { + current_level_together_groups_[together_group.value()] + .push_back(subpackage.get()); + } + else { + generate_alias(dynamic_cast(*subpackage), ostr); + generate(dynamic_cast(*subpackage), ostr); + } } } else if (dynamic_cast(subpackage.get()) != nullptr) { if (m_model.should_include(*subpackage)) { - generate_alias(dynamic_cast(*subpackage), ostr); - generate(dynamic_cast(*subpackage), ostr); + auto together_group = + m_config.get_together_group(subpackage->full_name(false)); + if (together_group) { + current_level_together_groups_[together_group.value()] + .push_back(subpackage.get()); + } + else { + generate_alias(dynamic_cast(*subpackage), ostr); + generate(dynamic_cast(*subpackage), ostr); + } } } } @@ -536,37 +552,71 @@ void generator::generate(std::ostream &ostr) const generate_plantuml_directives(ostr, m_config.puml().before); for (const auto &p : m_model) { - if (dynamic_cast(p.get()) != nullptr) { - const auto &sp = dynamic_cast(*p); - if (!sp.is_empty()) - generate(sp, ostr); + if (auto *pkg = dynamic_cast(p.get()); pkg) { + if (!pkg->is_empty()) + generate(*pkg, ostr); } - else if (dynamic_cast(p.get()) != nullptr) { - if (m_model.should_include(*p)) { - generate_alias(dynamic_cast(*p), ostr); - generate(dynamic_cast(*p), ostr); + else if (auto *cls = dynamic_cast(p.get()); cls) { + if (m_model.should_include(*cls)) { + auto together_group = + m_config.get_together_group(cls->full_name(false)); + if (together_group) { + current_level_together_groups_[together_group.value()] + .push_back(cls); + } + else { + generate_alias(*cls, ostr); + generate(*cls, ostr); + } } } - else if (dynamic_cast(p.get()) != nullptr) { - if (m_model.should_include(*p)) { - generate_alias(dynamic_cast(*p), ostr); - generate(dynamic_cast(*p), ostr); + else if (auto *enm = dynamic_cast(p.get()); enm) { + if (m_model.should_include(*enm)) { + auto together_group = + m_config.get_together_group(enm->full_name(false)); + if (together_group) { + current_level_together_groups_[together_group.value()] + .push_back(enm); + } + else { + generate_alias(*enm, ostr); + generate(*enm, ostr); + } } } } - for (const auto &p : m_model) { - if (dynamic_cast(p.get()) != nullptr) { - generate_relationships(dynamic_cast(*p), ostr); - } - else if (dynamic_cast(p.get()) != nullptr) { - if (m_model.should_include(*p)) { - generate_relationships(dynamic_cast(*p), ostr); + // Now generate any diagram elements which are in together groups + for (const auto &[group_name, group_elements] : + current_level_together_groups_) { + + ostr << "' together group for " << group_name << "\n"; + ostr << "together {\n"; + for (auto *e : group_elements) { + if (auto *cls = dynamic_cast(e); cls) { + generate_alias(*cls, ostr); + generate(*cls, ostr); + } + if (auto *enm = dynamic_cast(e); enm) { + generate_alias(*enm, ostr); + generate(*enm, ostr); } } - else if (dynamic_cast(p.get()) != nullptr) { - if (m_model.should_include(*p)) { - generate_relationships(dynamic_cast(*p), ostr); + ostr << "}\n"; + } + + for (const auto &p : m_model) { + if (auto *pkg = dynamic_cast(p.get()); pkg) { + generate_relationships(*pkg, ostr); + } + else if (auto *cls = dynamic_cast(p.get()); cls) { + if (m_model.should_include(*cls)) { + generate_relationships(*cls, ostr); + } + } + else if (auto *enm = dynamic_cast(p.get()); enm) { + if (m_model.should_include(*enm)) { + generate_relationships(*enm, ostr); } } } diff --git a/src/class_diagram/generators/plantuml/class_diagram_generator.h b/src/class_diagram/generators/plantuml/class_diagram_generator.h index 935269e7..f4539f26 100644 --- a/src/class_diagram/generators/plantuml/class_diagram_generator.h +++ b/src/class_diagram/generators/plantuml/class_diagram_generator.h @@ -83,6 +83,9 @@ public: private: std::string render_name(std::string name) const; + + mutable std::map> + current_level_together_groups_; }; } // namespace plantuml diff --git a/src/common/generators/plantuml/generator.h b/src/common/generators/plantuml/generator.h index 9b62a281..20b80d29 100644 --- a/src/common/generators/plantuml/generator.h +++ b/src/common/generators/plantuml/generator.h @@ -224,15 +224,22 @@ void generator::generate_config_layout_hints(std::ostream &ostr) const for (const auto &[entity_name, hints] : m_config.layout()) { for (const auto &hint : hints) { std::stringstream hint_str; + + // 'together' layout hint is handled separately + if (hint.hint == config::hint_t::together) + continue; + + const auto &hint_entity = std::get(hint.entity); + try { auto element_opt = m_model.get(entity_name); if (!element_opt) element_opt = m_model.get((uns | entity_name).to_string()); - auto hint_element_opt = m_model.get(hint.entity); + auto hint_element_opt = m_model.get(hint_entity); if (!hint_element_opt) hint_element_opt = - m_model.get((uns | hint.entity).to_string()); + m_model.get((uns | hint_entity).to_string()); if (!element_opt || !hint_element_opt) continue; @@ -244,7 +251,7 @@ void generator::generate_config_layout_hints(std::ostream &ostr) const catch (clanguml::error::uml_alias_missing &e) { LOG_DBG("=== Skipping layout hint from {} to {} due " "to: {}", - entity_name, hint.entity, e.what()); + entity_name, hint_entity, e.what()); } } } diff --git a/src/config/config.cc b/src/config/config.cc index add25931..e408fbc3 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -142,6 +142,31 @@ std::vector diagram::get_translation_units() const return translation_units; } +std::optional class_diagram::get_together_group( + const std::string &full_name) const +{ + const auto relative_name = using_namespace().relative(full_name); + + for (const auto &[hint_target, hints] : layout()) { + for (const auto &hint : hints) { + if (hint.hint == hint_t::together) { + const auto &together_others = + std::get>(hint.entity); + + if ((full_name == hint_target) || + util::contains(together_others, full_name)) + return hint_target; + + if ((relative_name == hint_target) || + util::contains(together_others, relative_name)) + return hint_target; + } + } + } + + return std::nullopt; +} + void diagram::initialize_type_aliases() { if (type_aliases().count("std::basic_string") == 0U) { diff --git a/src/config/config.h b/src/config/config.h index 79868b2e..977116de 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -80,13 +80,13 @@ struct filter { std::vector paths; }; -enum class hint_t { up, down, left, right }; +enum class hint_t { up, down, left, right, together }; std::string to_string(hint_t t); struct layout_hint { hint_t hint{hint_t::up}; - std::string entity; + std::variant> entity; }; using layout_hints = std::map>; @@ -184,6 +184,9 @@ struct class_diagram : public diagram { option layout{"layout"}; + std::optional get_together_group( + const std::string &full_name) const; + void initialize_relationship_hints(); }; diff --git a/src/config/yaml_decoders.cc b/src/config/yaml_decoders.cc index 2abe0b3c..5c17af46 100644 --- a/src/config/yaml_decoders.cc +++ b/src/config/yaml_decoders.cc @@ -468,6 +468,10 @@ template <> struct convert { rhs.hint = hint_t::right; rhs.entity = node["right"].as(); } + else if (node["together"]) { + rhs.hint = hint_t::together; + rhs.entity = node["together"].as>(); + } else return false; @@ -645,7 +649,8 @@ config load( doc["git"] = git_config; } - return doc.as(); + auto d = doc.as(); + return d; } catch (YAML::BadFile &e) { throw std::runtime_error(fmt::format( diff --git a/src/config/yaml_emitters.cc b/src/config/yaml_emitters.cc index e1147bc7..b31eedf1 100644 --- a/src/config/yaml_emitters.cc +++ b/src/config/yaml_emitters.cc @@ -159,7 +159,13 @@ YAML::Emitter &operator<<( YAML::Emitter &operator<<(YAML::Emitter &out, const layout_hint &c) { out << YAML::BeginMap; - out << YAML::Key << c.hint << YAML::Value << c.entity; + + out << YAML::Key << c.hint << YAML::Value; + if (std::holds_alternative(c.entity)) + out << std::get(c.entity); + else if (std::holds_alternative>(c.entity)) + out << std::get>(c.entity); + out << YAML::EndMap; return out; } diff --git a/tests/t00053/.clang-uml b/tests/t00053/.clang-uml new file mode 100644 index 00000000..45e67c0b --- /dev/null +++ b/tests/t00053/.clang-uml @@ -0,0 +1,19 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t00053_class: + type: class + glob: + - ../../tests/t00053/t00053.cc + include: + namespaces: + - clanguml::t00053 + using_namespace: + - clanguml::t00053 + layout: + a: + - together: [c,e,f] + A: + - together: [C,E,F] + h: + - together: [j] diff --git a/tests/t00053/t00053.cc b/tests/t00053/t00053.cc new file mode 100644 index 00000000..c93be191 --- /dev/null +++ b/tests/t00053/t00053.cc @@ -0,0 +1,38 @@ +namespace clanguml { +namespace t00053 { +struct a { +}; +struct b { +}; +struct c { +}; +struct d { +}; +struct e { +}; +struct f { +}; +struct g { +}; + +struct A { +}; +struct B { +}; +struct C { +}; +struct D { +}; +struct E { +}; +struct F { +}; +struct G { +}; + +enum class h { hhh }; +enum class i { iii }; +enum class j { jjj }; + +} +} \ No newline at end of file diff --git a/tests/t00053/test_case.h b/tests/t00053/test_case.h new file mode 100644 index 00000000..f51ae4a4 --- /dev/null +++ b/tests/t00053/test_case.h @@ -0,0 +1,55 @@ +/** + * tests/t00053/test_case.h + * + * Copyright (c) 2021-2023 Bartek Kryza + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +TEST_CASE("t00053", "[test-case][class]") +{ + auto [config, db] = load_config("t00053"); + + auto diagram = config.diagrams["t00053_class"]; + + REQUIRE(diagram->name == "t00053_class"); + + auto model = generate_class_diagram(*db, diagram); + + REQUIRE(model->name() == "t00053_class"); + + auto puml = generate_class_puml(diagram, *model); + AliasMatcher _A(puml); + + REQUIRE_THAT(puml, StartsWith("@startuml")); + REQUIRE_THAT(puml, EndsWith("@enduml\n")); + + // Check if all classes exist + REQUIRE_THAT(puml, IsClass(_A("a"))); + REQUIRE_THAT(puml, IsClass(_A("b"))); + REQUIRE_THAT(puml, IsClass(_A("c"))); + REQUIRE_THAT(puml, IsClass(_A("d"))); + REQUIRE_THAT(puml, IsClass(_A("e"))); + REQUIRE_THAT(puml, IsClass(_A("f"))); + REQUIRE_THAT(puml, IsClass(_A("g"))); + + REQUIRE_THAT(puml, IsClass(_A("A"))); + REQUIRE_THAT(puml, IsClass(_A("B"))); + REQUIRE_THAT(puml, IsClass(_A("C"))); + REQUIRE_THAT(puml, IsClass(_A("D"))); + REQUIRE_THAT(puml, IsClass(_A("E"))); + REQUIRE_THAT(puml, IsClass(_A("F"))); + REQUIRE_THAT(puml, IsClass(_A("G"))); + + save_puml(config.output_directory() + "/" + diagram->name + ".puml", puml); +} \ No newline at end of file diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 8d94c572..ae215fa2 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -246,6 +246,7 @@ using namespace clanguml::test::matchers; #include "t00050/test_case.h" #include "t00051/test_case.h" #include "t00052/test_case.h" +#include "t00053/test_case.h" /// /// Sequence diagram tests diff --git a/tests/test_cases.yaml b/tests/test_cases.yaml index 117ff78a..e97756bd 100644 --- a/tests/test_cases.yaml +++ b/tests/test_cases.yaml @@ -153,6 +153,9 @@ test_cases: - name: t00052 title: Test case for template methods rendering description: + - name: t00053 + title: Test case for `together` layout hint + description: Sequence diagrams: - name: t20001 title: Basic sequence diagram test case diff --git a/tests/test_config.cc b/tests/test_config.cc index 6c07a5eb..a4f2d946 100644 --- a/tests/test_config.cc +++ b/tests/test_config.cc @@ -150,6 +150,8 @@ TEST_CASE("Test config includes", "[unit-test]") TEST_CASE("Test config layout", "[unit-test]") { + using namespace std::string_literals; + auto cfg = clanguml::config::load("./test_config_data/layout.yml"); CHECK(cfg.diagrams.size() == 2); @@ -157,35 +159,78 @@ TEST_CASE("Test config layout", "[unit-test]") [[maybe_unused]] auto &def = static_cast( *cfg.diagrams["class_main"]); - auto check_layout = [](const auto &diagram, - const clanguml::common::model::diagram_t type) { - CHECK(diagram.type() == type); + auto check_class_layout = + [](const clanguml::config::class_diagram &diagram, + const clanguml::common::model::diagram_t type) { + CHECK(diagram.type() == type); - CHECK(diagram.layout().at("ABCD").size() == 2); - CHECK(diagram.layout().at("ABCD")[0].hint == - clanguml::config::hint_t::up); - CHECK(diagram.layout().at("ABCD")[0].entity == "ABCD_SUBCLASS"); - CHECK(diagram.layout().at("ABCD")[1].hint == - clanguml::config::hint_t::left); - CHECK(diagram.layout().at("ABCD")[1].entity == "ABCD_SIBLING"); + const auto &ABCD = diagram.layout().at("ABCD"); - CHECK(diagram.layout().at("ABCD_SIBLING").size() == 2); - CHECK(diagram.layout().at("ABCD_SIBLING")[0].hint == - clanguml::config::hint_t::right); - CHECK(diagram.layout().at("ABCD_SIBLING")[0].entity == - "ABCD_OTHER_SIBLING"); - CHECK(diagram.layout().at("ABCD_SIBLING")[1].hint == - clanguml::config::hint_t::down); - CHECK(diagram.layout().at("ABCD_SIBLING")[1].entity == - "ABCD_SIBLING_SIBLING"); - }; + CHECK(ABCD.size() == 3); - check_layout(static_cast( - *cfg.diagrams["class_main"]), + CHECK(ABCD[0].hint == clanguml::config::hint_t::up); + CHECK(std::get(ABCD[0].entity) == "ABCD_SUBCLASS"); + + CHECK(ABCD[1].hint == clanguml::config::hint_t::left); + CHECK(std::get(ABCD[1].entity) == "ABCD_SIBLING"); + + CHECK(ABCD[2].hint == clanguml::config::hint_t::together); + CHECK(std::get>(ABCD[2].entity) == + std::vector{"A"s, "B"s, "C"s, "D"s}); + + CHECK(diagram.get_together_group("ABCD").value() == "ABCD"); + CHECK( + diagram.get_together_group("clanguml::ABCD").value() == "ABCD"); + CHECK(diagram.get_together_group("B").value() == "ABCD"); + CHECK(diagram.get_together_group("clanguml::B").value() == "ABCD"); + CHECK(!diagram.get_together_group("clanguml::E")); + CHECK(!diagram.get_together_group("E")); + + const auto &ABCD_SIBLING = diagram.layout().at("ABCD_SIBLING"); + CHECK(ABCD_SIBLING.size() == 2); + + CHECK(ABCD_SIBLING[0].hint == clanguml::config::hint_t::right); + CHECK(std::get(ABCD_SIBLING[0].entity) == + "ABCD_OTHER_SIBLING"); + + CHECK(ABCD_SIBLING[1].hint == clanguml::config::hint_t::down); + CHECK(std::get(ABCD_SIBLING[1].entity) == + "ABCD_SIBLING_SIBLING"); + }; + + auto check_package_layout = + [](const clanguml::config::package_diagram &diagram, + const clanguml::common::model::diagram_t type) { + CHECK(diagram.type() == type); + + const auto &ABCD = diagram.layout().at("ABCD"); + + CHECK(ABCD.size() == 2); + + CHECK(ABCD[0].hint == clanguml::config::hint_t::up); + CHECK(std::get(ABCD[0].entity) == "ABCD_SUBCLASS"); + + CHECK(ABCD[1].hint == clanguml::config::hint_t::left); + CHECK(std::get(ABCD[1].entity) == "ABCD_SIBLING"); + + const auto &ABCD_SIBLING = diagram.layout().at("ABCD_SIBLING"); + CHECK(ABCD_SIBLING.size() == 2); + + CHECK(ABCD_SIBLING[0].hint == clanguml::config::hint_t::right); + CHECK(std::get(ABCD_SIBLING[0].entity) == + "ABCD_OTHER_SIBLING"); + + CHECK(ABCD_SIBLING[1].hint == clanguml::config::hint_t::down); + CHECK(std::get(ABCD_SIBLING[1].entity) == + "ABCD_SIBLING_SIBLING"); + }; + + check_class_layout(static_cast( + *cfg.diagrams["class_main"]), clanguml::common::model::diagram_t::kClass); - check_layout(static_cast( - *cfg.diagrams["package_main"]), + check_package_layout(static_cast( + *cfg.diagrams["package_main"]), clanguml::common::model::diagram_t::kPackage); } diff --git a/tests/test_config_data/layout.yml b/tests/test_config_data/layout.yml index e70502dc..2de084e1 100644 --- a/tests/test_config_data/layout.yml +++ b/tests/test_config_data/layout.yml @@ -13,6 +13,7 @@ diagrams: ABCD: - up: ABCD_SUBCLASS - left: ABCD_SIBLING + - together: [A, B, C, D] ABCD_SIBLING: - right: ABCD_OTHER_SIBLING - down: ABCD_SIBLING_SIBLING