Refactored command line handling

This commit is contained in:
Bartek Kryza
2023-03-11 18:59:53 +01:00
parent 41537c5401
commit f1c125bf32
17 changed files with 1066 additions and 609 deletions

View File

@@ -1,8 +1,8 @@
compilation_database_dir: debug
output_directory: docs/diagrams
generate_links:
link: 'https://github.com/bkryza/clang-uml/blob/{{ git.commit }}/{{ element.source.path }}#L{{ element.source.line }}'
tooltip: '{% if existsIn(element, "comment") and existsIn(element.comment, "brief") %}{{ abbrv(trim(replace(element.comment.brief.0, "\n+", " ")), 256) }}{% else %}{{ element.name }}{% endif %}'
link: https://github.com/bkryza/clang-uml/blob/{{ git.commit }}/{{ element.source.path }}#L{{ element.source.line }}
tooltip: "{% if existsIn(element, \"comment\") and existsIn(element.comment, \"brief\") %}{{ abbrv(trim(replace(element.comment.brief.0, \"\\n+\", \" \")), 256) }}{% else %}{{ element.name }}{% endif %}"
diagrams:
main_package:
include!: uml/main_package_diagram.yml
@@ -26,3 +26,15 @@ diagrams:
include!: uml/package_model_class_diagram.yml
include_graph:
include!: uml/include_diagram.yml
include3_diagram_parents_hierarchy:
type: class
include:
parents: [clanguml::config::include_diagram]
namespaces: [clanguml]
relationships:
- inheritance
exclude:
access: [public, protected, private]
plantuml:
before:
- left to right direction

561
src/cli/cli_handler.cc Normal file

File diff suppressed because it is too large Load Diff

153
src/cli/cli_handler.h Normal file
View File

@@ -0,0 +1,153 @@
/**
* src/options/cli_options.h
*
* Copyright (c) 2021-2023 Bartek Kryza <bkryza@gmail.com>
*
* 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.
*/
#pragma once
#include "common/model/enums.h"
#include "config/config.h"
#include <cli11/CLI11.hpp>
#include <optional>
namespace clanguml::cli {
enum class cli_flow_t { kExit, kError, kContinue };
class cli_handler {
public:
cli_handler(std::ostream &ostr = std::cout,
std::shared_ptr<spdlog::logger> logger = spdlog::stdout_color_mt(
"clanguml-logger", spdlog::color_mode::automatic));
/**
* Main CLI handling method.
*
* @param argc
* @param argv
* @return
*/
cli_flow_t handle_options(int argc, const char *argv[]);
/**
* Print the program version and basic information
*/
cli_flow_t print_version();
/**
* Print list of diagrams available in the configuration file
*/
cli_flow_t print_diagrams_list();
/**
* Print list of available diagram templates, including their names
* and types.
*/
cli_flow_t print_diagram_templates();
/**
* Print definition of a specific diagram template.
*
* @param template_name
* @return
*/
cli_flow_t print_diagram_template(const std::string &template_name);
/**
* Print effective config after loading and setting default values.
*/
cli_flow_t print_config();
/**
* Generate sample configuration file and exit.
*
* @return 0 on success or error code
*/
cli_flow_t create_config_file();
/**
* Add example diagram of given type to the config file.
*
* @param type Type of the sample diagram to add
* @param config_file_path Path to the config file
* @param name Name of the new diagram
* @return 0 on success or error code
*/
cli_flow_t add_config_diagram(clanguml::common::model::diagram_t type,
const std::string &config_file_path, const std::string &name);
/**
* Add diagram based on template
*
* @param config_file_path
* @param template_name
* @param template_variables
* @return
*/
cli_flow_t add_config_diagram_from_template(
const std::string &config_file_path, const std::string &template_name,
const std::vector<std::string> &template_variables);
/**
* Check if diagram output directory exists, if not create it
*
* @param dir Path to the output directory
* @return True if directory exists or has been created
*/
bool ensure_output_directory_exists(const std::string &dir);
std::string config_path{".clang-uml"};
std::optional<std::string> compilation_database_dir{};
std::vector<std::string> diagram_names{};
std::optional<std::string> output_directory{};
std::string effective_output_directory{};
unsigned int thread_count{};
bool show_version{false};
int verbose{};
bool list_diagrams{false};
bool quiet{false};
bool initialize{false};
std::optional<std::string> add_class_diagram;
std::optional<std::string> add_sequence_diagram;
std::optional<std::string> add_package_diagram;
std::optional<std::string> add_include_diagram;
std::optional<std::string> add_diagram_from_template;
bool dump_config{false};
std::optional<bool> paths_relative_to_pwd{};
std::vector<std::string> template_variables{};
bool list_templates{false};
std::optional<std::string> show_template;
clanguml::config::config config;
private:
cli_flow_t parse(int argc, const char *argv[]);
cli_flow_t handle_pre_config_options();
cli_flow_t load_config();
cli_flow_t handle_post_config_options();
void setup_logging();
std::ostream &ostr_;
std::shared_ptr<spdlog::logger> logger_;
CLI::App app{"Clang-based UML diagram generator for C++"};
};
} // namespace clanguml::options

