0.6.0
C++ to UML diagram generator based on Clang
Loading...
Searching...
No Matches
class_diagram_generator.cc
Go to the documentation of this file.
1/**
2 * @file src/class_diagram/generators/plantuml/class_diagram_generator.cc
3 *
4 * Copyright (c) 2021-2025 Bartek Kryza <bkryza@gmail.com>
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
20
21#include "util/error.h"
22
23#include <inja/inja.hpp>
24
26
28
32 *this, config.generate_packages()}
33{
34}
35
37 std::ostream &ostr, const class_diagram::model::class_element &e) const
38{
39 if (e.file_relative().empty())
40 return;
41
42 auto maybe_link_pattern = get_link_pattern(e);
43 if (maybe_link_pattern) {
44 const auto &[link_prefix, link_pattern] = *maybe_link_pattern;
47
49
50 ostr << " [[[";
51 ostr << common::jinja::render_template(env(), context, link_pattern)
52 .value_or("");
53 }
54
55 auto maybe_tooltip_pattern = get_tooltip_pattern(e);
56
57 if (maybe_tooltip_pattern) {
58 const auto &[tooltip_prefix, tooltip_pattern] = *maybe_tooltip_pattern;
62 context, tooltip_prefix);
63 ostr << "{";
64 ostr << common::jinja::render_template(env(), context, tooltip_pattern)
65 .value_or("");
66 ostr << "}";
67 }
68 ostr << "]]]";
69}
70
71void generator::generate_alias(const class_ &c, std::ostream &ostr) const
72{
73 std::string class_type{"class"};
74 if (c.is_abstract())
75 class_type = "abstract";
76
77 std::string full_name;
78 if (!config().generate_fully_qualified_name())
79 full_name = display_name_adapter(c).with_packages().full_name_no_ns();
80 else
81 full_name = display_name_adapter(c).full_name(true);
82
83 assert(!full_name.empty());
84
85 print_debug(c, ostr);
86
87 auto fn = config().simplify_template_type(full_name);
88
89 // PlantUML doesn't render empty template properly unless there's at least
90 // one character inside
91 if (util::ends_with(fn, std::string{"<>"})) {
92 fn = fn.substr(0, fn.size() - 2) + "< >";
93 }
94
95 ostr << class_type << " \"" << fn;
96
97 ostr << "\" as " << c.alias() << '\n';
98
99 // Register the added alias
100 m_generated_aliases.emplace(c.alias());
101}
102
103void generator::generate_alias(const enum_ &e, std::ostream &ostr) const
104{
105 print_debug(e, ostr);
106
107 if (!config().generate_fully_qualified_name())
108 ostr << "enum" << " \"" << display_name_adapter(e).name();
109 else
110 ostr << "enum" << " \"" << display_name_adapter(e).full_name(true);
111
112 ostr << "\" as " << e.alias() << '\n';
113
114 // Register the added alias
115 m_generated_aliases.emplace(e.alias());
116}
117
118void generator::generate_alias(const concept_ &c, std::ostream &ostr) const
119{
120 print_debug(c, ostr);
121
122 if (!config().generate_fully_qualified_name())
123 ostr << "class" << " \"" << c.full_name_no_ns();
124 else
125 ostr << "class" << " \"" << display_name_adapter(c).full_name(true);
126
127 ostr << "\" as " << c.alias() << '\n';
128
129 // Register the added alias
130 m_generated_aliases.emplace(c.alias());
131}
132
134 const objc_interface &e, std::ostream &ostr) const
135{
136 print_debug(e, ostr);
137
138 if (!e.is_protocol())
139 ostr << "class";
140 else
141 ostr << "protocol";
142
143 ostr << " \"" << display_name_adapter(e).full_name(true);
144
145 ostr << "\" as " << e.alias() << '\n';
146
147 // Register the added alias
148 m_generated_aliases.emplace(e.alias());
149}
150
151void generator::generate(const class_ &c, std::ostream &ostr) const
152{
153 std::string class_type{"class"};
154 if (c.is_abstract())
155 class_type = "abstract";
156
157 ostr << class_type << " " << c.alias();
158
159 if (c.is_union())
160 ostr << " " << "<<union>>";
161
162 if (config().generate_links) {
164 }
165
166 generate_style(ostr, c.type_name(), c);
167
168 ostr << " {" << '\n';
169
170 //
171 // Process methods
172 //
173 if (config().group_methods()) {
175 }
176 else {
177 generate_methods(c.methods(), ostr);
178 }
179
180 //
181 // Process relationships - here only generate the set of
182 // rendered_relationships we'll generate them in a seperate method
183 //
184 std::set<std::string> rendered_relations;
185
186 std::stringstream all_relations_str;
187 for (const auto &r : c.relationships()) {
188 try {
189 generate_relationship(r, rendered_relations);
190 }
191 catch (error::uml_alias_missing &e) {
192 LOG_DBG("Skipping {} relation from {} to {} due "
193 "to: {}",
194 to_string(r.type()), c.full_name(true), r.destination(),
195 e.what());
196 }
197 }
198
199 //
200 // Process members
201 //
202 std::vector<clanguml::class_diagram::model::class_member> members{
203 c.members()};
204
205 sort_class_elements(members);
206
207 if (config().group_methods())
208 ostr << "__\n";
209
210 for (const auto &m : members) {
211 if (!config().include_relations_also_as_members() &&
212 rendered_relations.find(m.name()) != rendered_relations.end())
213 continue;
214
215 generate_member(m, ostr);
216
217 ostr << '\n';
218 }
219
220 ostr << "}" << '\n';
221
222 generate_notes(ostr, c);
223
224 for (const auto &member : c.members())
225 generate_member_notes(ostr, member, c.alias());
226
227 for (const auto &method : c.methods())
228 generate_member_notes(ostr, method, c.alias());
229}
230
231void generator::start_method_group(std::ostream &ostr) const { ostr << "..\n"; }
232
234 const std::vector<class_method> &methods, std::ostream &ostr) const
235{
236 auto sorted_methods = methods;
237 sort_class_elements(sorted_methods);
238
239 for (const auto &m : sorted_methods) {
240 generate_method(m, ostr);
241 ostr << '\n';
242 }
243}
244
246 const std::vector<objc_method> &methods, std::ostream &ostr) const
247{
248 auto sorted_methods = methods;
249 sort_class_elements(sorted_methods);
250
251 for (const auto &m : sorted_methods) {
252 generate_method(m, ostr);
253 ostr << '\n';
254 }
255}
256
258 const class_diagram::model::class_method &m, std::ostream &ostr) const
259{
260 namespace plantuml_common = clanguml::common::generators::plantuml;
261 const auto &uns = config().using_namespace();
262
263 constexpr auto kAbbreviatedMethodArgumentsLength{15};
264
265 print_debug(m, ostr);
266
267 if (m.is_pure_virtual())
268 ostr << "{abstract} ";
269
270 if (m.is_static())
271 ostr << "{static} ";
272
273 std::string type{uns.relative(config().simplify_template_type(m.type()))};
274
275 ostr << plantuml_common::to_plantuml(m.access()) << m.name();
276
277 if (!m.template_params().empty()) {
278 m.render_template_params(ostr, config().using_namespace(), false);
279 }
280
281 ostr << "(";
282 if (config().generate_method_arguments() !=
284 std::vector<std::string> params;
285 std::transform(m.parameters().cbegin(), m.parameters().cend(),
286 std::back_inserter(params), [this](const auto &mp) {
287 return config().simplify_template_type(
288 mp.to_string(config().using_namespace()));
289 });
290 auto args_string = fmt::format("{}", fmt::join(params, ", "));
291 if (config().generate_method_arguments() ==
293 args_string = clanguml::util::abbreviate(
294 args_string, kAbbreviatedMethodArgumentsLength);
295 }
296 ostr << args_string;
297 }
298 ostr << ")";
299
300 if (m.is_constexpr())
301 ostr << " constexpr";
302 else if (m.is_consteval())
303 ostr << " consteval";
304
305 if (m.is_const())
306 ostr << " const";
307
308 if (m.is_noexcept())
309 ostr << " noexcept";
310
311 assert(!(m.is_pure_virtual() && m.is_defaulted()));
312
313 if (m.is_pure_virtual())
314 ostr << " = 0";
315
316 if (m.is_defaulted())
317 ostr << " = default";
318 else if (m.is_deleted())
319 ostr << " = deleted";
320
321 if (m.is_coroutine())
322 ostr << " [coroutine]";
323
324 ostr << " : " << type;
325
326 if (config().generate_links) {
327 generate_link(ostr, m);
328 }
329}
330
332 const class_diagram::model::class_member &m, std::ostream &ostr) const
333{
334 namespace plantuml_common = clanguml::common::generators::plantuml;
335 const auto &uns = config().using_namespace();
336
337 print_debug(m, ostr);
338
339 if (m.is_static())
340 ostr << "{static} ";
341
342 ostr << plantuml_common::to_plantuml(m.access())
343 << display_name_adapter(m).name() << " : "
344 << uns.relative(config().simplify_template_type(
345 display_name_adapter(m).type()));
346
347 if (config().generate_links) {
348 generate_link(ostr, m);
349 }
350}
351
352void generator::generate(const concept_ &c, std::ostream &ostr) const
353{
354 std::string class_type{"class"};
355
356 ostr << class_type << " " << c.alias() << " <<concept>>";
357
358 if (config().generate_links) {
360 }
361
362 generate_style(ostr, c.type_name(), c);
363
364 ostr << " {" << '\n';
365
366 if (config().generate_concept_requirements() &&
367 (c.requires_parameters().size() + c.requires_statements().size() > 0)) {
368 std::vector<std::string> parameters;
369 parameters.reserve(c.requires_parameters().size());
370 for (const auto &p : c.requires_parameters()) {
371 parameters.emplace_back(p.to_string(config().using_namespace()));
372 }
373
374 ostr << fmt::format("({})\n", fmt::join(parameters, ","));
375
376 ostr << "..\n";
377
378 ostr << fmt::format("{}\n", fmt::join(c.requires_statements(), "\n"));
379 }
380
381 ostr << "}" << '\n';
382}
383
384void generator::generate(const objc_interface &c, std::ostream &ostr) const
385{
386 std::string class_type{"class"};
387 if (c.is_protocol())
388 class_type = "protocol";
389
390 ostr << class_type << " " << c.alias();
391
392 ostr << " ";
393 if (c.is_protocol())
394 ostr << "<<ObjC Protocol>>";
395 else if (c.is_category())
396 ostr << "<<ObjC Category>>";
397 else
398 ostr << "<<ObjC Interface>>";
399
400 if (config().generate_links) {
402 }
403
404 generate_style(ostr, c.type_name(), c);
405
406 ostr << " {" << '\n';
407
408 //
409 // Process methods
410 //
411 generate_methods(c.methods(), ostr);
412
413 //
414 // Process relationships - here only generate the set of
415 // rendered_relationships we'll generate them in a seperate method
416 //
417 std::set<std::string> rendered_relations;
418
419 std::stringstream all_relations_str;
420 for (const auto &r : c.relationships()) {
421 try {
422 generate_relationship(r, rendered_relations);
423 }
424 catch (error::uml_alias_missing &e) {
425 LOG_DBG("Skipping {} relation from {} to {} due "
426 "to: {}",
427 to_string(r.type()), c.full_name(true), r.destination(),
428 e.what());
429 }
430 }
431
432 //
433 // Process members
434 //
435 std::vector<clanguml::class_diagram::model::objc_member> members{
436 c.members()};
437
438 sort_class_elements(members);
439
440 if (config().group_methods())
441 ostr << "__\n";
442
443 for (const auto &m : members) {
444 if (!config().include_relations_also_as_members() &&
445 rendered_relations.find(m.name()) != rendered_relations.end())
446 continue;
447
448 generate_member(m, ostr);
449
450 ostr << '\n';
451 }
452
453 ostr << "}" << '\n';
454
455 generate_notes(ostr, c);
456
457 for (const auto &member : c.members())
458 generate_member_notes(ostr, member, c.alias());
459
460 for (const auto &method : c.methods())
461 generate_member_notes(ostr, method, c.alias());
462}
463
465 const class_diagram::model::objc_method &m, std::ostream &ostr) const
466{
467 namespace plantuml_common = clanguml::common::generators::plantuml;
468 const auto &uns = config().using_namespace();
469
470 constexpr auto kAbbreviatedMethodArgumentsLength{15};
471
472 print_debug(m, ostr);
473
474 if (m.is_static())
475 ostr << "{static} ";
476
477 std::string type{uns.relative(config().simplify_template_type(m.type()))};
478
479 ostr << plantuml_common::to_plantuml(m.access()) << m.name();
480
481 ostr << "(";
482 if (config().generate_method_arguments() !=
484 std::vector<std::string> params;
485 std::transform(m.parameters().cbegin(), m.parameters().cend(),
486 std::back_inserter(params), [this](const auto &mp) {
487 return config().simplify_template_type(
488 mp.to_string(config().using_namespace()));
489 });
490 auto args_string = fmt::format("{}", fmt::join(params, ", "));
491 if (config().generate_method_arguments() ==
493 args_string = clanguml::util::abbreviate(
494 args_string, kAbbreviatedMethodArgumentsLength);
495 }
496 ostr << args_string;
497 }
498 ostr << ")";
499
500 ostr << " : " << type;
501
502 if (config().generate_links) {
503 generate_link(ostr, m);
504 }
505}
506
508 const class_diagram::model::objc_member &m, std::ostream &ostr) const
509{
510 namespace plantuml_common = clanguml::common::generators::plantuml;
511 const auto &uns = config().using_namespace();
512
513 print_debug(m, ostr);
514
515 if (m.is_static())
516 ostr << "{static} ";
517
518 ostr << plantuml_common::to_plantuml(m.access())
519 << display_name_adapter(m).name() << " : "
520 << uns.relative(config().simplify_template_type(
521 display_name_adapter(m).type()));
522
523 if (config().generate_links) {
524 generate_link(ostr, m);
525 }
526}
527
528void generator::generate_member_notes(std::ostream &ostr,
529 const class_element &member, const std::string &alias) const
530{
531 for (const auto &decorator : member.decorators()) {
532 auto note = std::dynamic_pointer_cast<decorators::note>(decorator);
533 if (note && note->applies_to_diagram(config().name)) {
534 ostr << "note " << note->position << " of " << alias
535 << "::" << member.name() << '\n'
536 << note->text << '\n'
537 << "end note\n";
538 }
539 }
540}
541
543 const relationship &r, std::set<std::string> &rendered_relations) const
544{
545 namespace plantuml_common = clanguml::common::generators::plantuml;
546
547 LOG_DBG("Processing relationship {}", to_string(r.type()));
548
549 std::string destination;
550
551 auto target_element = model().get(r.destination());
552 if (!target_element.has_value())
553 throw error::uml_alias_missing{fmt::format(
554 "Missing element in the model for ID: {}", r.destination())};
555
556 destination = target_element.value().full_name(false);
557
558 if (util::starts_with(destination, std::string{"::"}))
559 destination = destination.substr(2, destination.size());
560
561 std::string puml_relation;
562 if (!r.multiplicity_source().empty())
563 puml_relation += "\"" + r.multiplicity_source() + "\" ";
564
565 puml_relation += plantuml_common::to_plantuml(r, config());
566
567 if (!r.multiplicity_destination().empty())
568 puml_relation += " \"" + r.multiplicity_destination() + "\"";
569
570 if (!r.label().empty()) {
571 rendered_relations.emplace(r.label());
572 }
573}
574
576 const class_ &c, std::ostream &ostr) const
577{
578 namespace plantuml_common = clanguml::common::generators::plantuml;
579
580 //
581 // Process relationships
582 //
583 std::set<std::string> rendered_relations;
584
585 std::stringstream all_relations_str;
586 std::set<std::string> unique_relations;
587
588 for (const auto &r : c.relationships()) {
589 LOG_DBG("== Processing relationship {}",
590 plantuml_common::to_plantuml(r, config()));
591
592 std::stringstream relstr;
593 eid_t destination{};
594 try {
595 destination = r.destination();
596
597 std::string puml_relation;
598 if (!r.multiplicity_source().empty())
599 puml_relation += "\"" + r.multiplicity_source() + "\" ";
600
601 puml_relation += plantuml_common::to_plantuml(r, config());
602
603 if (!r.multiplicity_destination().empty())
604 puml_relation += " \"" + r.multiplicity_destination() + "\"";
605
606 std::string target_alias;
607 try {
608 target_alias = model().to_alias(destination);
609 }
610 catch (...) {
611 LOG_DBG("Failed to find alias to {}", destination);
612 continue;
613 }
614
615 if (m_generated_aliases.find(target_alias) ==
617 continue;
618
619 if (r.type() != relationship_t::kExtension)
620 relstr << c.alias() << " " << puml_relation << " "
621 << target_alias;
622 else
623 relstr << target_alias << " <|-- " << c.alias();
624
625 if (config().generate_links) {
627 relstr, r);
628 }
629
630 if (!r.label().empty()) {
631 relstr << " : " << plantuml_common::to_plantuml(r.access())
632 << r.label();
633 rendered_relations.emplace(r.label());
634 }
635
636 if (unique_relations.count(relstr.str()) == 0) {
637 unique_relations.emplace(relstr.str());
638
639 LOG_TRACE("=== Adding relation {}", relstr.str());
640
641 all_relations_str << relstr.str() << '\n';
642 }
643 }
644 catch (error::uml_alias_missing &e) {
645 LOG_DBG("=== Skipping {} relation from {} to {} due "
646 "to: {}",
647 to_string(r.type()), c.full_name(true), destination, e.what());
648 }
649 }
650
651 ostr << all_relations_str.str();
652}
653
655 const concept_ &c, std::ostream &ostr) const
656{
657 namespace plantuml_common = clanguml::common::generators::plantuml;
658
659 //
660 // Process relationships
661 //
662 std::set<std::string> rendered_relations;
663
664 std::stringstream all_relations_str;
665 std::set<std::string> unique_relations;
666
667 for (const auto &r : c.relationships()) {
668 if (!model().should_include(r.type()))
669 continue;
670
671 LOG_DBG("== Processing relationship {}", to_string(r.type()));
672
673 std::stringstream relstr;
674 eid_t destination{};
675 try {
676 destination = r.destination();
677
678 std::string puml_relation;
679 if (!r.multiplicity_source().empty())
680 puml_relation += "\"" + r.multiplicity_source() + "\" ";
681
682 puml_relation += plantuml_common::to_plantuml(r, config());
683
684 if (!r.multiplicity_destination().empty())
685 puml_relation += " \"" + r.multiplicity_destination() + "\"";
686
687 std::string target_alias;
688 try {
689 target_alias = model().to_alias(destination);
690 }
691 catch (...) {
692 LOG_DBG("Failed to find alias to {}", destination);
693 continue;
694 }
695
696 if (m_generated_aliases.find(target_alias) ==
698 continue;
699
700 relstr << c.alias() << " " << puml_relation << " " << target_alias;
701
702 if (config().generate_links) {
704 relstr, r);
705 }
706
707 if (!r.label().empty()) {
708 relstr << " : " << plantuml_common::to_plantuml(r.access())
709 << r.label();
710 rendered_relations.emplace(r.label());
711 }
712
713 if (unique_relations.count(relstr.str()) == 0) {
714 unique_relations.emplace(relstr.str());
715
716 LOG_TRACE("=== Adding relation {}", relstr.str());
717
718 all_relations_str << relstr.str() << '\n';
719 }
720 }
721 catch (error::uml_alias_missing &e) {
722 LOG_DBG("=== Skipping {} relation from {} to {} due "
723 "to: {}",
724 to_string(r.type()), c.full_name(true), destination, e.what());
725 }
726 }
727
728 ostr << all_relations_str.str();
729}
730
731void generator::generate(const enum_ &e, std::ostream &ostr) const
732{
733 ostr << "enum " << e.alias();
734
735 if (config().generate_links) {
737 }
738
739 generate_style(ostr, e.type_name(), e);
740
741 ostr << " {" << '\n';
742
743 for (const auto &enum_constant : e.constants()) {
744 ostr << enum_constant << '\n';
745 }
746
747 ostr << "}" << '\n';
748
749 generate_notes(ostr, e);
750}
751
752void generator::generate_relationships(const enum_ &e, std::ostream &ostr) const
753{
754 for (const auto &r : e.relationships()) {
755 eid_t destination{};
756 std::stringstream relstr;
757 try {
758 destination = r.destination();
759
760 auto target_alias = model().to_alias(destination);
761
762 if (m_generated_aliases.find(target_alias) ==
764 continue;
765
766 relstr << e.alias() << " "
768 r, config())
769 << " " << target_alias;
770
771 if (config().generate_links) {
773 relstr, r);
774 }
775
776 if (!r.label().empty())
777 relstr << " : " << r.label();
778
779 relstr << '\n';
780
781 ostr << relstr.str();
782 }
783 catch (error::uml_alias_missing &ex) {
784 LOG_DBG("Skipping {} relation from {} to {} due "
785 "to: {}",
787 r, config()),
788 e.full_name(true), destination, ex.what());
789 }
790 }
791}
792
794 const objc_interface &c, std::ostream &ostr) const
795{
796 namespace plantuml_common = clanguml::common::generators::plantuml;
797
798 //
799 // Process relationships
800 //
801 std::set<std::string> rendered_relations;
802
803 std::stringstream all_relations_str;
804 std::set<std::string> unique_relations;
805
806 for (const auto &r : c.relationships()) {
807 LOG_DBG("== Processing relationship {}",
808 plantuml_common::to_plantuml(r, config()));
809
810 std::stringstream relstr;
811 eid_t destination{};
812 try {
813 destination = r.destination();
814
815 std::string puml_relation;
816 if (!r.multiplicity_source().empty())
817 puml_relation += "\"" + r.multiplicity_source() + "\" ";
818
819 puml_relation += plantuml_common::to_plantuml(r, config());
820
821 if (!r.multiplicity_destination().empty())
822 puml_relation += " \"" + r.multiplicity_destination() + "\"";
823
824 std::string target_alias;
825 try {
826 target_alias = model().to_alias(destination);
827 }
828 catch (...) {
829 LOG_DBG("Failed to find alias to {}", destination);
830 continue;
831 }
832
833 if (m_generated_aliases.find(target_alias) ==
835 continue;
836
837 if (r.type() != relationship_t::kExtension)
838 relstr << c.alias() << " " << puml_relation << " "
839 << target_alias;
840 else
841 relstr << target_alias << " <|-- " << c.alias();
842
843 if (config().generate_links) {
845 relstr, r);
846 }
847
848 if (!r.label().empty()) {
849 relstr << " : " << plantuml_common::to_plantuml(r.access())
850 << r.label();
851 rendered_relations.emplace(r.label());
852 }
853
854 if (unique_relations.count(relstr.str()) == 0) {
855 unique_relations.emplace(relstr.str());
856
857 LOG_TRACE("=== Adding relation {}", relstr.str());
858
859 all_relations_str << relstr.str() << '\n';
860 }
861 }
862 catch (error::uml_alias_missing &e) {
863 LOG_DBG("=== Skipping {} relation from {} to {} due "
864 "to: {}",
865 to_string(r.type()), c.full_name(true), destination, e.what());
866 }
867 }
868
869 ostr << all_relations_str.str();
870}
871
872void generator::start_package(const package &p, std::ostream &ostr) const
873{
874 if (config().generate_packages()) {
875 LOG_DBG("Generating package {}",
876 display_name_adapter(p).with_packages().name());
877
878 print_debug(p, ostr);
879 ostr << "package [";
880 ostr << display_name_adapter(p).with_packages().name() << "] ";
881 ostr << "as " << p.alias();
882
883 if (p.is_deprecated())
884 ostr << " <<deprecated>>";
885
886 generate_style(ostr, p.type_name(), p);
887
888 ostr << " {" << '\n';
889 }
890}
891
892void generator::end_package(const package &p, std::ostream &ostr) const
893{
894 if (config().generate_packages()) {
895 ostr << "}" << '\n';
896 generate_notes(ostr, p);
897 }
898}
899
901 const std::string & /*group_name*/, std::ostream &ostr) const
902{
903 ostr << "together {\n";
904}
905
907 const std::string & /*group_name*/, std::ostream &ostr) const
908{
909 ostr << "}\n";
910}
911
912void generator::generate_diagram(std::ostream &ostr) const
913{
915
916 generate_groups(ostr);
917
919
921}
922
923} // namespace clanguml::class_diagram::generators::plantuml