diff --git a/src/puml/class_diagram_generator.h b/src/puml/class_diagram_generator.h index 9ebb9790..849f6ce0 100644 --- a/src/puml/class_diagram_generator.h +++ b/src/puml/class_diagram_generator.h @@ -75,24 +75,24 @@ public: } } - std::string to_string(relationship_t r) const + std::string to_string(relationship_t r, std::string style = "") const { switch (r) { case relationship_t::kOwnership: case relationship_t::kComposition: - return "*--"; + return style.empty() ? "*--" : fmt::format("*-[{}]-", style); case relationship_t::kAggregation: - return "o--"; + return style.empty() ? "o--" : fmt::format("o-[{}]-", style); case relationship_t::kContainment: - return "--+"; + return style.empty() ? "--+" : fmt::format("-[{}]-+", style); case relationship_t::kAssociation: - return "-->"; + return style.empty() ? "-->" : fmt::format("-[{}]->", style); case relationship_t::kInstantiation: - return "..|>"; + return style.empty() ? "..|>" : fmt::format(".[{}].|>", style); case relationship_t::kFriendship: - return "<.."; + return style.empty() ? "<.." : fmt::format("<.[{}].", style); case relationship_t::kDependency: - return "..>"; + return style.empty() ? "..>" : fmt::format(".[{}].>", style); default: return ""; } @@ -149,7 +149,12 @@ public: if (c.is_abstract()) class_type = "abstract"; - ostr << class_type << " " << c.alias() << " {" << '\n'; + ostr << class_type << " " << c.alias(); + + if(!c.style.empty()) + ostr << " " << c.style; + + ostr << " {" << '\n'; // // Process methods @@ -228,7 +233,7 @@ public: if (!r.multiplicity_source.empty()) puml_relation += "\"" + r.multiplicity_source + "\" "; - puml_relation += to_string(r.type); + puml_relation += to_string(r.type, r.style); if (!r.multiplicity_destination.empty()) puml_relation += " \"" + r.multiplicity_destination + "\""; @@ -310,7 +315,12 @@ public: void generate(const enum_ &e, std::ostream &ostr) const { - ostr << "enum " << e.alias() << " {" << '\n'; + ostr << "enum " << e.alias(); + + if(!e.style.empty()) + ostr << " " << e.style; + + ostr << " {" << '\n'; for (const auto &enum_constant : e.constants) { ostr << enum_constant << '\n'; diff --git a/src/uml/class_diagram_model.h b/src/uml/class_diagram_model.h index f3f95d7a..961d85a5 100644 --- a/src/uml/class_diagram_model.h +++ b/src/uml/class_diagram_model.h @@ -54,6 +54,10 @@ enum class relationship_t { std::string to_string(relationship_t r); +struct stylable_element { + std::string style; +}; + struct decorated_element { std::vector> decorators; @@ -93,6 +97,15 @@ struct decorated_element { return {relationship_t::kNone, ""}; } + + std::string style_spec() + { + for (auto d : decorators) + if (std::dynamic_pointer_cast(d)) + return std::dynamic_pointer_cast(d)->spec; + + return ""; + } }; class element : public decorated_element { @@ -157,7 +170,7 @@ struct class_parent { access_t access; }; -struct class_relationship : public decorated_element { +struct class_relationship : public decorated_element, public stylable_element { relationship_t type{relationship_t::kAssociation}; std::string destination; std::string multiplicity_source; @@ -190,7 +203,7 @@ struct type_alias { std::string underlying_type; }; -class class_ : public element { +class class_ : public element, public stylable_element { public: std::string usr; bool is_struct{false}; @@ -275,7 +288,7 @@ public: } }; -struct enum_ : public element { +struct enum_ : public element, public stylable_element { std::vector constants; std::vector relationships; diff --git a/src/uml/class_diagram_visitor.cc b/src/uml/class_diagram_visitor.cc index 613dc50b..30c9fea8 100644 --- a/src/uml/class_diagram_visitor.cc +++ b/src/uml/class_diagram_visitor.cc @@ -199,6 +199,8 @@ void tu_visitor::process_enum_declaration(const cppast::cpp_enum &enm) if (e.skip()) return; + e.style = e.style_spec(); + // Process enum documentation comment if (enm.comment().has_value()) e.decorators = decorators::parse(enm.comment().value()); @@ -239,9 +241,6 @@ void tu_visitor::process_class_declaration(const cppast::cpp_class &cls, if (cls.comment().has_value()) c.decorators = decorators::parse(cls.comment().value()); - if (c.skip()) - return; - cppast::cpp_access_specifier_kind last_access_specifier = cppast::cpp_access_specifier_kind::cpp_private; @@ -256,6 +255,11 @@ void tu_visitor::process_class_declaration(const cppast::cpp_class &cls, c.decorators = decorators::parse(cls.comment().value()); } + if (c.skip()) + return; + + c.style = c.style_spec(); + // Process class child entities if (c.is_struct) last_access_specifier = cppast::cpp_access_specifier_kind::cpp_public; @@ -515,7 +519,7 @@ void tu_visitor::process_class_declaration(const cppast::cpp_class &cls, bool tu_visitor::process_field_with_template_instantiation( const cppast::cpp_member_variable &mv, const cppast::cpp_type &tr, - class_ &c, cppast::cpp_access_specifier_kind as) + class_ &c, class_member &m, cppast::cpp_access_specifier_kind as) { LOG_DBG("Processing field with template instatiation type {}", cppast::to_string(tr)); @@ -572,6 +576,17 @@ bool tu_visitor::process_field_with_template_instantiation( rr.type = relationship_t::kAggregation; rr.label = mv.name(); rr.scope = detail::cpp_access_specifier_to_scope(as); + rr.style = m.style_spec(); + + auto [decorator_rtype, decorator_rmult] = m.relationship(); + if (decorator_rtype != relationship_t::kNone) { + rr.type = decorator_rtype; + auto mult = util::split(decorator_rmult, ":"); + if (mult.size() == 2) { + rr.multiplicity_source = mult[0]; + rr.multiplicity_destination = mult[1]; + } + } LOG_DBG("Adding field instantiation relationship {} {} {} : {}", rr.destination, model::class_diagram::to_string(rr.type), c.usr, @@ -623,7 +638,7 @@ void tu_visitor::process_field(const cppast::cpp_member_variable &mv, class_ &c, else if (tr.kind() == cppast::cpp_type_kind::template_instantiation_t) { template_instantiation_added_as_aggregation = process_field_with_template_instantiation( - mv, resolve_alias(tr), c, as); + mv, resolve_alias(tr), c, m, as); } else if (tr.kind() == cppast::cpp_type_kind::unexposed_t) { LOG_DBG( @@ -646,6 +661,7 @@ void tu_visitor::process_field(const cppast::cpp_member_variable &mv, class_ &c, r.type = relationship_type; r.label = m.name; r.scope = m.scope; + r.style = m.style_spec(); auto [decorator_rtype, decorator_rmult] = m.relationship(); if (decorator_rtype != relationship_t::kNone) { diff --git a/src/uml/class_diagram_visitor.h b/src/uml/class_diagram_visitor.h index 48f4ef37..f2656bf8 100644 --- a/src/uml/class_diagram_visitor.h +++ b/src/uml/class_diagram_visitor.h @@ -166,6 +166,7 @@ public: bool process_field_with_template_instantiation( const cppast::cpp_member_variable &mv, const cppast::cpp_type &tr, clanguml::model::class_diagram::class_ &c, + clanguml::model::class_diagram::class_member &m, cppast::cpp_access_specifier_kind as); void process_static_field(const cppast::cpp_variable &mv, diff --git a/tests/t00029/test_case.h b/tests/t00029/test_case.h index 1225ad36..8cd2bc7e 100644 --- a/tests/t00029/test_case.h +++ b/tests/t00029/test_case.h @@ -45,7 +45,7 @@ TEST_CASE("t00029", "[test-case][class]") REQUIRE_THAT(puml, IsClass(_A("A"))); REQUIRE_THAT(puml, !IsClass(_A("B"))); REQUIRE_THAT(puml, IsClassTemplate("C", "T")); - REQUIRE_THAT(puml, IsClassTemplate("D", "T")); + REQUIRE_THAT(puml, !IsClassTemplate("D", "T")); REQUIRE_THAT(puml, IsEnum(_A("E"))); REQUIRE_THAT(puml, !IsEnum(_A("F"))); REQUIRE_THAT(puml, IsClass(_A("G1"))); diff --git a/tests/t00031/.clang-uml b/tests/t00031/.clang-uml new file mode 100644 index 00000000..276a156f --- /dev/null +++ b/tests/t00031/.clang-uml @@ -0,0 +1,12 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t00031_class: + type: class + glob: + - ../../tests/t00031/t00031.cc + using_namespace: + - clanguml::t00031 + include: + namespaces: + - clanguml::t00031 diff --git a/tests/t00031/t00031.cc b/tests/t00031/t00031.cc new file mode 100644 index 00000000..e01ff4b9 --- /dev/null +++ b/tests/t00031/t00031.cc @@ -0,0 +1,42 @@ +#include +#include + +namespace clanguml { +namespace t00031 { + +/// @uml{style[#back:lightgreen|yellow;header:blue/red]} +class A { +}; + +/// @uml{style[#line.dotted:blue]} +enum B { + one, + two, + three +}; + +/// @uml{style[#pink;line:red;line.bold;text:red]} +template class C { + T ttt; +}; + +class D { +}; + +struct R { + /// @uml{style[#red,dashed,thickness=2]} + A *aaa; + + /// @uml{composition} + /// @uml{style[#green,dashed,thickness=4]} + std::vector bbb; + + /// @uml{style[#blue,dotted,thickness=8]} + C ccc; + + /// @uml{style[#blue,plain,thickness=16]} + D *ddd; +}; + +} // namespace t00031 +} // namespace clanguml diff --git a/tests/t00031/test_case.h b/tests/t00031/test_case.h new file mode 100644 index 00000000..fce9ac45 --- /dev/null +++ b/tests/t00031/test_case.h @@ -0,0 +1,65 @@ +/** + * tests/t00031/test_case.cc + * + * Copyright (c) 2021 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("t00031", "[test-case][class]") +{ + auto [config, db] = load_config("t00031"); + + auto diagram = config.diagrams["t00031_class"]; + + REQUIRE(diagram->name == "t00031_class"); + + REQUIRE(diagram->include.namespaces.size() == 1); + REQUIRE_THAT(diagram->include.namespaces, + VectorContains(std::string{"clanguml::t00031"})); + + REQUIRE(diagram->exclude.namespaces.size() == 0); + + REQUIRE(diagram->should_include("clanguml::t00031::A")); + + auto model = generate_class_diagram(db, diagram); + + REQUIRE(model.name == "t00031_class"); + + auto puml = generate_class_puml(diagram, model); + AliasMatcher _A(puml); + + REQUIRE_THAT(puml, StartsWith("@startuml")); + REQUIRE_THAT(puml, EndsWith("@enduml\n")); + + REQUIRE_THAT(puml, IsClass(_A("A"))); + REQUIRE_THAT(puml, IsEnum(_A("B"))); + REQUIRE_THAT(puml, IsClassTemplate("C", "T")); + REQUIRE_THAT(puml, IsClass(_A("D"))); + + REQUIRE_THAT(puml, + IsAssociationWithStyle( + _A("R"), _A("A"), "+aaa", "#red,dashed,thickness=2")); + REQUIRE_THAT(puml, + IsCompositionWithStyle( + _A("R"), _A("B"), "+bbb", "#green,dashed,thickness=4")); + REQUIRE_THAT(puml, + IsAggregationWithStyle( + _A("R"), _A("C"), "+ccc", "#blue,dotted,thickness=8")); + REQUIRE_THAT(puml, + IsAssociationWithStyle( + _A("R"), _A("D"), "+ddd", "#blue,plain,thickness=16")); + + save_puml( + "./" + config.output_directory + "/" + diagram->name + ".puml", puml); +} diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 90455471..a3f2fe62 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -134,6 +134,7 @@ using namespace clanguml::test::matchers; #include "t00028/test_case.h" #include "t00029/test_case.h" #include "t00030/test_case.h" +#include "t00031/test_case.h" // // Sequence diagram tests diff --git a/tests/test_cases.h b/tests/test_cases.h index b55139e0..ddfd54b3 100644 --- a/tests/test_cases.h +++ b/tests/test_cases.h @@ -283,6 +283,33 @@ ContainsMatcher IsAggregation(std::string const &from, std::string const &to, fmt::format(format_string, from, to, label), caseSensitivity)); } +ContainsMatcher IsAggregationWithStyle(std::string const &from, + std::string const &to, std::string const &label, std::string style, + CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) +{ + return ContainsMatcher( + CasedString(fmt::format("{} o-[{}]- {} : {}", from, style, to, label), + caseSensitivity)); +} + +ContainsMatcher IsAssociationWithStyle(std::string const &from, + std::string const &to, std::string const &label, std::string style, + CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) +{ + return ContainsMatcher( + CasedString(fmt::format("{} -[{}]-> {} : {}", from, style, to, label), + caseSensitivity)); +} + +ContainsMatcher IsCompositionWithStyle(std::string const &from, + std::string const &to, std::string const &label, std::string style, + CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) +{ + return ContainsMatcher( + CasedString(fmt::format("{} *-[{}]- {} : {}", from, style, to, label), + caseSensitivity)); +} + ContainsMatcher IsInstantiation(std::string const &from, std::string const &to, CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) {