View File

@@ -17,6 +17,7 @@
*/
#include "config.h"
#include "diagram_templates.h"
#include "glob/glob.hpp"
#include <filesystem>

View File

@@ -51,6 +51,7 @@ struct plantuml {
};
struct diagram_template {
std::string description;
common::model::diagram_t type;
std::string jinja_template;
};
@@ -227,6 +228,8 @@ struct config : public inheritable_diagram_options {
"diagram_templates"};
std::map<std::string, std::shared_ptr<diagram>> diagrams;
void initialize_diagram_templates();
};
//
@@ -287,6 +290,8 @@ YAML::Emitter &operator<<(YAML::Emitter &out, const option<T> &o)
config load(const std::string &config_file,
std::optional<bool> paths_relative_to_pwd = {});
config load_plain(const std::string &config_file);
} // namespace config
namespace common::model {

View File

@@ -0,0 +1,93 @@
/**
* src/config/diagram_templates.cc
*
* Copyright (c) 2021-2023 Bartek Kryza <bkryza@gmail.com>
*
* 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 "diagram_templates.h"
namespace clanguml {
namespace config {
const std::string &get_predefined_diagram_templates()
{
static const std::string predefined_diagram_templates =
R"(# Predefined diagram templates
parents_hierarchy_tmpl:
description: Generate class parents inheritance diagram
type: class
template: |
{{ diagram_name }}:
type: class
{% if exists("glob") %}
glob: [{{ glob }}]
{% endif %}
{% if exists("using_namespace") %}
using_namespace: {{ using_namespace }}
{% endif %}
include:
parents: [{{ class_name }}]
namespaces: [{{ namespace_names }}]
relationships:
- inheritance
exclude:
access: [public, protected, private]
plantuml:
before:
- left to right direction
subclass_hierarchy_tmpl:
description: Generate class children inheritance diagram
type: class
template: |
{{ diagram_name }}:
type: class
{% if exists("glob") %}
glob: [{{ glob }}]
{% endif %}
{% if exists("using_namespace") %}
using_namespace: {{ using_namespace }}
{% endif %}
include:
parents: [{{ class_name }}]
namespaces: [{{ namespace_name }}]
relationships:
- inheritance
exclude:
access: [public, protected, private]
plantuml:
before:
- left to right direction
class_context_tmpl:
description: Generate class context diagram
type: class
template: |
"{{ diagram_name }}":
type: class
{% if exists("glob") %}
glob: [{{ glob }}]
{% endif %}
{% if exists("using_namespace") %}
using_namespace: {{ using_namespace }}
{% endif %}
include:
context: [{{ class_name }}]
namespaces: [{{ namespace_name }}]
)";
return predefined_diagram_templates;
}
} // namespace config
} // namespace clanguml

View File

@@ -0,0 +1,28 @@
/**
* src/config/diagram_templates.h
*
* Copyright (c) 2021-2023 Bartek Kryza <bkryza@gmail.com>
*
* 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.
*/
#pragma once
#include <string>
namespace clanguml {
namespace config {
const std::string &get_predefined_diagram_templates();
} // namespace config
} // namespace clanguml

View File

@@ -17,6 +17,7 @@
*/
#include "config.h"
#include "diagram_templates.h"
namespace YAML {
using clanguml::common::model::access_t;
@@ -112,9 +113,24 @@ void get_option<std::map<std::string, clanguml::config::diagram_template>>(
return;
}
option.set(
node[option.name]
.as<std::map<std::string, clanguml::config::diagram_template>>());
if (node[option.name].IsMap() && node[option.name]["include!"]) {
auto parent_path = node["__parent_path"].as<std::string>();
// Load templates from file
auto include_path = std::filesystem::path{parent_path};
include_path /= node[option.name]["include!"].as<std::string>();
YAML::Node included_node = YAML::LoadFile(include_path.string());
// diagram_config = parse_diagram_config(included_node);
option.set(
included_node.as<
std::map<std::string, clanguml::config::diagram_template>>());
}
else
option.set(node[option.name]
.as<std::map<std::string,
clanguml::config::diagram_template>>());
}
std::shared_ptr<clanguml::config::diagram> parse_diagram_config(const Node &d)
@@ -548,28 +564,12 @@ template <> struct convert<relationship_hint_t> {
template <> struct convert<diagram_template> {
static bool decode(const Node &node, diagram_template &rhs)
{
assert(node.Type() == NodeType::Map || node.Type() == NodeType::Scalar);
assert(node.Type() == NodeType::Map);
if (node.Type() == NodeType::Scalar) {
// Check that the template provided as string is at least valid YAML
const auto yaml_node = Load(node.as<std::string>());
const auto template_root_it = yaml_node.begin();
const auto diagram_name_template =
template_root_it->first.as<std::string>();
const auto diagram_type =
template_root_it->second["type"].as<std::string>();
rhs.type = clanguml::common::model::from_string(diagram_type);
rhs.jinja_template = Dump(yaml_node);
}
else {
const auto template_root_it = node.begin();
const auto diagram_name_template =
template_root_it->first.as<std::string>();
const auto diagram_type =
template_root_it->second["type"].as<std::string>();
rhs.type = clanguml::common::model::from_string(diagram_type);
rhs.jinja_template = Dump(node);
}
rhs.type = clanguml::common::model::from_string(
node["type"].as<std::string>());
rhs.jinja_template = node["template"].as<std::string>();
rhs.description = node["description"].as<std::string>();
return true;
}
@@ -611,10 +611,11 @@ template <> struct convert<config> {
auto include_path = std::filesystem::path{parent_path};
include_path /= d.second["include!"].as<std::string>();
YAML::Node node = YAML::LoadFile(include_path.string());
node.force_insert("__parent_path", parent_path);
YAML::Node included_node =
YAML::LoadFile(include_path.string());
included_node.force_insert("__parent_path", parent_path);
diagram_config = parse_diagram_config(node);
diagram_config = parse_diagram_config(included_node);
}
else {
d.second.force_insert("__parent_path", parent_path);
@@ -638,6 +639,20 @@ template <> struct convert<config> {
namespace clanguml::config {
void config::initialize_diagram_templates()
{
const auto &predefined_templates_str = get_predefined_diagram_templates();
YAML::Node predefined_templates = YAML::Load(predefined_templates_str);
if (!diagram_templates) {
diagram_templates.set({});
}
diagram_templates().merge(
predefined_templates.as<std::map<std::string, diagram_template>>());
}
namespace {
void resolve_option_path(YAML::Node &doc, const std::string &option)
{
@@ -729,6 +744,30 @@ config load(
doc["git"] = git_config;
}
auto d = doc.as<config>();
d.initialize_diagram_templates();
return d;
}
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()));
}
}
config load_plain(const std::string &config_file)
{
try {
YAML::Node doc;
std::filesystem::path config_file_path{};
doc = YAML::LoadFile(config_file);
auto d = doc.as<config>();
return d;
}

File diff suppressed because it is too large Load Diff

View File

@@ -25,27 +25,6 @@ namespace clanguml::util {
static const auto WHITESPACE = " \n\r\t\f\v";
void setup_logging(int verbose)
{
auto console =
spdlog::stdout_color_mt("console", spdlog::color_mode::automatic);
console->set_pattern("[%^%l%^] [tid %t] %v");
if (verbose == 0) {
console->set_level(spdlog::level::err);
}
else if (verbose == 1) {
console->set_level(spdlog::level::info);
}
else if (verbose == 2) {
console->set_level(spdlog::level::debug);
}
else {
console->set_level(spdlog::level::trace);
}
}
std::string get_process_output(const std::string &command)
{
constexpr size_t kBufferSize{1024};

View File

@@ -28,29 +28,29 @@
#include <vector>
#define LOG_ERROR(fmt__, ...) \
spdlog::get("console")->error( \
fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, __LINE__, \
##__VA_ARGS__)
spdlog::get("clanguml-logger") \
->error(fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, \
__LINE__, ##__VA_ARGS__)
#define LOG_WARN(fmt__, ...) \
spdlog::get("console")->warn( \
fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, __LINE__, \
##__VA_ARGS__)
spdlog::get("clanguml-logger") \
->warn(fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, \
__LINE__, ##__VA_ARGS__)
#define LOG_INFO(fmt__, ...) \
spdlog::get("console")->info( \
fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, __LINE__, \
##__VA_ARGS__)
spdlog::get("clanguml-logger") \
->info(fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, \
__LINE__, ##__VA_ARGS__)
#define LOG_DBG(fmt__, ...) \
spdlog::get("console")->debug( \
fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, __LINE__, \
##__VA_ARGS__)
spdlog::get("clanguml-logger") \
->debug(fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, \
__LINE__, ##__VA_ARGS__)
#define LOG_TRACE(fmt__, ...) \
spdlog::get("console")->trace( \
fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, __LINE__, \
##__VA_ARGS__)
spdlog::get("clanguml-logger") \
->trace(fmt::runtime(std::string("[{}:{}] ") + fmt__), FILENAME_, \
__LINE__, ##__VA_ARGS__)
namespace clanguml::util {
@@ -61,13 +61,6 @@ std::string trim(const std::string &s);
#define FILENAME_ \
(strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
/**
* @brief Setup spdlog logger.
*
* @param verbose Whether the logging should be verbose or not.
*/
void setup_logging(int verbose);
std::string get_process_output(const std::string &command);
std::string get_env(const std::string &name);

View File

@@ -38,6 +38,7 @@ set(TEST_CASES
test_cases
test_decorator_parser
test_config
test_cli_handler
test_filters
test_thread_pool_executor)

View File

@@ -17,6 +17,8 @@
*/
#include "test_cases.h"
#include "cli/cli_handler.h"
#include "common/generators/plantuml/generator.h"
#include <spdlog/spdlog.h>
@@ -345,7 +347,17 @@ int main(int argc, char *argv[])
if (returnCode != 0)
return returnCode;
clanguml::util::setup_logging(debug_log ? 3 : 1);
clanguml::cli::cli_handler clih;
std::vector<const char *> argvv = {
"clang-uml", "--config", "./test_config_data/simple.yml"};
if (debug_log)
argvv.push_back("-vvv");
else
argvv.push_back("-q");
clih.handle_options(argvv.size(), argvv.data());
return session.run();
}

74
tests/test_cli_handler.cc Normal file
View File

@@ -0,0 +1,74 @@
/**
* tests/test_cli_handler.cc
*
* Copyright (c) 2021-2023 Bartek Kryza <bkryza@gmail.com>
*
* 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.
*/
#define CATCH_CONFIG_MAIN
#include "cli/cli_handler.h"
#include "version.h"
#include "catch.h"
#include <spdlog/sinks/ostream_sink.h>
#include <spdlog/spdlog.h>
std::shared_ptr<spdlog::logger> make_sstream_logger(std::ostream &ostr)
{
auto oss_sink = std::make_shared<spdlog::sinks::ostream_sink_mt>(ostr);
return std::make_shared<spdlog::logger>(
"clanguml-logger", std::move(oss_sink));
}
TEST_CASE("Test cli handler print_version", "[unit-test]")
{
using clanguml::cli::cli_flow_t;
using clanguml::cli::cli_handler;
std::vector<const char *> argv = {"clang-uml", "--version"};
std::ostringstream ostr;
cli_handler cli{ostr, make_sstream_logger(ostr)};
auto res = cli.handle_options(argv.size(), argv.data());
REQUIRE(res == cli_flow_t::kExit);
auto output = ostr.str();
REQUIRE(output.find(fmt::format(
"clang-uml {}", clanguml::version::CLANG_UML_VERSION)) == 0);
}
TEST_CASE("Test cli handler print_config", "[unit-test]")
{
using clanguml::cli::cli_flow_t;
using clanguml::cli::cli_handler;
std::vector<const char *> argv = {"clang-uml", "--config",
"./test_config_data/simple.yml", "--dump-config"};
std::ostringstream ostr;
cli_handler cli{ostr, make_sstream_logger(ostr)};
auto res = cli.handle_options(argv.size(), argv.data());
REQUIRE(res == cli_flow_t::kExit);
auto output = ostr.str();
YAML::Node doc = YAML::Load(output);
REQUIRE(doc["diagrams"]["class_main"]);
}

View File

@@ -265,39 +265,55 @@ TEST_CASE("Test config diagram_templates", "[unit-test]")
auto cfg =
clanguml::config::load("./test_config_data/diagram_templates.yml");
REQUIRE(cfg.diagram_templates().size() == 3);
REQUIRE(cfg.diagram_templates().size() == 4);
REQUIRE(cfg.diagram_templates()["bases_hierarchy_tmpl"].type ==
REQUIRE(cfg.diagram_templates()["parents_hierarchy_tmpl"].type ==
clanguml::common::model::diagram_t::kClass);
REQUIRE(cfg.diagram_templates()["bases_hierarchy_tmpl"].jinja_template ==
R"("{{ class_name }}_parents_hierarchy":
REQUIRE(cfg.diagram_templates()["parents_hierarchy_tmpl"].jinja_template ==
"{{ diagram_name }}:"
R"(
type: class
{% if exists("glob") %}
glob: [{{ glob }}]
{% endif %}
{% if exists("using_namespace") %}
using_namespace: {{ using_namespace }}
{% endif %}
include:
parents: "{{ class_name }}"
namespaces: "{{ namespace_name }}"
parents: [{{ class_name }}]
namespaces: [{{ namespace_names }}]
relationships:
- inheritance
exclude:
access: [public, protected, private]
plantuml:
before:
- left to right direction)");
- left to right direction
)");
REQUIRE(cfg.diagram_templates()["children_hierarchy_tmpl"].type ==
clanguml::common::model::diagram_t::kClass);
REQUIRE(cfg.diagram_templates()["children_hierarchy_tmpl"].jinja_template ==
R"("{{ class_name }}_children_hierarchy":
REQUIRE(cfg.diagram_templates()["subclass_hierarchy_tmpl"].jinja_template ==
"{{ diagram_name }}:"
R"(
type: class
{% if exists("glob") %}
glob: [{{ glob }}]
{% endif %}
{% if exists("using_namespace") %}
using_namespace: {{ using_namespace }}
{% endif %}
include:
subclasses: "{{ class_name }}"
namespaces: "{{ namespace_name }}"
parents: [{{ class_name }}]
namespaces: [{{ namespace_name }}]
relationships:
- inheritance
exclude:
access: [public, protected, private]
plantuml:
before:
- left to right direction)");
- left to right direction
)");
REQUIRE(cfg.diagram_templates()["main_sequence_tmpl"].type ==
clanguml::common::model::diagram_t::kSequence);

View File

@@ -2,33 +2,10 @@ compilation_database_dir: debug
output_directory: output
diagram_templates:
bases_hierarchy_tmpl:
'{{ class_name }}_parents_hierarchy':
type: class
include:
parents: '{{ class_name }}'
namespaces: '{{ namespace_name }}'
relationships:
- inheritance
exclude:
access: [public, protected, private]
plantuml:
before:
- left to right direction
children_hierarchy_tmpl: |
'{{ class_name }}_children_hierarchy':
type: class
include:
subclasses: '{{ class_name }}'
namespaces: '{{ namespace_name }}'
relationships:
- inheritance
exclude:
access: [public, protected, private]
plantuml:
before:
- left to right direction
main_sequence_tmpl: |
main_sequence_tmpl:
description: Sequence diagram of the main() function
type: sequence
template: |
main_sequence_diagram:
type: sequence
glob: [ {{ glob }} ]

View File

@@ -1,6 +1,5 @@
compilation_database_dir: debug
output_directory: output
diagrams:
class_main:
type: class