diff --git a/src/puml/class_diagram_generator.h b/src/puml/class_diagram_generator.h index b25f8dbc..9ebb9790 100644 --- a/src/puml/class_diagram_generator.h +++ b/src/puml/class_diagram_generator.h @@ -224,9 +224,18 @@ public: destination = r.destination; } + std::string puml_relation; + if (!r.multiplicity_source.empty()) + puml_relation += "\"" + r.multiplicity_source + "\" "; + + puml_relation += to_string(r.type); + + if (!r.multiplicity_destination.empty()) + puml_relation += " \"" + r.multiplicity_destination + "\""; + relstr << m_model.to_alias( uns, ns_relative(uns, c.full_name(uns))) - << " " << to_string(r.type) << " " + << " " << puml_relation << " " << m_model.to_alias(uns, ns_relative(uns, destination)); if (!r.label.empty()) { diff --git a/src/uml/class_diagram_model.h b/src/uml/class_diagram_model.h index 1b5faacf..f3f95d7a 100644 --- a/src/uml/class_diagram_model.h +++ b/src/uml/class_diagram_model.h @@ -57,7 +57,7 @@ std::string to_string(relationship_t r); struct decorated_element { std::vector> decorators; - bool skip() + bool skip() const { for (auto d : decorators) if (std::dynamic_pointer_cast(d)) @@ -66,7 +66,7 @@ struct decorated_element { return false; } - bool skip_relationship() + bool skip_relationship() const { for (auto d : decorators) if (std::dynamic_pointer_cast(d)) @@ -74,6 +74,25 @@ struct decorated_element { return false; } + + std::pair relationship() const + { + for (auto &d : decorators) + if (std::dynamic_pointer_cast(d)) + return {relationship_t::kAssociation, + std::dynamic_pointer_cast(d) + ->multiplicity}; + else if (std::dynamic_pointer_cast(d)) + return {relationship_t::kAggregation, + std::dynamic_pointer_cast(d) + ->multiplicity}; + else if (std::dynamic_pointer_cast(d)) + return {relationship_t::kComposition, + std::dynamic_pointer_cast(d) + ->multiplicity}; + + return {relationship_t::kNone, ""}; + } }; class element : public decorated_element { @@ -141,8 +160,8 @@ struct class_parent { struct class_relationship : public decorated_element { relationship_t type{relationship_t::kAssociation}; std::string destination; - std::string cardinality_source; - std::string cardinality_destination; + std::string multiplicity_source; + std::string multiplicity_destination; std::string label; scope_t scope{scope_t::kNone}; diff --git a/src/uml/class_diagram_visitor.cc b/src/uml/class_diagram_visitor.cc index c3deed87..613dc50b 100644 --- a/src/uml/class_diagram_visitor.cc +++ b/src/uml/class_diagram_visitor.cc @@ -647,6 +647,16 @@ void tu_visitor::process_field(const cppast::cpp_member_variable &mv, class_ &c, r.label = m.name; r.scope = m.scope; + auto [decorator_rtype, decorator_rmult] = m.relationship(); + if (decorator_rtype != relationship_t::kNone) { + r.type = decorator_rtype; + auto mult = util::split(decorator_rmult, ":"); + if (mult.size() == 2) { + r.multiplicity_source = mult[0]; + r.multiplicity_destination = mult[1]; + } + } + LOG_DBG("Adding field relationship {} {} {} : {}", r.destination, model::class_diagram::to_string(r.type), c.usr, r.label); diff --git a/src/uml/decorators.cc b/src/uml/decorators.cc index 05bb4b37..23b9f8fe 100644 --- a/src/uml/decorators.cc +++ b/src/uml/decorators.cc @@ -30,6 +30,8 @@ const std::string skip::label = "skip"; const std::string skip_relationship::label = "skiprelationship"; const std::string style::label = "style"; const std::string aggregation::label = "aggregation"; +const std::string composition::label = "composition"; +const std::string association::label = "association"; std::shared_ptr decorator::from_string(std::string_view c) { @@ -48,6 +50,12 @@ std::shared_ptr decorator::from_string(std::string_view c) else if (c.find(aggregation::label) == 0) { return aggregation::from_string(c); } + else if (c.find(composition::label) == 0) { + return composition::from_string(c); + } + else if (c.find(association::label) == 0) { + return association::from_string(c); + } return {}; } @@ -151,6 +159,28 @@ std::shared_ptr aggregation::from_string(std::string_view c) return res; } +std::shared_ptr composition::from_string(std::string_view c) +{ + auto res = std::make_shared(); + auto toks = res->tokenize(composition::label, c); + + res->diagrams = toks.diagrams; + res->multiplicity = toks.param; + + return res; +} + +std::shared_ptr association::from_string(std::string_view c) +{ + auto res = std::make_shared(); + auto toks = res->tokenize(association::label, c); + + res->diagrams = toks.diagrams; + res->multiplicity = toks.param; + + return res; +} + std::vector> parse( std::string documentation_block, std::string clanguml_tag) { diff --git a/src/uml/decorators.h b/src/uml/decorators.h index 1c2bf77c..04c2f10d 100644 --- a/src/uml/decorators.h +++ b/src/uml/decorators.h @@ -72,10 +72,25 @@ struct style : public decorator { static std::shared_ptr from_string(std::string_view c); }; -struct aggregation : public decorator { +struct relationship : public decorator { + std::string multiplicity; +}; + +struct aggregation : public relationship { + static const std::string label; + + static std::shared_ptr from_string(std::string_view c); +}; + +struct composition : public relationship { + static const std::string label; + + static std::shared_ptr from_string(std::string_view c); +}; + +struct association : public relationship { static const std::string label; - std::string multiplicity; static std::shared_ptr from_string(std::string_view c); }; diff --git a/tests/t00029/.clang-uml b/tests/t00029/.clang-uml new file mode 100644 index 00000000..9c673a31 --- /dev/null +++ b/tests/t00029/.clang-uml @@ -0,0 +1,12 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t00029_class: + type: class + glob: + - ../../tests/t00029/t00029.cc + using_namespace: + - clanguml::t00029 + include: + namespaces: + - clanguml::t00029 diff --git a/tests/t00029/t00029.cc b/tests/t00029/t00029.cc new file mode 100644 index 00000000..b56162bb --- /dev/null +++ b/tests/t00029/t00029.cc @@ -0,0 +1,53 @@ +#include +#include + +namespace clanguml { +namespace t00029 { + +class A { +}; + +/// \uml{skip} +class B { +}; + +template class C { + T param; +}; + +/// @uml{skip:t00029_class} +template class D { + T param; +}; + +enum class E { one, two, three }; + +/// \uml{skip} +enum class F { red, green, blue }; + +class G1 { +}; + +class G2 { +}; + +class G3 { +}; + +class G4 { +}; + +struct R { + G1 g1; + + /// \uml{skip} + G2 g2; + + /// \uml{skiprelationship} + G3 &g3; + + std::shared_ptr g4; +}; + +} // namespace t00029 +} // namespace clanguml diff --git a/tests/t00029/test_case.h b/tests/t00029/test_case.h new file mode 100644 index 00000000..1225ad36 --- /dev/null +++ b/tests/t00029/test_case.h @@ -0,0 +1,65 @@ +/** + * tests/t00029/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("t00029", "[test-case][class]") +{ + auto [config, db] = load_config("t00029"); + + auto diagram = config.diagrams["t00029_class"]; + + REQUIRE(diagram->name == "t00029_class"); + + REQUIRE(diagram->include.namespaces.size() == 1); + REQUIRE_THAT(diagram->include.namespaces, + VectorContains(std::string{"clanguml::t00029"})); + + REQUIRE(diagram->exclude.namespaces.size() == 0); + + REQUIRE(diagram->should_include("clanguml::t00029::A")); + + auto model = generate_class_diagram(db, diagram); + + REQUIRE(model.name == "t00029_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, !IsClass(_A("B"))); + REQUIRE_THAT(puml, IsClassTemplate("C", "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"))); + REQUIRE_THAT(puml, IsClass(_A("G2"))); + REQUIRE_THAT(puml, IsClass(_A("G3"))); + REQUIRE_THAT(puml, IsClass(_A("G4"))); + + REQUIRE_THAT(puml, IsClass(_A("R"))); + + REQUIRE_THAT(puml, IsAggregation(_A("R"), _A("G1"), "+g1")); + REQUIRE_THAT(puml, !IsAggregation(_A("R"), _A("G2"), "+g2")); + REQUIRE_THAT(puml, !IsAggregation(_A("R"), _A("G3"), "+g3")); + REQUIRE_THAT(puml, IsAssociation(_A("R"), _A("G4"), "+g4")); + + save_puml( + "./" + config.output_directory + "/" + diagram->name + ".puml", puml); +} diff --git a/tests/t00030/.clang-uml b/tests/t00030/.clang-uml new file mode 100644 index 00000000..1795565b --- /dev/null +++ b/tests/t00030/.clang-uml @@ -0,0 +1,12 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t00030_class: + type: class + glob: + - ../../tests/t00030/t00030.cc + using_namespace: + - clanguml::t00030 + include: + namespaces: + - clanguml::t00030 diff --git a/tests/t00030/t00030.cc b/tests/t00030/t00030.cc new file mode 100644 index 00000000..cd873e4a --- /dev/null +++ b/tests/t00030/t00030.cc @@ -0,0 +1,34 @@ +#include +#include + +namespace clanguml { +namespace t00030 { + +class A { +}; + +class B { +}; + +class C { +}; + +class D { +}; + +struct R { + /// @uml{association[]} + A aaa; + + /// @uml{composition[0..1:1..*]} + std::vector bbb; + + /// @uml{aggregation[0..1:1..5]} + std::vector ccc; + + /// @uml{association[:1]} + D ddd; +}; + +} // namespace t00030 +} // namespace clanguml diff --git a/tests/t00030/test_case.h b/tests/t00030/test_case.h new file mode 100644 index 00000000..3427bab5 --- /dev/null +++ b/tests/t00030/test_case.h @@ -0,0 +1,57 @@ +/** + * tests/t00030/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("t00030", "[test-case][class]") +{ + auto [config, db] = load_config("t00030"); + + auto diagram = config.diagrams["t00030_class"]; + + REQUIRE(diagram->name == "t00030_class"); + + REQUIRE(diagram->include.namespaces.size() == 1); + REQUIRE_THAT(diagram->include.namespaces, + VectorContains(std::string{"clanguml::t00030"})); + + REQUIRE(diagram->exclude.namespaces.size() == 0); + + REQUIRE(diagram->should_include("clanguml::t00030::A")); + + auto model = generate_class_diagram(db, diagram); + + REQUIRE(model.name == "t00030_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, IsClass(_A("B"))); + REQUIRE_THAT(puml, IsClass(_A("C"))); + REQUIRE_THAT(puml, IsClass(_A("D"))); + + REQUIRE_THAT(puml, IsAssociation(_A("R"), _A("A"), "+aaa")); + REQUIRE_THAT(puml, IsComposition(_A("R"), _A("B"), "+bbb", "0..1", "1..*")); + REQUIRE_THAT(puml, IsAggregation(_A("R"), _A("C"), "+ccc", "0..1", "1..5")); + REQUIRE_THAT(puml, IsAssociation(_A("R"), _A("D"), "+ddd", "", "1")); + + save_puml( + "./" + config.output_directory + "/" + diagram->name + ".puml", puml); +} diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 6a51ca17..90455471 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -133,6 +133,7 @@ using namespace clanguml::test::matchers; #include "t00027/test_case.h" #include "t00028/test_case.h" #include "t00029/test_case.h" +#include "t00030/test_case.h" // // Sequence diagram tests diff --git a/tests/test_cases.h b/tests/test_cases.h index 2d5bd79a..b55139e0 100644 --- a/tests/test_cases.h +++ b/tests/test_cases.h @@ -217,11 +217,23 @@ ContainsMatcher IsInnerClass(std::string const &parent, } ContainsMatcher IsAssociation(std::string const &from, std::string const &to, - std::string const &label, + std::string const &label, std::string multiplicity_source = "", + std::string multiplicity_dest = "", CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) { + std::string format_string = "{}"; + if (!multiplicity_source.empty()) + format_string += " \"" + multiplicity_source + "\""; + + format_string += " -->"; + + if (!multiplicity_dest.empty()) + format_string += " \"" + multiplicity_dest + "\""; + + format_string += " {} : {}"; + return ContainsMatcher(CasedString( - fmt::format("{} --> {} : {}", from, to, label), caseSensitivity)); + fmt::format(format_string, from, to, label), caseSensitivity)); } ContainsMatcher IsFriend(std::string const &from, std::string const &to, @@ -232,19 +244,43 @@ ContainsMatcher IsFriend(std::string const &from, std::string const &to, } ContainsMatcher IsComposition(std::string const &from, std::string const &to, - std::string const &label, + std::string const &label, std::string multiplicity_source = "", + std::string multiplicity_dest = "", CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) { + std::string format_string = "{}"; + if (!multiplicity_source.empty()) + format_string += " \"" + multiplicity_source + "\""; + + format_string += " *--"; + + if (!multiplicity_dest.empty()) + format_string += " \"" + multiplicity_dest + "\""; + + format_string += " {} : {}"; + return ContainsMatcher(CasedString( - fmt::format("{} *-- {} : {}", from, to, label), caseSensitivity)); + fmt::format(format_string, from, to, label), caseSensitivity)); } ContainsMatcher IsAggregation(std::string const &from, std::string const &to, - std::string const &label, + std::string const &label, std::string multiplicity_source = "", + std::string multiplicity_dest = "", CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) { + std::string format_string = "{}"; + if (!multiplicity_source.empty()) + format_string += " \"" + multiplicity_source + "\""; + + format_string += " o--"; + + if (!multiplicity_dest.empty()) + format_string += " \"" + multiplicity_dest + "\""; + + format_string += " {} : {}"; + return ContainsMatcher(CasedString( - fmt::format("{} o-- {} : {}", from, to, label), caseSensitivity)); + fmt::format(format_string, from, to, label), caseSensitivity)); } ContainsMatcher IsInstantiation(std::string const &from, std::string const &to,