From 00b93210348cd6d8f562f0be286ac41b9a64161d Mon Sep 17 00:00:00 2001 From: Bartek Kryza Date: Tue, 17 Jan 2023 23:43:44 +0100 Subject: [PATCH] Added --dump-config command line option (Fixes #77) --- CHANGELOG.md | 1 + docs/common_options.md | 6 + src/config/config.cc | 639 ++---------------- src/config/config.h | 76 ++- src/config/yaml_decoders.cc | 610 +++++++++++++++++ src/config/yaml_emitters.cc | 263 +++++++ src/main.cc | 26 + .../plantuml/sequence_diagram_generator.cc | 4 +- tests/test_config.cc | 26 + tests/test_config_data/complete.yml | 61 ++ 10 files changed, 1106 insertions(+), 606 deletions(-) create mode 100644 src/config/yaml_decoders.cc create mode 100644 src/config/yaml_emitters.cc create mode 100644 tests/test_config_data/complete.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index b93cf941..cb813fde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # CHANGELOG + * Added command line option (--dump-config) to print effective config (#77) * Added support for building with Microsoft Visual Studio ### 0.3.0 diff --git a/docs/common_options.md b/docs/common_options.md index a13870ee..52e46091 100644 --- a/docs/common_options.md +++ b/docs/common_options.md @@ -21,6 +21,12 @@ do not override this option. For detailed reference of all configuration options see [here](./configuration_file.md). +Effective configuration, including default values can be printed out in YAML format using the following option: + +```bash +clang-uml --dump-config +``` + ## Translation unit glob patterns One of the key options of the diagram configuration is the list of translation units, which should be parsed to get all necessary information for a diagram. diff --git a/src/config/config.cc b/src/config/config.cc index 36e56d5d..75297355 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -23,43 +23,6 @@ namespace clanguml::config { -config load(const std::string &config_file) -{ - try { - YAML::Node doc = YAML::LoadFile(config_file); - - // Store the parent path of the config_file to properly resolve - // the include files paths - auto config_file_path = - std::filesystem::absolute(std::filesystem::path{config_file}); - doc.force_insert( - "__parent_path", config_file_path.parent_path().string()); - - // If the current directory is also a git repository, - // load some config values, which can be included in the - // generated diagrams - if (util::is_git_repository() && !doc["git"]) { - YAML::Node git_config{YAML::NodeType::Map}; - git_config["branch"] = util::get_git_branch(); - git_config["revision"] = util::get_git_revision(); - git_config["commit"] = util::get_git_commit(); - git_config["toplevel"] = util::get_git_toplevel_dir(); - - doc["git"] = git_config; - } - - return doc.as(); - } - catch (YAML::BadFile &e) { - throw std::runtime_error(fmt::format( - "Could not open config file {}: {}", config_file, e.what())); - } - catch (YAML::Exception &e) { - throw std::runtime_error(fmt::format( - "Cannot parse YAML file {}: {}", config_file, e.what())); - } -} - std::string to_string(const hint_t t) { switch (t) { @@ -77,6 +40,49 @@ std::string to_string(const hint_t t) } } +std::string to_string(const method_arguments ma) +{ + switch (ma) { + case method_arguments::full: + return "full"; + case method_arguments::abbreviated: + return "abbreviated"; + case method_arguments::none: + return "none"; + default: + assert(false); + return ""; + } +} + +std::string to_string(const comment_parser_t cp) +{ + switch (cp) { + case comment_parser_t::clang: + return "clang"; + case comment_parser_t::plain: + return "plain"; + default: + assert(false); + return ""; + } +} + +std::string to_string(location_t cp) +{ + switch (cp) { + case location_t::fileline: + return "fileline"; + case location_t::function: + return "function"; + case location_t::marker: + return "marker"; + default: + assert(false); + return ""; + } +} + void plantuml::append(const plantuml &r) { before.insert(before.end(), r.before.begin(), r.before.end()); @@ -219,562 +225,3 @@ template <> void append_value(plantuml &l, const plantuml &r) l.append(r); } } // namespace clanguml::config - -namespace YAML { -using clanguml::common::model::access_t; -using clanguml::common::model::relationship_t; -using clanguml::config::class_diagram; -using clanguml::config::config; -using clanguml::config::filter; -using clanguml::config::generate_links_config; -using clanguml::config::git_config; -using clanguml::config::hint_t; -using clanguml::config::include_diagram; -using clanguml::config::layout_hint; -using clanguml::config::method_arguments; -using clanguml::config::package_diagram; -using clanguml::config::plantuml; -using clanguml::config::relationship_hint_t; -using clanguml::config::sequence_diagram; -using clanguml::config::source_location; - -inline bool has_key(const YAML::Node &n, const std::string &key) -{ - assert(n.Type() == NodeType::Map); - - return std::count_if(n.begin(), n.end(), [&key](auto &&n) { - return n.first.template as() == key; - }) > 0; -} - -template -void get_option(const Node &node, clanguml::config::option &option) -{ - if (node[option.name]) - option.set(node[option.name].template as()); -} - -template <> -void get_option(const Node &node, - clanguml::config::option &option) -{ - if (node[option.name]) { - if (node[option.name].Type() == NodeType::Scalar) - option.set({node[option.name].template as()}); - else if (node[option.name].Type() == NodeType::Sequence) - option.set( - {node[option.name].template as>()[0]}); - else - throw std::runtime_error("Invalid using_namespace value"); - } -} - -template <> -void get_option( - const Node &node, clanguml::config::option &option) -{ - if (node[option.name]) { - const auto &val = node[option.name].as(); - if (val == "full") - option.set(method_arguments::full); - else if (val == "abbreviated") - option.set(method_arguments::abbreviated); - else if (val == "none") - option.set(method_arguments::none); - else - throw std::runtime_error( - "Invalid generate_method_arguments value: " + val); - } -} - -template <> -void get_option(const Node &node, - clanguml::config::option &option) -{ - if (node[option.name]) { - const auto &val = node[option.name].as(); - if (val == "plain") - option.set(clanguml::config::comment_parser_t::plain); - else if (val == "clang") - option.set(clanguml::config::comment_parser_t::clang); - else - throw std::runtime_error("Invalid comment_parser value: " + val); - } -} - -std::shared_ptr parse_diagram_config(const Node &d) -{ - const auto diagram_type = d["type"].as(); - - if (diagram_type == "class") { - return std::make_shared(d.as()); - } - if (diagram_type == "sequence") { - return std::make_shared(d.as()); - } - if (diagram_type == "package") { - return std::make_shared(d.as()); - } - if (diagram_type == "include") { - return std::make_shared(d.as()); - } - - LOG_ERROR("Diagrams of type {} are not supported... ", diagram_type); - - return {}; -} - -// -// config std::filesystem::path decoder -// -template <> struct convert { - static bool decode(const Node &node, std::filesystem::path &rhs) - { - if (!node.IsScalar()) - return false; - - rhs = std::filesystem::path{node.as()}; - - return true; - } -}; - -// -// config access_t decoder -// -template <> struct convert { - static bool decode(const Node &node, access_t &rhs) - { - if (node.as() == "public") - rhs = access_t::kPublic; - else if (node.as() == "protected") - rhs = access_t::kProtected; - else if (node.as() == "private") - rhs = access_t::kPrivate; - else - return false; - - return true; - } -}; - -// -// config relationship_t decoder -// -template <> struct convert { - static bool decode(const Node &node, relationship_t &rhs) - { - assert(node.Type() == NodeType::Scalar); - - auto relationship_name = node.as(); - if (relationship_name == "extension" || - relationship_name == "inheritance") { - rhs = relationship_t::kExtension; - } - else if (relationship_name == "composition") { - rhs = relationship_t::kComposition; - } - else if (relationship_name == "aggregation") { - rhs = relationship_t::kAggregation; - } - else if (relationship_name == "containment") { - rhs = relationship_t::kContainment; - } - else if (relationship_name == "ownership") { - rhs = relationship_t::kOwnership; - } - else if (relationship_name == "association") { - rhs = relationship_t::kAssociation; - } - else if (relationship_name == "instantiation") { - rhs = relationship_t::kInstantiation; - } - else if (relationship_name == "friendship") { - rhs = relationship_t::kFriendship; - } - else if (relationship_name == "dependency") { - rhs = relationship_t::kDependency; - } - else if (relationship_name == "none") { - rhs = relationship_t::kNone; - } - else - return false; - - return true; - } -}; - -template <> struct convert> { - static bool decode(const Node &node, std::vector &rhs) - { - for (auto it = node.begin(); it != node.end(); ++it) { - const YAML::Node &n = *it; - if (n["usr"]) { - source_location loc; - loc.location_type = source_location::location_t::usr; - loc.location = n["usr"].as(); - rhs.emplace_back(std::move(loc)); - } - else if (n["marker"]) { - source_location loc; - loc.location_type = source_location::location_t::marker; - loc.location = n["marker"].as(); - rhs.emplace_back(std::move(loc)); - } - else if (n["file"] && n["line"]) { - source_location loc; - loc.location_type = source_location::location_t::fileline; - loc.location = n["file"].as() + ":" + - n["line"].as(); - rhs.emplace_back(std::move(loc)); - } - else if (n["function"]) { - source_location loc; - loc.location_type = source_location::location_t::function; - loc.location = n["function"].as(); - rhs.emplace_back(std::move(loc)); - } - else { - return false; - } - } - - return true; - } -}; - -template <> struct convert { - static bool decode(const Node &node, plantuml &rhs) - { - if (node["before"]) - rhs.before = node["before"].as(); - - if (node["after"]) - rhs.after = node["after"].as(); - return true; - } -}; - -// -// filter Yaml decoder -// -template <> struct convert { - static bool decode(const Node &node, filter &rhs) - { - if (node["namespaces"]) { - auto namespace_list = - node["namespaces"].as>(); - for (const auto &ns : namespace_list) - rhs.namespaces.push_back({ns}); - } - - if (node["relationships"]) - rhs.relationships = - node["relationships"].as(); - - if (node["elements"]) - rhs.elements = node["elements"].as(); - - if (node["access"]) - rhs.access = node["access"].as(); - - if (node["subclasses"]) - rhs.subclasses = node["subclasses"].as(); - - if (node["specializations"]) - rhs.specializations = - node["specializations"].as(); - - if (node["dependants"]) - rhs.dependants = node["dependants"].as(); - - if (node["dependencies"]) - rhs.dependencies = - node["dependencies"].as(); - - if (node["context"]) - rhs.context = node["context"].as(); - - if (node["paths"]) - rhs.paths = node["paths"].as(); - - return true; - } -}; - -// -// generate_links_config Yaml decoder -// -template <> struct convert { - static bool decode(const Node &node, generate_links_config &rhs) - { - if (node["link"]) - rhs.link = node["link"].as(); - - if (node["tooltip"]) - rhs.tooltip = node["tooltip"].as(); - - return true; - } -}; - -// -// git_config Yaml decoder -// -template <> struct convert { - static bool decode(const Node &node, git_config &rhs) - { - if (node["branch"]) - rhs.branch = node["branch"].as(); - - if (node["revision"]) - rhs.revision = node["revision"].as(); - - if (node["commit"]) - rhs.commit = node["commit"].as(); - - if (node["toplevel"]) - rhs.toplevel = node["toplevel"].as(); - - return true; - } -}; - -template bool decode_diagram(const Node &node, T &rhs) -{ - get_option(node, rhs.glob); - get_option(node, rhs.using_namespace); - get_option(node, rhs.include); - get_option(node, rhs.exclude); - get_option(node, rhs.puml); - get_option(node, rhs.git); - get_option(node, rhs.generate_links); - get_option(node, rhs.type_aliases); - get_option(node, rhs.comment_parser); - get_option(node, rhs.debug_mode); - - return true; -} - -// -// class_diagram Yaml decoder -// -template <> struct convert { - static bool decode(const Node &node, class_diagram &rhs) - { - if (!decode_diagram(node, rhs)) - return false; - - get_option(node, rhs.classes); - get_option(node, rhs.layout); - get_option(node, rhs.include_relations_also_as_members); - get_option(node, rhs.generate_method_arguments); - get_option(node, rhs.generate_packages); - get_option(node, rhs.relationship_hints); - get_option(node, rhs.type_aliases); - // get_option(node, rhs.comment_parser); - - rhs.initialize_relationship_hints(); - rhs.initialize_type_aliases(); - - return true; - } -}; - -// -// sequence_diagram Yaml decoder -// -template <> struct convert { - static bool decode(const Node &node, sequence_diagram &rhs) - { - if (!decode_diagram(node, rhs)) - return false; - - get_option(node, rhs.start_from); - get_option(node, rhs.combine_free_functions_into_file_participants); - get_option(node, rhs.relative_to); - get_option(node, rhs.participants_order); - get_option(node, rhs.generate_method_arguments); - - // Ensure relative_to has a value - if (!rhs.relative_to.has_value) - rhs.relative_to.set( - std::filesystem::current_path().lexically_normal()); - - rhs.initialize_type_aliases(); - - return true; - } -}; - -// -// package_diagram Yaml decoder -// -template <> struct convert { - static bool decode(const Node &node, package_diagram &rhs) - { - if (!decode_diagram(node, rhs)) - return false; - - get_option(node, rhs.layout); - - return true; - } -}; - -// -// include_diagram Yaml decoder -// -template <> struct convert { - static bool decode(const Node &node, include_diagram &rhs) - { - if (!decode_diagram(node, rhs)) - return false; - - get_option(node, rhs.layout); - get_option(node, rhs.relative_to); - get_option(node, rhs.generate_system_headers); - - if (!rhs.relative_to) - rhs.relative_to.set(std::filesystem::current_path()); - - // Convert the path in relative_to to an absolute path, with respect - // to the directory where the `.clang-uml` configuration file is located - if (rhs.relative_to) { - auto absolute_relative_to = - std::filesystem::path{node["__parent_path"].as()} / - rhs.relative_to(); - rhs.relative_to.set(absolute_relative_to.lexically_normal()); - } - - return true; - } -}; - -// -// layout_hint Yaml decoder -// -template <> struct convert { - static bool decode(const Node &node, layout_hint &rhs) - { - assert(node.Type() == NodeType::Map); - - if (node["up"]) { - rhs.hint = hint_t::up; - rhs.entity = node["up"].as(); - } - else if (node["down"]) { - rhs.hint = hint_t::down; - rhs.entity = node["down"].as(); - } - else if (node["left"]) { - rhs.hint = hint_t::left; - rhs.entity = node["left"].as(); - } - else if (node["right"]) { - rhs.hint = hint_t::right; - rhs.entity = node["right"].as(); - } - else - return false; - - return true; - } -}; - -// -// relationship_hint_t Yaml decoder -// -template <> struct convert { - static bool decode(const Node &node, relationship_hint_t &rhs) - { - assert(node.Type() == NodeType::Map || node.Type() == NodeType::Scalar); - - if (node.Type() == NodeType::Scalar) { - // This will be default relationship hint for all arguments - // of this template (useful for instance for tuples) - rhs.default_hint = node.as(); - } - else { - for (const auto &it : node) { - auto key = it.first.as(); - if (key == "default") { - rhs.default_hint = node["default"].as(); - } - else { - try { - auto index = stoul(key); - rhs.argument_hints[index] = - it.second.as(); - } - catch (std::exception &e) { - return false; - } - } - } - } - - return true; - } -}; - -// -// config Yaml decoder -// -template <> struct convert { - - static bool decode(const Node &node, config &rhs) - { - get_option(node, rhs.glob); - get_option(node, rhs.using_namespace); - get_option(node, rhs.output_directory); - get_option(node, rhs.compilation_database_dir); - get_option(node, rhs.include_relations_also_as_members); - get_option(node, rhs.puml); - get_option(node, rhs.generate_method_arguments); - get_option(node, rhs.generate_packages); - get_option(node, rhs.generate_links); - get_option(node, rhs.generate_system_headers); - get_option(node, rhs.git); - get_option(node, rhs.debug_mode); - rhs.base_directory.set(node["__parent_path"].as()); - get_option(node, rhs.relative_to); - - auto diagrams = node["diagrams"]; - - assert(diagrams.Type() == NodeType::Map); - - for (auto d : diagrams) { - auto name = d.first.as(); - std::shared_ptr diagram_config{}; - auto parent_path = node["__parent_path"].as(); - - if (has_key(d.second, "include!")) { - auto include_path = std::filesystem::path{parent_path}; - include_path /= d.second["include!"].as(); - - YAML::Node node = YAML::LoadFile(include_path.string()); - node.force_insert("__parent_path", parent_path); - - diagram_config = parse_diagram_config(node); - } - else { - d.second.force_insert("__parent_path", parent_path); - diagram_config = parse_diagram_config(d.second); - } - - if (diagram_config) { - diagram_config->name = name; - diagram_config->inherit(rhs); - rhs.diagrams[name] = diagram_config; - } - else { - return false; - } - } - - return true; - } -}; -} // namespace YAML diff --git a/src/config/config.h b/src/config/config.h index 2ebf3e49..9ed5c199 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -37,8 +37,12 @@ namespace config { enum class method_arguments { full, abbreviated, none }; +std::string to_string(method_arguments ma); + enum class comment_parser_t { plain, clang }; +std::string to_string(comment_parser_t cp); + struct plantuml { std::vector before; std::vector after; @@ -78,6 +82,8 @@ struct filter { enum class hint_t { up, down, left, right }; +std::string to_string(hint_t t); + struct layout_hint { hint_t hint{hint_t::up}; std::string entity; @@ -120,7 +126,14 @@ using relationship_hints_t = std::map; using type_aliases_t = std::map; -std::string to_string(hint_t t); +enum class location_t { marker, fileline, function }; + +std::string to_string(location_t cp); + +struct source_location { + location_t location_type{location_t::function}; + std::string location; +}; struct inheritable_diagram_options { option> glob{"glob"}; @@ -165,18 +178,11 @@ struct diagram : public inheritable_diagram_options { std::string name; }; -struct source_location { - enum class location_t { usr, marker, fileline, function }; - location_t location_type{location_t::function}; - std::string location; -}; - struct class_diagram : public diagram { ~class_diagram() override = default; common::model::diagram_t type() const override; - option> classes{"classes"}; option layout{"layout"}; void initialize_relationship_hints(); @@ -216,6 +222,60 @@ struct config : public inheritable_diagram_options { std::map> diagrams; }; +// +// YAML serialization emitters +// +YAML::Emitter &operator<<(YAML::Emitter &out, const config &c); + +YAML::Emitter &operator<<( + YAML::Emitter &out, const inheritable_diagram_options &c); + +YAML::Emitter &operator<<(YAML::Emitter &out, const filter &f); + +YAML::Emitter &operator<<(YAML::Emitter &out, const plantuml &p); + +YAML::Emitter &operator<<(YAML::Emitter &out, const method_arguments &ma); + +YAML::Emitter &operator<<(YAML::Emitter &out, const generate_links_config &glc); + +YAML::Emitter &operator<<(YAML::Emitter &out, const git_config &gc); + +YAML::Emitter &operator<<(YAML::Emitter &out, const relationship_hint_t &rh); + +YAML::Emitter &operator<<(YAML::Emitter &out, const comment_parser_t &cp); + +YAML::Emitter &operator<<(YAML::Emitter &out, const hint_t &h); + +YAML::Emitter &operator<<(YAML::Emitter &out, const class_diagram &c); + +YAML::Emitter &operator<<(YAML::Emitter &out, const sequence_diagram &c); + +YAML::Emitter &operator<<(YAML::Emitter &out, const include_diagram &c); + +YAML::Emitter &operator<<(YAML::Emitter &out, const package_diagram &c); + +YAML::Emitter &operator<<(YAML::Emitter &out, const layout_hint &c); + +YAML::Emitter &operator<<(YAML::Emitter &out, const source_location &sc); + +template +YAML::Emitter &operator<<(YAML::Emitter &out, const option &o) +{ + if (o.has_value) { + out << YAML::Key << o.name; + out << YAML::Value << o.value; + } + return out; +} + config load(const std::string &config_file); } // namespace config + +namespace common::model { +YAML::Emitter &operator<<(YAML::Emitter &out, const namespace_ &n); +YAML::Emitter &operator<<(YAML::Emitter &out, const relationship_t &r); +YAML::Emitter &operator<<(YAML::Emitter &out, const access_t &r); +YAML::Emitter &operator<<(YAML::Emitter &out, const diagram_t &d); +} // namespace common::model + } // namespace clanguml diff --git a/src/config/yaml_decoders.cc b/src/config/yaml_decoders.cc new file mode 100644 index 00000000..27417b60 --- /dev/null +++ b/src/config/yaml_decoders.cc @@ -0,0 +1,610 @@ +/** + * src/config/yaml_decoders.cc + * + * 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. + */ + +#include "config.h" + +namespace YAML { +using clanguml::common::model::access_t; +using clanguml::common::model::relationship_t; +using clanguml::config::class_diagram; +using clanguml::config::config; +using clanguml::config::filter; +using clanguml::config::generate_links_config; +using clanguml::config::git_config; +using clanguml::config::hint_t; +using clanguml::config::include_diagram; +using clanguml::config::layout_hint; +using clanguml::config::location_t; +using clanguml::config::method_arguments; +using clanguml::config::package_diagram; +using clanguml::config::plantuml; +using clanguml::config::relationship_hint_t; +using clanguml::config::sequence_diagram; +using clanguml::config::source_location; + +inline bool has_key(const YAML::Node &n, const std::string &key) +{ + assert(n.Type() == NodeType::Map); + + return std::count_if(n.begin(), n.end(), [&key](auto &&n) { + return n.first.template as() == key; + }) > 0; +} + +template +void get_option(const Node &node, clanguml::config::option &option) +{ + if (node[option.name]) + option.set(node[option.name].template as()); +} + +template <> +void get_option(const Node &node, + clanguml::config::option &option) +{ + if (node[option.name]) { + if (node[option.name].Type() == NodeType::Scalar) + option.set({node[option.name].template as()}); + else if (node[option.name].Type() == NodeType::Sequence) + option.set( + {node[option.name].template as>()[0]}); + else + throw std::runtime_error("Invalid using_namespace value"); + } +} + +template <> +void get_option( + const Node &node, clanguml::config::option &option) +{ + if (node[option.name]) { + const auto &val = node[option.name].as(); + if (val == "full") + option.set(method_arguments::full); + else if (val == "abbreviated") + option.set(method_arguments::abbreviated); + else if (val == "none") + option.set(method_arguments::none); + else + throw std::runtime_error( + "Invalid generate_method_arguments value: " + val); + } +} + +template <> +void get_option(const Node &node, + clanguml::config::option &option) +{ + if (node[option.name]) { + const auto &val = node[option.name].as(); + if (val == "plain") + option.set(clanguml::config::comment_parser_t::plain); + else if (val == "clang") + option.set(clanguml::config::comment_parser_t::clang); + else + throw std::runtime_error("Invalid comment_parser value: " + val); + } +} + +std::shared_ptr parse_diagram_config(const Node &d) +{ + const auto diagram_type = d["type"].as(); + + if (diagram_type == "class") { + return std::make_shared(d.as()); + } + if (diagram_type == "sequence") { + return std::make_shared(d.as()); + } + if (diagram_type == "package") { + return std::make_shared(d.as()); + } + if (diagram_type == "include") { + return std::make_shared(d.as()); + } + + LOG_ERROR("Diagrams of type {} are not supported... ", diagram_type); + + return {}; +} + +// +// config std::filesystem::path decoder +// +template <> struct convert { + static bool decode(const Node &node, std::filesystem::path &rhs) + { + if (!node.IsScalar()) + return false; + + rhs = std::filesystem::path{node.as()}; + + return true; + } +}; + +// +// config access_t decoder +// +template <> struct convert { + static bool decode(const Node &node, access_t &rhs) + { + if (node.as() == "public") + rhs = access_t::kPublic; + else if (node.as() == "protected") + rhs = access_t::kProtected; + else if (node.as() == "private") + rhs = access_t::kPrivate; + else + return false; + + return true; + } +}; + +// +// config relationship_t decoder +// +template <> struct convert { + static bool decode(const Node &node, relationship_t &rhs) + { + assert(node.Type() == NodeType::Scalar); + + auto relationship_name = node.as(); + if (relationship_name == "extension" || + relationship_name == "inheritance") { + rhs = relationship_t::kExtension; + } + else if (relationship_name == "composition") { + rhs = relationship_t::kComposition; + } + else if (relationship_name == "aggregation") { + rhs = relationship_t::kAggregation; + } + else if (relationship_name == "containment") { + rhs = relationship_t::kContainment; + } + else if (relationship_name == "ownership") { + rhs = relationship_t::kOwnership; + } + else if (relationship_name == "association") { + rhs = relationship_t::kAssociation; + } + else if (relationship_name == "instantiation") { + rhs = relationship_t::kInstantiation; + } + else if (relationship_name == "friendship") { + rhs = relationship_t::kFriendship; + } + else if (relationship_name == "dependency") { + rhs = relationship_t::kDependency; + } + else if (relationship_name == "none") { + rhs = relationship_t::kNone; + } + else + return false; + + return true; + } +}; + +template <> struct convert> { + static bool decode(const Node &node, std::vector &rhs) + { + for (auto it = node.begin(); it != node.end(); ++it) { + const YAML::Node &n = *it; + if (n["marker"]) { + source_location loc; + loc.location_type = location_t::marker; + loc.location = n["marker"].as(); + rhs.emplace_back(std::move(loc)); + } + else if (n["file"] && n["line"]) { + source_location loc; + loc.location_type = location_t::fileline; + loc.location = n["file"].as() + ":" + + n["line"].as(); + rhs.emplace_back(std::move(loc)); + } + else if (n["function"]) { + source_location loc; + loc.location_type = location_t::function; + loc.location = n["function"].as(); + rhs.emplace_back(std::move(loc)); + } + else { + return false; + } + } + + return true; + } +}; + +template <> struct convert { + static bool decode(const Node &node, plantuml &rhs) + { + if (node["before"]) + rhs.before = node["before"].as(); + + if (node["after"]) + rhs.after = node["after"].as(); + return true; + } +}; + +// +// filter Yaml decoder +// +template <> struct convert { + static bool decode(const Node &node, filter &rhs) + { + if (node["namespaces"]) { + auto namespace_list = + node["namespaces"].as>(); + for (const auto &ns : namespace_list) + rhs.namespaces.push_back({ns}); + } + + if (node["relationships"]) + rhs.relationships = + node["relationships"].as(); + + if (node["elements"]) + rhs.elements = node["elements"].as(); + + if (node["access"]) + rhs.access = node["access"].as(); + + if (node["subclasses"]) + rhs.subclasses = node["subclasses"].as(); + + if (node["specializations"]) + rhs.specializations = + node["specializations"].as(); + + if (node["dependants"]) + rhs.dependants = node["dependants"].as(); + + if (node["dependencies"]) + rhs.dependencies = + node["dependencies"].as(); + + if (node["context"]) + rhs.context = node["context"].as(); + + if (node["paths"]) + rhs.paths = node["paths"].as(); + + return true; + } +}; + +// +// generate_links_config Yaml decoder +// +template <> struct convert { + static bool decode(const Node &node, generate_links_config &rhs) + { + if (node["link"]) + rhs.link = node["link"].as(); + + if (node["tooltip"]) + rhs.tooltip = node["tooltip"].as(); + + return true; + } +}; + +// +// git_config Yaml decoder +// +template <> struct convert { + static bool decode(const Node &node, git_config &rhs) + { + if (node["branch"]) + rhs.branch = node["branch"].as(); + + if (node["revision"]) + rhs.revision = node["revision"].as(); + + if (node["commit"]) + rhs.commit = node["commit"].as(); + + if (node["toplevel"]) + rhs.toplevel = node["toplevel"].as(); + + return true; + } +}; + +template bool decode_diagram(const Node &node, T &rhs) +{ + get_option(node, rhs.glob); + get_option(node, rhs.using_namespace); + get_option(node, rhs.include); + get_option(node, rhs.exclude); + get_option(node, rhs.puml); + get_option(node, rhs.git); + get_option(node, rhs.generate_links); + get_option(node, rhs.type_aliases); + get_option(node, rhs.comment_parser); + get_option(node, rhs.debug_mode); + + return true; +} + +// +// class_diagram Yaml decoder +// +template <> struct convert { + static bool decode(const Node &node, class_diagram &rhs) + { + if (!decode_diagram(node, rhs)) + return false; + + get_option(node, rhs.layout); + get_option(node, rhs.include_relations_also_as_members); + get_option(node, rhs.generate_method_arguments); + get_option(node, rhs.generate_packages); + get_option(node, rhs.relationship_hints); + get_option(node, rhs.type_aliases); + get_option(node, rhs.comment_parser); + + rhs.initialize_relationship_hints(); + rhs.initialize_type_aliases(); + + return true; + } +}; + +// +// sequence_diagram Yaml decoder +// +template <> struct convert { + static bool decode(const Node &node, sequence_diagram &rhs) + { + if (!decode_diagram(node, rhs)) + return false; + + get_option(node, rhs.start_from); + get_option(node, rhs.combine_free_functions_into_file_participants); + get_option(node, rhs.relative_to); + get_option(node, rhs.participants_order); + get_option(node, rhs.generate_method_arguments); + + // Ensure relative_to has a value + if (!rhs.relative_to.has_value) + rhs.relative_to.set( + std::filesystem::current_path().lexically_normal()); + + rhs.initialize_type_aliases(); + + return true; + } +}; + +// +// package_diagram Yaml decoder +// +template <> struct convert { + static bool decode(const Node &node, package_diagram &rhs) + { + if (!decode_diagram(node, rhs)) + return false; + + get_option(node, rhs.layout); + + return true; + } +}; + +// +// include_diagram Yaml decoder +// +template <> struct convert { + static bool decode(const Node &node, include_diagram &rhs) + { + if (!decode_diagram(node, rhs)) + return false; + + get_option(node, rhs.layout); + get_option(node, rhs.relative_to); + get_option(node, rhs.generate_system_headers); + + if (!rhs.relative_to) + rhs.relative_to.set(std::filesystem::current_path()); + + // Convert the path in relative_to to an absolute path, with respect + // to the directory where the `.clang-uml` configuration file is located + if (rhs.relative_to) { + auto absolute_relative_to = + std::filesystem::path{node["__parent_path"].as()} / + rhs.relative_to(); + rhs.relative_to.set(absolute_relative_to.lexically_normal()); + } + + return true; + } +}; + +// +// layout_hint Yaml decoder +// +template <> struct convert { + static bool decode(const Node &node, layout_hint &rhs) + { + assert(node.Type() == NodeType::Map); + + if (node["up"]) { + rhs.hint = hint_t::up; + rhs.entity = node["up"].as(); + } + else if (node["down"]) { + rhs.hint = hint_t::down; + rhs.entity = node["down"].as(); + } + else if (node["left"]) { + rhs.hint = hint_t::left; + rhs.entity = node["left"].as(); + } + else if (node["right"]) { + rhs.hint = hint_t::right; + rhs.entity = node["right"].as(); + } + else + return false; + + return true; + } +}; + +// +// relationship_hint_t Yaml decoder +// +template <> struct convert { + static bool decode(const Node &node, relationship_hint_t &rhs) + { + assert(node.Type() == NodeType::Map || node.Type() == NodeType::Scalar); + + if (node.Type() == NodeType::Scalar) { + // This will be default relationship hint for all arguments + // of this template (useful for instance for tuples) + rhs.default_hint = node.as(); + } + else { + for (const auto &it : node) { + auto key = it.first.as(); + if (key == "default") { + rhs.default_hint = node["default"].as(); + } + else { + try { + auto index = stoul(key); + rhs.argument_hints[index] = + it.second.as(); + } + catch (std::exception &e) { + return false; + } + } + } + } + + return true; + } +}; + +// +// config Yaml decoder +// +template <> struct convert { + static bool decode(const Node &node, config &rhs) + { + get_option(node, rhs.glob); + get_option(node, rhs.using_namespace); + get_option(node, rhs.output_directory); + get_option(node, rhs.compilation_database_dir); + get_option(node, rhs.include_relations_also_as_members); + get_option(node, rhs.puml); + get_option(node, rhs.generate_method_arguments); + get_option(node, rhs.generate_packages); + get_option(node, rhs.generate_links); + get_option(node, rhs.generate_system_headers); + get_option(node, rhs.git); + get_option(node, rhs.debug_mode); + rhs.base_directory.set(node["__parent_path"].as()); + get_option(node, rhs.relative_to); + + auto diagrams = node["diagrams"]; + + assert(diagrams.Type() == NodeType::Map); + + for (auto d : diagrams) { + auto name = d.first.as(); + std::shared_ptr diagram_config{}; + auto parent_path = node["__parent_path"].as(); + + if (has_key(d.second, "include!")) { + auto include_path = std::filesystem::path{parent_path}; + include_path /= d.second["include!"].as(); + + YAML::Node node = YAML::LoadFile(include_path.string()); + node.force_insert("__parent_path", parent_path); + + diagram_config = parse_diagram_config(node); + } + else { + d.second.force_insert("__parent_path", parent_path); + diagram_config = parse_diagram_config(d.second); + } + + if (diagram_config) { + diagram_config->name = name; + diagram_config->inherit(rhs); + rhs.diagrams[name] = diagram_config; + } + else { + return false; + } + } + + return true; + } +}; +} // namespace YAML + +namespace clanguml::config { +config load(const std::string &config_file) +{ + try { + YAML::Node doc = YAML::LoadFile(config_file); + + // Store the parent path of the config_file to properly resolve + // the include files paths + auto config_file_path = + std::filesystem::absolute(std::filesystem::path{config_file}); + doc.force_insert( + "__parent_path", config_file_path.parent_path().string()); + + // If the current directory is also a git repository, + // load some config values, which can be included in the + // generated diagrams + if (util::is_git_repository() && !doc["git"]) { + YAML::Node git_config{YAML::NodeType::Map}; + git_config["branch"] = util::get_git_branch(); + git_config["revision"] = util::get_git_revision(); + git_config["commit"] = util::get_git_commit(); + git_config["toplevel"] = util::get_git_toplevel_dir(); + + doc["git"] = git_config; + } + + return doc.as(); + } + catch (YAML::BadFile &e) { + throw std::runtime_error(fmt::format( + "Could not open config file {}: {}", config_file, e.what())); + } + catch (YAML::Exception &e) { + throw std::runtime_error(fmt::format( + "Cannot parse YAML file {}: {}", config_file, e.what())); + } +} +} // namespace clanguml::config \ No newline at end of file diff --git a/src/config/yaml_emitters.cc b/src/config/yaml_emitters.cc new file mode 100644 index 00000000..00b2c5fa --- /dev/null +++ b/src/config/yaml_emitters.cc @@ -0,0 +1,263 @@ +/** + * src/config/yaml_emitters.cc + * + * 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. + */ + +#include "config.h" + +namespace clanguml::common::model { +YAML::Emitter &operator<<(YAML::Emitter &out, const namespace_ &n) +{ + out << n.to_string(); + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const relationship_t &r) +{ + out << to_string(r); + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const access_t &a) +{ + out << to_string(a); + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const diagram_t &d) +{ + out << to_string(d); + return out; +} +} // namespace clanguml::common::model + +namespace clanguml::config { +YAML::Emitter &operator<<(YAML::Emitter &out, const filter &f) +{ + out << YAML::BeginMap; + if (!f.namespaces.empty()) + out << YAML::Key << "namespaces" << YAML::Value << f.namespaces; + if (!f.access.empty()) + out << YAML::Key << "access" << YAML::Value << f.access; + if (!f.context.empty()) + out << YAML::Key << "context" << YAML::Value << f.context; + if (!f.dependants.empty()) + out << YAML::Key << "dependants" << YAML::Value << f.dependants; + if (!f.dependencies.empty()) + out << YAML::Key << "dependencies" << YAML::Value << f.dependencies; + if (!f.elements.empty()) + out << YAML::Key << "elements" << YAML::Value << f.elements; + if (!f.paths.empty()) + out << YAML::Key << "paths" << YAML::Value << f.paths; + if (!f.relationships.empty()) + out << YAML::Key << "relationships" << YAML::Value << f.relationships; + if (!f.specializations.empty()) + out << YAML::Key << "specializations" << YAML::Value + << f.specializations; + if (!f.subclasses.empty()) + out << YAML::Key << "subclasses" << YAML::Value << f.subclasses; + + out << YAML::EndMap; + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const plantuml &p) +{ + if (p.before.empty() && p.after.empty()) + return out; + + out << YAML::BeginMap; + if (!p.before.empty()) + out << YAML::Key << "before" << YAML::Value << p.before; + if (!p.after.empty()) + out << YAML::Key << "after" << YAML::Value << p.after; + out << YAML::EndMap; + + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const method_arguments &ma) +{ + out << to_string(ma); + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const generate_links_config &glc) +{ + out << YAML::BeginMap; + out << YAML::Key << "link" << YAML::Value << glc.link; + out << YAML::Key << "tooltip" << YAML::Value << glc.tooltip; + out << YAML::EndMap; + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const git_config &gc) +{ + out << YAML::BeginMap; + out << YAML::Key << "branch" << YAML::Value << gc.branch; + out << YAML::Key << "revision" << YAML::Value << gc.revision; + out << YAML::Key << "commit" << YAML::Value << gc.commit; + out << YAML::Key << "toplevel" << YAML::Value << gc.toplevel; + out << YAML::EndMap; + + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const relationship_hint_t &rh) +{ + out << YAML::BeginMap; + out << YAML::Key << "default" << YAML::Value << rh.default_hint; + for (const auto &[k, v] : rh.argument_hints) + out << YAML::Key << k << YAML::Value << v; + out << YAML::EndMap; + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const hint_t &h) +{ + out << to_string(h); + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const comment_parser_t &cp) +{ + out << to_string(cp); + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const layout_hint &c) +{ + out << YAML::BeginMap; + out << YAML::Key << c.hint << YAML::Value << c.entity; + out << YAML::EndMap; + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const source_location &sc) +{ + out << YAML::BeginMap; + out << YAML::Key << "location" << YAML::Value << sc.location; + out << YAML::Key << "location_type" << YAML::Value + << to_string(sc.location_type); + out << YAML::EndMap; + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const config &c) +{ + out << YAML::BeginMap; + + out << c.compilation_database_dir; + out << c.output_directory; + + out << dynamic_cast(c); + + out << YAML::Key << "diagrams"; + out << YAML::BeginMap; + + for (const auto &[k, v] : c.diagrams) { + out << YAML::Key << k; + if (v->type() == common::model::diagram_t::kClass) { + out << YAML::Value << dynamic_cast(*v); + } + else if (v->type() == common::model::diagram_t::kSequence) { + out << YAML::Value << dynamic_cast(*v); + } + else if (v->type() == common::model::diagram_t::kInclude) { + out << YAML::Value << dynamic_cast(*v); + } + else if (v->type() == common::model::diagram_t::kPackage) { + out << YAML::Value << dynamic_cast(*v); + } + } + + out << YAML::EndMap; + out << YAML::EndMap; + return out; +} + +YAML::Emitter &operator<<( + YAML::Emitter &out, const inheritable_diagram_options &c) +{ + out << c.glob; + out << c.using_namespace; + out << c.include_relations_also_as_members; + out << c.include; + out << c.exclude; + out << c.puml; + out << c.generate_method_arguments; + out << c.generate_packages; + out << c.generate_links; + out << c.git; + out << c.base_directory; + out << c.relative_to; + out << c.generate_system_headers; + if (c.relationship_hints) { + out << YAML::Key << "relationship_hints" << YAML::Value + << c.relationship_hints(); + } + if (c.type_aliases) { + out << YAML::Key << "type_aliases" << YAML::Value << c.type_aliases(); + } + out << c.comment_parser; + out << c.combine_free_functions_into_file_participants; + out << c.participants_order; + out << c.debug_mode; + + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const class_diagram &c) +{ + out << YAML::BeginMap; + out << YAML::Key << "type" << YAML::Value << c.type(); + out << c.layout; + out << dynamic_cast(c); + out << YAML::EndMap; + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const sequence_diagram &c) +{ + out << YAML::BeginMap; + out << YAML::Key << "type" << YAML::Value << c.type(); + out << c.start_from; + out << dynamic_cast(c); + out << YAML::EndMap; + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const include_diagram &c) +{ + out << YAML::BeginMap; + out << YAML::Key << "type" << YAML::Value << c.type(); + out << c.layout; + out << dynamic_cast(c); + out << YAML::EndMap; + return out; +} + +YAML::Emitter &operator<<(YAML::Emitter &out, const package_diagram &c) +{ + out << YAML::BeginMap; + out << YAML::Key << "type" << YAML::Value << c.type(); + out << c.layout; + out << dynamic_cast(c); + out << YAML::EndMap; + return out; +} + +} // namespace clanguml::config \ No newline at end of file diff --git a/src/main.cc b/src/main.cc index eca1773d..a6fdbbdf 100644 --- a/src/main.cc +++ b/src/main.cc @@ -61,6 +61,13 @@ void print_version(); */ void print_diagrams_list(const clanguml::config::config &cfg); +/** + * Print effective config after loading and setting default values. + * + * @param cfg Configuration instance loaded from configuration file + */ +void print_config(const clanguml::config::config &cfg); + /** * Generate sample configuration file and exit. * @@ -164,6 +171,7 @@ int main(int argc, const char *argv[]) std::optional add_sequence_diagram; std::optional add_package_diagram; std::optional add_include_diagram; + bool dump_config{false}; app.add_option( "-c,--config", config_path, "Location of configuration file"); @@ -190,6 +198,8 @@ int main(int argc, const char *argv[]) "Add package diagram config"); app.add_option("--add-include-diagram", add_include_diagram, "Add include diagram config"); + app.add_flag( + "--dump-config", dump_config, "Print effective config to stdout"); CLI11_PARSE(app, argc, argv); @@ -243,6 +253,11 @@ int main(int argc, const char *argv[]) return 0; } + if (dump_config) { + print_config(config); + return 0; + } + LOG_INFO("Loaded clang-uml config from {}", config_path); if (!compilation_database_dir.empty()) { @@ -620,4 +635,15 @@ int add_config_diagram(clanguml::common::model::diagram_t type, ofs.close(); return 0; +} + +void print_config(const clanguml::config::config &cfg) +{ + YAML::Emitter out; + out.SetIndent(2); + + out << cfg; + out << YAML::Newline; + + std::cout << out.c_str(); } \ No newline at end of file diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc index 19e7955b..fe332656 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc @@ -21,7 +21,7 @@ namespace clanguml::sequence_diagram::generators::plantuml { using clanguml::common::model::message_t; -using clanguml::config::source_location; +using clanguml::config::location_t; using clanguml::sequence_diagram::model::activity; using clanguml::sequence_diagram::model::message; using namespace clanguml::util; @@ -383,7 +383,7 @@ void generator::generate(std::ostream &ostr) const } for (const auto &sf : m_config.start_from()) { - if (sf.location_type == source_location::location_t::function) { + if (sf.location_type == location_t::function) { common::model::diagram_element::id_t start_from{0}; for (const auto &[k, v] : m_model.sequences()) { const auto &caller = *m_model.participants().at(v.from()); diff --git a/tests/test_config.cc b/tests/test_config.cc index 3b07867a..fbe1c82d 100644 --- a/tests/test_config.cc +++ b/tests/test_config.cc @@ -187,4 +187,30 @@ TEST_CASE("Test config layout", "[unit-test]") check_layout(static_cast( *cfg.diagrams["package_main"]), clanguml::common::model::diagram_t::kPackage); +} + +TEST_CASE("Test config emitters", "[unit-test]") +{ + auto cfg = clanguml::config::load("./test_config_data/complete.yml"); + + YAML::Emitter out; + out.SetIndent(2); + + out << cfg; + out << YAML::Newline; + + // Write the emitted YAML to a temp file + auto tmp_file = std::filesystem::temp_directory_path() / + fmt::format("clang-uml-{:16}", rand()); + + { + std::ofstream stream(tmp_file.string().c_str(), std::ios::binary); + stream << out.c_str(); + } + + auto cfg_emitted = clanguml::config::load(tmp_file.string()); + + REQUIRE(cfg.diagrams.size() == cfg_emitted.diagrams.size()); + + std::filesystem::remove(tmp_file); } \ No newline at end of file diff --git a/tests/test_config_data/complete.yml b/tests/test_config_data/complete.yml new file mode 100644 index 00000000..1ede29bc --- /dev/null +++ b/tests/test_config_data/complete.yml @@ -0,0 +1,61 @@ +# Directory containing the compile_commands.json file +compilation_database_dir: debug +# The directory where *.puml files will be generated +output_directory: docs/diagrams +# Set this as default for all diagrams +generate_method_arguments: none +# Enable generation of hyperlinks to diagram elements +generate_links: + # Link pattern + link: "https://github.com/bkryza/clang-uml/blob/{{ git.commit }}/{{ element.source.path }}#L{{ element.source.line }}" + # Tooltip pattern + tooltip: "{{ element.name }}" +# The map of diagrams - keys are also diagram file names +diagrams: + config_class: + type: class + # Do not include rendered relations in the class box + include_relations_also_as_members: false + # Limiting the number of files to include can significantly + # improve the generation time + glob: + - src/common/model/*.h + - src/common/model/*.cc + - src/class_diagram/model/*.h + - src/class_diagram/model/*.cc + include: + # Only include entities from the following namespaces + namespaces: + - clanguml::common::model + - clanguml::class_diagram::model + # Only include elements in direct relationship with ClassA + context: + - ClassA + exclude: + # Do not include private members and methods in the diagram + access: + - private + layout: + # Add layout hints for PlantUML + ClassA: + - up: ClassB + - left: ClassC + # Specify customized relationship hints for types which are + # arguments in template instantiations + relationship_hints: + # All tuple arguments should be treated as aggregation + std::tuple: aggregation + # All some_template arguments should be treated as associations + # except for arguments with indexes 2 and 10 + ns1::n2::some_template: + default: association + 2: composition + 10: aggregation + # Entities from this namespace will be shortened + # (can only contain one element at the moment) + using_namespace: + - clanguml::class_diagram::model + plantuml: + # Add this line to the beginning of the resulting puml file + before: + - 'title clang-uml class diagram model' \ No newline at end of file