Recfactored and simplified more code

- Added function to convert color string to ansi color string
- Added `trim` and `join` utility functions
- Added initial plugin test case
- Implemented `config get` command to see config properties
- Improved logging functionality and removed duplicate logging functions
- Removed unused functions
- Fixed more styling issues
- Fixed some CLI commands not working correctly
- Fixed CLI documentation format
- Fixed some error handling issues
This commit is contained in:
David Allen 2023-06-18 10:47:05 -06:00
parent e48c54aa40
commit 02a4e879a8
21 changed files with 541 additions and 384 deletions

View file

@ -50,7 +50,7 @@ The project uses the CMake or Meson build system and has been tested with GCC an
* doctest (optional; for tests, but still WIP)
* cxxopts (header only)
* clipp (header only)
* SQLite 3

View file

@ -1,3 +1,7 @@
#pragma once
#include "colors.hpp"
#include <string>
#if GDPM_ENABLE_COLORS == 1
#define GDPM_COLOR_BLACK "\033[0;30m"
@ -16,24 +20,26 @@
#define GDPM_COLOR_LIGHT_PURPLE "\033[0;35m"
#define GDPM_COLOR_YELLOW "\033[0;33m"
#define GDPM_COLOR_WHITE "\033[0;37m"
#define GDPM_COLOR_RESET GDPM_COLOR_WHITE
#else
#define GDPM_COLOR_BLACK
#define GDPM_COLOR_BLUE
#define GDPM_COLOR_GREEN
#define GDPM_COLOR_CYAN
#define GDPM_COLOR_RED
#define GDPM_COLOR_PURPLE
#define GDPM_COLOR_BROWN
#define GDPM_COLOR_GRAY
#define GDPM_COLOR_DARK_GRAY
#define GDPM_COLOR_LIGHT_BLUE
#define GDPM_COLOR_LIGHT_GREEN
#define GDPM_COLOR_LIGHT_CYAN
#define GDPM_COLOR_LIGHT_RED
#define GDPM_COLOR_LIGHT_PURPLE
#define GDPM_COLOR_YELLOW
#define GDPM_COLOR_WHITE
#define GDPM_COLOR_BLACK ""
#define GDPM_COLOR_BLUE ""
#define GDPM_COLOR_GREEN ""
#define GDPM_COLOR_CYAN ""
#define GDPM_COLOR_RED ""
#define GDPM_COLOR_PURPLE ""
#define GDPM_COLOR_BROWN ""
#define GDPM_COLOR_GRAY ""
#define GDPM_COLOR_DARK_GRAY ""
#define GDPM_COLOR_LIGHT_BLUE ""
#define GDPM_COLOR_LIGHT_GREEN ""
#define GDPM_COLOR_LIGHT_CYAN ""
#define GDPM_COLOR_LIGHT_RED ""
#define GDPM_COLOR_LIGHT_PURPLE ""
#define GDPM_COLOR_YELLOW ""
#define GDPM_COLOR_WHITE ""
#define GDPM_COLOR_RESET GDPM_COLOR_WHITE
#endif
@ -42,4 +48,24 @@
#define GDPM_COLOR_LOG_INFO GDPM_COLOR_LOG_RESET
#define GDPM_COLOR_LOG_ERROR GDPM_COLOR_RED
#define GDPM_COLOR_LOG_DEBUG GDPM_COLOR_YELLOW
#define GDPM_COLOR_LOG_WARNING GDPM_COLOR_YELLOW
#define GDPM_COLOR_LOG_WARNING GDPM_COLOR_YELLOW
namespace gdpm::color{
inline std::string from_string(const std::string& color_name){
if (color_name == "red"){ return GDPM_COLOR_RED; }
else if (color_name == "yellow"){ return GDPM_COLOR_YELLOW; }
else if (color_name == "green"){ return GDPM_COLOR_GREEN; }
else if (color_name == "blue"){ return GDPM_COLOR_BLUE; }
else if (color_name == "brown"){ return GDPM_COLOR_BROWN; }
else if (color_name == "gray"){ return GDPM_COLOR_GRAY; }
else if (color_name == "black"){ return GDPM_COLOR_BLACK; }
else if (color_name == "purple"){ return GDPM_COLOR_PURPLE; }
else if (color_name == "gray"){ return GDPM_COLOR_DARK_GRAY; }
else if (color_name == "light-blue"){ return GDPM_COLOR_LIGHT_BLUE; }
else if (color_name == "light-green"){ return GDPM_COLOR_LIGHT_GREEN; }
else if (color_name == "light-cyan"){ return GDPM_COLOR_LIGHT_CYAN; }
else if (color_name == "light-red"){ return GDPM_COLOR_LIGHT_RED; }
else if (color_name == "light-purple"){ return GDPM_COLOR_LIGHT_PURPLE; }
return "";
}
}

View file

@ -43,7 +43,8 @@ namespace gdpm::config{
error handle_config(config::context& config, const args_t& args, const var_opts& opts);
context make_context(const string& username = GDPM_CONFIG_USERNAME, const string& password = GDPM_CONFIG_PASSWORD, const string& path = GDPM_CONFIG_PATH, const string& token = GDPM_CONFIG_TOKEN, const string& godot_version = GDPM_CONFIG_GODOT_VERSION, const string& packages_dir = GDPM_CONFIG_LOCAL_PACKAGES_DIR, const string& tmp_dir = GDPM_CONFIG_LOCAL_TMP_DIR, const string_map& remote_sources = {GDPM_CONFIG_REMOTE_SOURCES}, size_t threads = GDPM_CONFIG_THREADS, size_t timeout = 0, bool enable_sync = GDPM_CONFIG_ENABLE_SYNC, bool enable_file_logging = GDPM_CONFIG_ENABLE_FILE_LOGGING, int verbose = GDPM_CONFIG_VERBOSE);
error validate(const rapidjson::Document& doc);
void print(const context& config);
void print_json(const context& config);
void print_properties(const context& config, const string_list& properties);
extern context config;
}

View file

@ -15,6 +15,7 @@ namespace gdpm::constants{
const std::string UserAgent("libcurl-agent/1.0 via GDPM (https://github.com/davidallendj/gdpm)");
const std::string AssetRepo("https://godotengine.org/asset-library/api/asset");
const std::string HostUrl("https://godotengine.org/asset-library/api");
constexpr std::string WHITESPACE = " \n\r\t\f\v";
}
/* Define default macros to set when building with -DGPM_* */

View file

@ -29,8 +29,10 @@ namespace gdpm::constants::error{
INVALID_ARG_COUNT,
INVALID_CONFIG,
INVALID_KEY,
HTTP_RESPONSE_ERROR,
STD_ERROR
HTTP_RESPONSE_ERR,
SQLITE_ERR,
JSON_ERR,
STD_ERR
};
const string_list messages {

View file

@ -1,10 +1,12 @@
#pragma once
#include "clipp.h"
#include "utils.hpp"
#include "colors.hpp"
#include "types.hpp"
#include <format>
#include <bitset>
// #include <fmt/core.h>
#if __cplusplus > 201703L
@ -21,22 +23,51 @@ TODO: Write log information to file
namespace gdpm::log
{
enum level{
NONE = 0,
INFO,
WARNING,
DEBUG,
ERROR
enum level_e : int{
NONE = 0,
INFO = 1,
WARNING = 2,
DEBUG = 3,
ERROR = 4
};
struct context {
int level;
string prefix;
string path;
bool print_to_stdout;
bool print_to_stderr;
enum flag_opt {
PRINT_STDOUT = 0b00000001,
PRINT_STDERR = 0b00000010
};
static int level = INFO;
static string prefix = "";
static string suffix = "";
static string path = "";
static std::bitset<8> flags = PRINT_STDOUT | PRINT_STDERR;
static bool print_to_stdout;
static bool print_to_stderr;
inline constexpr level_e to_level(int l){
return static_cast<level_e>(l);
}
inline constexpr int to_int(const level_e& l){
return static_cast<int>(l);
}
inline constexpr void set_flag(uint8_t flag, bool value){
(value) ? flags.set(flag) : flags.reset(flag);
}
inline constexpr bool get_flag(uint8_t flag){
return flags.test(flag);
}
inline constexpr void set_prefix_if(const std::string& v, bool predicate = !prefix.empty()){
prefix = (predicate) ? v : prefix;
}
inline constexpr void set_suffix_if(const std::string& v, bool predicate = !suffix.empty()){
suffix = (predicate) ? v : suffix;
}
static void vlog(fmt::string_view format, fmt::format_args args){
fmt::vprint(format, args);
}
@ -47,30 +78,13 @@ namespace gdpm::log
template <typename S, typename...Args>
static constexpr void info(const S& format, Args&&...args){
if(log::level < to_int(log::INFO))
return;
#if GDPM_LOG_LEVEL > NONE
set_prefix_if(fmt::format("[INFO {}] ", utils::timestamp()));
set_suffix_if("\n");
vlog(
fmt::format(GDPM_COLOR_LOG_INFO "[INFO {}] {}\n" GDPM_COLOR_LOG_RESET, utils::timestamp(), format),
// fmt::make_format_args<Args...>(args...)
fmt::make_format_args(args...)
);
#endif
}
template <typename S, typename...Args>
static constexpr void info(const S& prefix, const S& format, Args&&...args){
#if GDPM_LOG_LEVEL > INFO
vlog(
fmt::format(GDPM_COLOR_LOG_INFO + prefix + GDPM_COLOR_LOG_RESET, format),
fmt::make_format_args(args...)
);
#endif
}
template <typename S, typename...Args>
static constexpr void info(const context& context, const S& format, Args&&...args){
#if GDPM_LOG_LEVEL > INFO
vlog(
fmt::format(GDPM_COLOR_LOG_INFO + context.prefix + GDPM_COLOR_LOG_RESET, format),
fmt::format(GDPM_COLOR_LOG_INFO "{}{}{}" GDPM_COLOR_LOG_RESET, prefix, format, suffix),
fmt::make_format_args(args...)
);
#endif
@ -78,10 +92,13 @@ namespace gdpm::log
template <typename S, typename...Args>
static constexpr void info_n(const S& format, Args&&...args){
if(log::level < to_int(log::INFO))
return;
#if GDPM_LOG_LEVEL > INFO
set_prefix_if(fmt::format("[INFO {}] ", utils::timestamp()));
set_suffix_if("");
vlog(
fmt::format(GDPM_COLOR_LOG_INFO "[INFO {}] {}" GDPM_COLOR_LOG_RESET, utils::timestamp(), format),
// fmt::make_format_args<Args...>(args...)
fmt::format(GDPM_COLOR_LOG_INFO "{}{}{}" GDPM_COLOR_LOG_RESET, prefix, format, suffix),
fmt::make_format_args(args...)
);
#endif
@ -89,30 +106,13 @@ namespace gdpm::log
template <typename S, typename...Args>
static constexpr void error(const S& format, Args&&...args){
if(log::level < to_int(log::ERROR))
return;
#if GDPM_LOG_LEVEL > ERROR
set_prefix_if(std::format("[ERROR {}] ", utils::timestamp()));
set_suffix_if("\n");
vlog(
fmt::format(GDPM_COLOR_LOG_ERROR "[ERROR {}] {}\n" GDPM_COLOR_LOG_RESET, utils::timestamp(), format),
// fmt::make_format_args<Args...>(args...)
fmt::make_format_args(args...)
);
#endif
}
template <typename S, typename...Args>
static constexpr void error(const S& prefix, const S& format, Args&&...args){
#if GDPM_LOG_LEVEL > ERROR
vlog(
fmt::format(GDPM_COLOR_LOG_ERROR + prefix + GDPM_COLOR_LOG_RESET, format),
fmt::make_format_args(args...)
);
#endif
}
template <typename S, typename...Args>
static constexpr void error(const context& context, const S& format, Args&&...args){
#if GDPM_LOG_LEVEL > ERROR
vlog(
fmt::format(GDPM_COLOR_LOG_ERROR + context.prefix + GDPM_COLOR_LOG_RESET, format),
fmt::format(GDPM_COLOR_LOG_ERROR "{}{}{}" GDPM_COLOR_LOG_RESET, prefix, format, suffix),
fmt::make_format_args(args...)
);
#endif
@ -120,30 +120,13 @@ namespace gdpm::log
template <typename S, typename...Args>
static constexpr void debug(const S& format, Args&&...args){
if(log::level < to_int(log::DEBUG))
return;
#if GDPM_LOG_LEVEL > DEBUG
set_prefix_if(std::format("[DEBUG {}] ", utils::timestamp()));
set_suffix_if("\n");
vlog(
fmt::format(GDPM_COLOR_LOG_DEBUG "[DEBUG {}] {}\n" GDPM_COLOR_LOG_RESET, utils::timestamp(), format),
// fmt::make_format_args<Args...>(args...)
fmt::make_format_args(args...)
);
#endif
}
template <typename S, typename...Args>
static constexpr void debug(const S& prefix, const S& format, Args&&...args){
#if GDPM_LOG_LEVEL > DEBUG
vlog(
fmt::format(GDPM_COLOR_LOG_DEBUG + prefix + GDPM_COLOR_LOG_RESET, format),
fmt::make_format_args(args...)
);
#endif
}
template <typename S, typename...Args>
static constexpr void debug(const context& context, const S& format, Args&&...args){
#if GDPM_LOG_LEVEL > DEBUG
vlog(
fmt::format(GDPM_COLOR_LOG_DEBUG + context.prefix + GDPM_COLOR_LOG_RESET, format),
fmt::format(GDPM_COLOR_LOG_DEBUG "{}{}{}" GDPM_COLOR_LOG_RESET, prefix, format, suffix),
fmt::make_format_args(args...)
);
#endif
@ -153,7 +136,6 @@ namespace gdpm::log
static constexpr void print(const S& format, Args&&...args){
vlog(
fmt::format("{}", format),
// fmt::make_format_args<Args...>(args...)
fmt::make_format_args(args...)
);
}
@ -162,7 +144,6 @@ namespace gdpm::log
static constexpr void println(const S& format, Args&&...args){
vlog(
fmt::format("{}\n", format),
// fmt::make_format_args<Args...>(args...)
fmt::make_format_args(args...)
);
}

View file

@ -8,6 +8,7 @@
#include "rest_api.hpp"
#include <cstdio>
#include <filesystem>
#include <functional>
#include <string>
#include <vector>
#include <rapidjson/document.h>
@ -48,11 +49,11 @@ namespace gdpm::package {
};
struct params {
args_t sub_commands;
args_t args;
var_opts opts;
string_list paths;
string_list input_files;
string remote_source = "origin";
string remote_source = "origin";
install_method_e install_method = GLOBAL_LINK_LOCAL;
};
@ -61,6 +62,7 @@ namespace gdpm::package {
using id_list = std::vector<size_t>;
using path = std::string;
using path_list = std::vector<path>;
using path_refs = std::vector<std::reference_wrapper<const path>>;
/*!
@brief Install a Godot package from the Asset Library in the current project.

View file

@ -16,7 +16,6 @@
#include <curl/curl.h>
namespace gdpm::package_manager {
extern remote::repository_map remote_sources;
extern CURL *curl;
extern CURLcode res;
extern config::context config;
@ -32,10 +31,13 @@ namespace gdpm::package_manager {
link,
clone,
clean,
config,
config_get,
config_set,
fetch,
sync,
remote,
remote_add,
remote_remove,
remote_list,
ui,
help,
none

View file

@ -1,6 +1,7 @@
#pragma once
#include "types.hpp"
#include <string>
#include <filesystem>
namespace gdpm::plugin{
struct info{
@ -8,9 +9,11 @@ namespace gdpm::plugin{
string description;
string version;
};
extern int init(int argc, char **argv);
extern int set_name(const char *name);
extern int set_description(const char *description);
extern int set_version(const char *version);
extern int finalize();
extern error initialize(int argc, char **argv);
extern error set_name(const char *name);
extern error set_description(const char *description);
extern error set_version(const char *version);
extern error finalize();
error load(std::filesystem::path path);
}

View file

@ -8,14 +8,8 @@
#include "config.hpp"
namespace gdpm::remote{
using repo_names = string_list;
using repo_urls = string_list;
using repository_map = string_map;
GDPM_DLL_EXPORT error handle_remote(config::context& config, const args_t& args, const var_opts& opts);
GDPM_DLL_EXPORT void set_repositories(config::context& context, const repository_map& repos);
GDPM_DLL_EXPORT void add_repositories(config::context& context, const repository_map& repos);
GDPM_DLL_EXPORT void remove_respositories(config::context& context, const repo_names& names);
GDPM_DLL_EXPORT void move_repository(config::context& context, int old_position, int new_position);
GDPM_DLL_EXPORT void print_repositories(const config::context& context);
GDPM_DLL_EXPORT error add_repository(config::context& config, const args_t& args);
GDPM_DLL_EXPORT error remove_respositories(config::context& config, const args_t& names);
GDPM_DLL_EXPORT void move_repository(config::context& config, int old_position, int new_position);
GDPM_DLL_EXPORT void print_repositories(const config::context& config);
}

View file

@ -101,5 +101,4 @@ namespace gdpm{
default: /*return*/ target = 0;
}
}
}
}

View file

@ -96,7 +96,12 @@ namespace gdpm::utils {
}
std::string readfile(const std::string& path);
void to_lower(std::string& s);
std::string to_lower(const std::string& s);
std::string trim(const std::string& s);
std::string trim_left(const std::string& s);
std::string trim_left(const std::string& s, const std::string& ref);
std::string trim_right(const std::string& s);
std::string trim_right(const std::string& s, const std::string& ref);
std::vector<std::string> parse_lines(const std::string& s);
std::string replace_first(std::string& s, const std::string& from, const std::string& to);
std::string replace_all(std::string& s, const std::string& from, const std::string& to);
@ -105,7 +110,7 @@ namespace gdpm::utils {
bool prompt_user_yn(const char *message);
void delay(std::chrono::milliseconds milliseconds = GDPM_REQUEST_DELAY);
std::string join(const std::vector<std::string>& target, const std::string& delimiter = ", ");
std::string join(const std::unordered_map<std::string, std::string>& target, const std::string& prefix = "", const std::string& delimiter = "\n");
// TODO: Add function to get size of decompressed zip
namespace json {

View file

View file

@ -1,5 +1,6 @@
#include "cache.hpp"
#include "error.hpp"
#include "log.hpp"
#include "constants.hpp"
#include "package.hpp"
@ -12,7 +13,10 @@
namespace gdpm::cache{
error create_package_database(bool overwrite, const params& params){
error create_package_database(
bool overwrite,
const params& params
){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg;
@ -77,7 +81,10 @@ namespace gdpm::cache{
}
error insert_package_info(const package::info_list& packages, const params& params){
error insert_package_info(
const package::info_list& packages,
const params& params
){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
@ -115,7 +122,10 @@ namespace gdpm::cache{
}
result_t<package::info_list> get_package_info_by_id(const package::id_list& package_ids, const params& params){
result_t<package::info_list> get_package_info_by_id(
const package::id_list& package_ids,
const params& params
){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
@ -179,7 +189,10 @@ namespace gdpm::cache{
}
result_t<package::info_list> get_package_info_by_title(const package::title_list& package_titles, const params& params){
result_t<package::info_list> get_package_info_by_title(
const package::title_list& package_titles,
const params& params
){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
@ -306,17 +319,20 @@ namespace gdpm::cache{
}
error update_package_info(const package::info_list& packages, const params& params){
error update_package_info(
const package::info_list& packages,
const params& params
){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
int rc = sqlite3_open(params.cache_path.c_str(), &db);
if(rc != SQLITE_OK){
error error(rc, std::format(
error error(
constants::error::SQLITE_ERR, std::format(
"update_package_info.sqlite3_open(): {}", sqlite3_errmsg(db)
));
log::error(error);
sqlite3_close(db);
return error;
}
@ -360,7 +376,10 @@ namespace gdpm::cache{
}
error delete_packages(const package::title_list& package_titles, const params& params){
error delete_packages(
const package::title_list& package_titles,
const params& params
){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
@ -395,7 +414,10 @@ namespace gdpm::cache{
}
error delete_packages(const package::id_list& package_ids, const params& params){
error delete_packages(
const package::id_list& package_ids,
const params& params
){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;

View file

@ -37,8 +37,6 @@ namespace gdpm::config{
const context& config,
bool pretty_print
){
/* Build a JSON string to pass to document */
string prefix = (pretty_print) ? "\n\t" : "";
string spaces = (pretty_print) ? " " : "";
@ -232,6 +230,7 @@ namespace gdpm::config{
return error();
}
context make_context(
const string& username,
const string& password,
@ -286,8 +285,60 @@ namespace gdpm::config{
return error;
}
void print(const context& config){
void print_json(const context& config){
log::println("{}", to_json(config, true));
}
void _print_property(
const context& config,
const string& property
){
if(property.empty()) return;
else if(property == "username") log::println("username: {}", config.username);
else if(property == "password") log::println("password: {}", config.password);
else if(property == "path") log::println("path: {}", config.path);
else if(property == "token") log::println("token: {}", config.token);
else if(property == "packages_dir") log::println("package directory: {}", config.packages_dir);
else if(property == "tmp_dir") log::println("temporary directory: {}", config.tmp_dir);
else if(property == "remote_sources") log::println("remote sources: \n{}", utils::join(config.remote_sources, "\t", "\n"));
else if(property == "jobs") log::println("parallel jobs: {}", config.jobs);
else if(property == "timeout") log::println("timeout: {}", config.timeout);
else if(property == "sync") log::println("enable sync: {}", config.enable_sync);
else if(property == "cache") log::println("enable cache: {}", config.enable_cache);
else if(property == "prompt") log::println("skip prompt: {}", config.skip_prompt);
else if(property == "logging") log::println("enable file logging: {}", config.enable_file_logging);
else if(property == "clean") log::println("clean temporary files: {}", config.clean_temporary);
else if(property == "verbose") log::println("verbose: {}", config.verbose);
}
void print_properties(
const context& config,
const string_list& properties
){
if(properties.empty()){
_print_property(config, "username");
_print_property(config, "password");
_print_property(config, "path");
_print_property(config, "token");
_print_property(config, "packages_dir");
_print_property(config, "tmp_dir");
_print_property(config, "remote_sources");
_print_property(config, "jobs");
_print_property(config, "timeout");
_print_property(config, "sync");
_print_property(config, "cache");
_print_property(config, "prompt");
_print_property(config, "logging");
_print_property(config, "clean");
_print_property(config, "verbose");
}
std::for_each(
properties.begin(),
properties.end(),
[&config](const string& property){
_print_property(config, property);
}
);
}
}

View file

@ -5,6 +5,7 @@
#include "config.hpp"
#include "package_manager.hpp"
#include "result.hpp"
#include <cstdlib>
int main(int argc, char **argv){
@ -13,6 +14,7 @@ int main(int argc, char **argv){
error error = initialize(argc, argv);
parse_arguments(argc, argv);
finalize();
return 0;
finalize();
return EXIT_SUCCESS;
}

View file

@ -8,6 +8,8 @@
#include "http.hpp"
#include "remote.hpp"
#include "types.hpp"
#include "utils.hpp"
#include <functional>
#include <future>
#include <rapidjson/ostreamwrapper.h>
#include <rapidjson/prettywriter.h>
@ -31,6 +33,7 @@ namespace gdpm::package{
2. Check if the package is installed. If it is, make sure it is latest version.
If not, download and update to the latest version.
3. Extract package contents and copy/move to the correct install location.
*/
result_t result = cache::get_package_info_by_title(package_titles);
@ -167,7 +170,7 @@ namespace gdpm::package{
log::println("Done.");
}else{
error error(
constants::error::HTTP_RESPONSE_ERROR,
constants::error::HTTP_RESPONSE_ERR,
std::format("HTTP Error: {}", response.code)
);
log::error(error);
@ -186,7 +189,12 @@ namespace gdpm::package{
/* Update the cache data with information from */
log::info_n("Updating local asset data...");
cache::update_package_info(p_found);
error error = cache::update_package_info(p_found);
if(error()){
log::error(error);
return error;
}
log::println("done.");
// })
// );
@ -201,7 +209,8 @@ namespace gdpm::package{
const title_list& package_titles,
const params& params
){
/* Install packages in local project instead of package database.
This will not cache the package information in the cache database. */
return error();
}
@ -289,7 +298,13 @@ namespace gdpm::package{
}
log::println("Done.");
log::info_n("Updating local asset data...");
cache::update_package_info(p_cache);
{
error error = cache::update_package_info(p_cache);
if(error.has_occurred()){
log::error(error);
return error;
}
}
log::println("done.");
return error();
@ -402,7 +417,7 @@ namespace gdpm::package{
return error;
}
log::info("{} package(s) found...", doc["total_items"].GetInt());
// log::info("{} package(s) found...", doc["total_items"].GetInt());
print_list(doc);
}
return error();
@ -416,16 +431,13 @@ namespace gdpm::package{
using namespace rapidjson;
using namespace std::filesystem;
string show((!params.sub_commands.empty()) ? params.sub_commands[0] : "");
string show((!params.args.empty()) ? params.args[0] : "");
if(show.empty() || show == "packages"){
result_t r_installed = cache::get_installed_packages();
info_list p_installed = r_installed.unwrap_unsafe();
if(!p_installed.empty()){
print_list(p_installed);
}
else{
log::println("empty");
}
}
else if(show == "remote"){
remote::print_repositories(config);
@ -476,30 +488,27 @@ namespace gdpm::package{
error link(
const config::context& config,
const title_list& package_titles,
const package::params& params
const package::params& params /* path is last arg */
){
using namespace std::filesystem;
path_list paths = {};
if(params.opts.contains("path")){
paths = get<path_list>(params.opts.at("path"));
}
if(paths.empty()){
if(params.args.empty()){
error error(
constants::error::PATH_NOT_DEFINED,
"No path set. Use '--path' option to set a path."
constants::error::INVALID_ARG_COUNT,
"Must supply at least 2 arguments (package name and path)"
);
log::error(error);
return error;
}
/* Check for packages in cache to link */
result_t r_cache = cache::get_package_info_by_title(package_titles);
info_list p_found = {};
info_list p_cache = r_cache.unwrap_unsafe();
if(p_cache.empty()){
error error(
constants::error::NOT_FOUND,
"Could not find any packages to link."
"Could not find any packages to link in cache."
);
log::error(error);
return error;
@ -522,20 +531,22 @@ namespace gdpm::package{
}
/* Get the storage paths for all packages to create symlinks */
path_refs paths = path_refs({params.args.back()});
const path package_dir{config.packages_dir};
for(const auto& p : p_found){
for(const auto& path : paths){
log::info_n("Creating symlink for \"{}\" package to '{}'...", p.title, path + "/" + p.title);
const string _path = path;
log::info_n("link: \"{}\" -> '{}'...", p.title, _path + "/" + p.title);
// std::filesystem::path target{config.packages_dir + "/" + p.title};
std::filesystem::path target = {current_path().string() + "/" + config.packages_dir + "/" + p.title};
std::filesystem::path symlink_path{path + "/" + p.title};
std::filesystem::path symlink_path{_path + "/" + p.title};
if(!std::filesystem::exists(symlink_path.string()))
std::filesystem::create_directories(path + "/");
std::filesystem::create_directories(_path + "/");
std::error_code ec;
std::filesystem::create_directory_symlink(target, symlink_path, ec);
if(ec){
error error(
constants::error::STD_ERROR,
constants::error::STD_ERR,
std::format("Could not create symlink: {}", ec.message())
);
log::error(error);
@ -554,10 +565,10 @@ namespace gdpm::package{
){
using namespace std::filesystem;
if(params.opts.empty()){
if(params.args.empty()){
error error(
constants::error::PATH_NOT_DEFINED,
"No path set. Use '--path' option to set a path."
constants::error::INVALID_ARG_COUNT,
"Must supply at least 2 arguments (package name and path)"
);
log::error(error);
return error;
@ -565,18 +576,25 @@ namespace gdpm::package{
result_t r_cache = cache::get_package_info_by_title(package_titles);
package::info_list p_found = {};
package::info_list p_cache = r_cache.unwrap_unsafe();
package::info_list p_cache = r_cache.unwrap_unsafe();
/* Check for installed packages to clone */
if(p_cache.empty()){
error error(
constants::error::NO_PACKAGE_FOUND,
"Could not find any packages to clone."
"Could not find any packages to clone in cache."
);
log::error(error);
return error;
}
for(const auto& p_title : package_titles){
auto found = std::find_if(p_cache.begin(), p_cache.end(), [&p_title](const package::info& p){ return p.title == p_title; });
auto found = std::find_if(
p_cache.begin(),
p_cache.end(),
[&p_title](const package::info& p){
return p.title == p_title;
});
if(found != p_cache.end()){
p_found.emplace_back(*found);
}
@ -592,15 +610,17 @@ namespace gdpm::package{
}
/* Get the storage paths for all packages to create clones */
path_list paths = get<path_list>(params.opts.at("--path"));
path_refs paths = path_refs{params.args.back()};
// path_list paths = path_list({params.args.back()});
const path package_dir{config.packages_dir};
for(const auto& p : p_found){
for(const auto& path : paths){
log::info("Cloning \"{}\" package to {}", p.title, path + "/" + p.title);
const string _path = string(path);
log::info("clone: \"{}\" -> {}", p.title, _path + "/" + p.title);
std::filesystem::path from{config.packages_dir + "/" + p.title};
std::filesystem::path to{path + "/" + p.title};
std::filesystem::path to{_path + "/" + p.title};
if(!std::filesystem::exists(to.string()))
std::filesystem::create_directories(to);
std::filesystem::create_directories(to); /* This should only occur if using a --force flag */
/* TODO: Add an option to force overwriting (i.e. --overwrite) */
std::filesystem::copy(from, to, copy_options::update_existing | copy_options::recursive);
@ -609,19 +629,23 @@ namespace gdpm::package{
return error();
}
void print_list(const info_list& packages){
for(const auto& p : packages){
log::println("{}/{}/{} {} id={}\n\tGodot {}, {}, {}, Last Modified: {}",
log::println(
GDPM_COLOR_BLUE"{}/"
GDPM_COLOR_RESET "{}/{}/{} "
GDPM_COLOR_GREEN "v{} "
GDPM_COLOR_CYAN "{} "
GDPM_COLOR_RESET "Godot {}, {}",
p.support_level,
p.category,
p.author,
p.title,
p.version,
p.asset_id,
p.modify_date,
// p.asset_id,
p.godot_version,
p.cost,
p.category,
p.modify_date
p.cost
);
}
}
@ -629,16 +653,22 @@ namespace gdpm::package{
void print_list(const rapidjson::Document& json){
for(const auto& o : json["result"].GetArray()){
log::println("{}/{}/{} {} id={}\n\tGodot {}, {}, {}, Last Modified: {}",
log::println(
GDPM_COLOR_BLUE"{}/"
GDPM_COLOR_CYAN "{}/"
GDPM_COLOR_RESET "{}/{} "
GDPM_COLOR_GREEN "v{} "
GDPM_COLOR_CYAN "{} "
GDPM_COLOR_RESET "Godot {}, {}",
o["support_level"] .GetString(),
utils::to_lower(o["category"].GetString()),
o["author"] .GetString(),
o["title"] .GetString(),
o["version_string"] .GetString(),
o["asset_id"] .GetString(),
o["modify_date"] .GetString(),
// o["asset_id"] .GetString(),
o["godot_version"] .GetString(),
o["cost"] .GetString(),
o["category"] .GetString(),
o["modify_date"] .GetString()
o["cost"] .GetString()
);
}
}

View file

@ -36,18 +36,16 @@
*/
namespace gdpm::package_manager{
CURL *curl;
CURLcode res;
config::context config;
remote::repository_map remote_sources;
action_e action;
CURL *curl;
CURLcode res;
config::context config;
action_e action;
// opts_t opts;
bool skip_prompt = false;
bool clean_tmp_dir = false;
int priority = -1;
error initialize(int argc, char **argv){
// curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();
@ -83,166 +81,178 @@ namespace gdpm::package_manager{
error parse_arguments(int argc, char **argv){
using namespace clipp;
/* Replace cxxopts with clipp */
action_e action = action_e::none;
package::title_list package_titles;
string_list input;
package::params params;
args_t args;
var_opts opts;
auto doc_format = clipp::doc_formatting{}
.first_column(7)
.doc_column(45)
.last_column(99);
/* Set global options */
auto configOpt = clipp::option("--config-path").set(config.path)% "set config path";
auto fileOpt = clipp::option("--file", "-f").set(input) % "read file as input";
auto pathOpt = clipp::option("--path").set(params.paths) % "specify a path to use with command";
auto typeOpt = clipp::option("--type").set(config.info.type) % "set package type (any|addon|project)";
auto sortOpt = clipp::option("--sort").set(config.api_params.sort) % "sort packages in order (rating|cost|name|updated)";
auto supportOpt = clipp::option("--support").set(config.api_params.support) % "set the support level for API (all|official|community|testing)";
auto maxResultsOpt = clipp::option("--max-results").set(config.api_params.max_results) % "set the request max results";
auto godotVersionOpt = clipp::option("--godot-version").set(config.api_params.godot_version) % "set the request Godot version";
auto packageDirOpt = clipp::option("--package-dir").set(config.packages_dir) % "set the global package location";
auto tmpDirOpt = clipp::option("--tmp-dir").set(config.tmp_dir) % "set the temporary download location";
auto timeoutOpt = clipp::option("--timeout").set(config.timeout) % "set the request timeout";
auto verboseOpt = clipp::option("--verbose", "-v").set(config.verbose) % "show verbose output";
auto debugOpt = option("-d", "--debug").set(config.verbose, to_int(log::DEBUG)) % "show debug output";
auto configOpt = option("--config-path").set(config.path) % "set config path";
auto fileOpt = repeatable(option("--file", "-f").set(params.args) % "read file as input");
auto pathOpt = option("--path").set(params.paths) % "specify a path to use with command";
auto typeOpt = option("--type").set(config.info.type) % "set package type (any|addon|project)";
auto sortOpt = option("--sort").set(config.api_params.sort) % "sort packages in order (rating|cost|name|updated)";
auto supportOpt = option("--support").set(config.api_params.support) % "set the support level for API (all|official|community|testing)";
auto maxResultsOpt = option("--max-results").set(config.api_params.max_results) % "set the request max results";
auto godotVersionOpt = option("--godot-version").set(config.api_params.godot_version) % "set the request Godot version";
auto packageDirOpt = option("--package-dir").set(config.packages_dir) % "set the global package location";
auto tmpDirOpt = option("--tmp-dir").set(config.tmp_dir) % "set the temporary download location";
auto timeoutOpt = option("--timeout").set(config.timeout) % "set the request timeout";
auto verboseOpt = repeatable(option("-v", "--verbose", "-v").call([]{ config.verbose += 1; })) % "show verbose output";
/* Set the options */
auto cleanOpt = clipp::option("--clean").set(config.clean_temporary) % "enable/disable cleaning temps";
auto parallelOpt = clipp::option("--jobs").set(config.jobs) % "set number of parallel jobs";
auto cacheOpt = clipp::option("--enable-cache").set(config.enable_cache) % "enable/disable local caching";
auto syncOpt = clipp::option("--enable-sync").set(config.enable_sync) % "enable/disable remote syncing";
auto skipOpt = clipp::option("--skip-prompt").set(config.skip_prompt) % "skip the y/n prompt";
auto remoteOpt = clipp::option("--remote").set(params.remote_source) % "set remote source to use";
auto cleanOpt = option("--clean").set(config.clean_temporary) % "enable/disable cleaning temps";
auto parallelOpt = option("--jobs").set(config.jobs) % "set number of parallel jobs";
auto cacheOpt = option("--enable-cache").set(config.enable_cache) % "enable/disable local caching";
auto syncOpt = option("--enable-sync").set(config.enable_sync) % "enable/disable remote syncing";
auto skipOpt = option("--skip-prompt").set(config.skip_prompt) % "skip the y/n prompt";
auto remoteOpt = option("--remote").set(params.remote_source) % "set remote source to use";
auto packageValues = clipp::values("packages", package_titles);
auto requiredPath = clipp::required("--path", input);
auto installCmd = (
clipp::command("install")
.set(action, action_e::install)
.doc("Install packages from asset library"),
clipp::values("packages", package_titles),
clipp::option("--godot-version") & clipp::value("version", config.info.godot_version),
cleanOpt, parallelOpt, syncOpt, skipOpt, remoteOpt
auto packageValues = values("packages", package_titles);
auto requiredPath = required("--path", params.args);
auto installCmd = "install" % (
command("install").set(action, action_e::install),
packageValues % "packages to install from asset library",
godotVersionOpt, cleanOpt, parallelOpt, syncOpt, skipOpt, remoteOpt
);
auto addCmd = (
clipp::command("add").set(action, action_e::add)
.doc("Add a package to a local project"),
packageValues
auto addCmd = "add" % (
command("add").set(action, action_e::add),
packageValues % "package(s) to add to local project",
parallelOpt, skipOpt, remoteOpt
);
auto removeCmd = (
clipp::command("remove")
.set(action, action_e::remove)
.doc("Remove a package from local project"),
packageValues
auto removeCmd = "remove" % (
command("remove").set(action, action_e::remove),
packageValues % "package(s) to remove from local project"
);
auto updateCmd = (
clipp::command("update")
.set(action, action_e::update)
.doc("Update package(s)"),
packageValues
auto updateCmd = "update" % (
command("update").set(action, action_e::update),
packageValues % "update package(s)"
);
auto searchCmd = (
clipp::command("search")
.set(action, action_e::search)
.doc("Search for package(s)"),
packageValues
auto searchCmd = "search" % (
command("search").set(action, action_e::search),
packageValues % "package(s) to search for"
);
auto exportCmd = (
clipp::command("export")
.set(action, action_e::p_export)
.doc("Export package list"),
clipp::values("path", input)
auto exportCmd = "export" % (
command("export").set(action, action_e::p_export),
values("paths", params.args) % "export installed package list to file"
);
auto listCmd = (
clipp::command("list")
.set(action, action_e::list)
.doc("Show installed packages")
auto listCmd = "show installed packages" % (
command("list").set(action, action_e::list)
);
auto linkCmd = (
clipp::command("link")
.set(action, action_e::link)
.doc("Create symlink packages to project"),
packageValues,
requiredPath
auto linkCmd = "link" % (
command("link").set(action, action_e::link),
value("package", package_titles) % "package name to link",
value("path", params.args) % "path to project"
);
auto cloneCmd = (
clipp::command("clone")
.set(action, action_e::clone)
.doc("Clone packages to project"),
packageValues,
requiredPath
auto cloneCmd = "clone" % (
command("clone").set(action, action_e::clone),
value("package", package_titles) % "packages to clone",
value("path", params.args) % "path to project"
);
auto cleanCmd = (
clipp::command("clean")
.set(action, action_e::clean)
.doc("Clean temporary files"),
packageValues
auto cleanCmd = "clean" % (
command("clean").set(action, action_e::clean),
values("packages", package_titles) % "package temporary files to remove"
);
auto configCmd = (
clipp::command("config")
.set(action, action_e::config)
.doc("Set/get config properties")
auto configCmd = "get/set config properties" % (
command("config").set(action, action_e::config_get),
(
( greedy(command("get")).set(action, action_e::config_get),
option(repeatable(values("properties", params.args))) % "get config properties"
)
|
( command("set").set(action, action_e::config_set),
value("property", params.args[1]).call([]{}) % "config property",
value("value", params.args[2]).call([]{}) % "config value"
)
)
);
auto fetchCmd = (
clipp::command("fetch")
.set(action, action_e::fetch)
.doc("Fetch asset metadata from remote source")
auto fetchCmd = "fetch" % (
command("fetch").set(action, action_e::fetch),
option(values("remote", params.args)) % "remote to fetch asset data"
);
auto add_arg = [&params](string arg) { params.args.emplace_back(arg); };
auto remoteCmd = (
clipp::command("remote")
.set(action, action_e::remote)
.doc("Manage remote sources")
.required("subcommand")
command("remote").set(action, action_e::remote_list).if_missing(
[]{
remote::print_repositories(config);
}
),
(
"add a remote source" % ( command("add").set(action, action_e::remote_add),
word("name").call(add_arg) % "remote name",
value("url").call(add_arg) % "remote URL"
)
|
"remove a remote source" % ( command("remove").set(action, action_e::remote_remove),
words("names", params.args) % "remote name(s)"
)
|
"list remote sources" % ( command("list").set(action, action_e::remote_list))
)
);
auto uiCmd = (
clipp::command("ui")
.set(action, action_e::ui)
.doc("Show the UI")
auto uiCmd = "start with UI" % (
command("ui").set(action, action_e::ui)
);
auto helpCmd = (
clipp::command("help")
.set(action, action_e::help)
auto helpCmd = "show this message and exit" % (
command("help").set(action, action_e::help)
);
auto cli = (
debugOpt, configOpt,
(installCmd | addCmd | removeCmd | updateCmd | searchCmd | exportCmd |
listCmd | linkCmd | cloneCmd | cleanCmd | configCmd | fetchCmd |
remoteCmd | uiCmd | helpCmd)
);
/* Make help output */
string map_page_format("");
auto man_page = clipp::make_man_page(cli);
string man_page_format("");
auto man_page = make_man_page(cli, argv[0], doc_format)
.prepend_section("DESCRIPTION", "\tManage Godot Game Engine assets from the command-line.")
.append_section("LICENSE", "\tSee the 'LICENSE.md' file for more details.");
std::for_each(man_page.begin(), man_page.end(),
[&map_page_format](const clipp::man_page::section& s){
map_page_format += s.title() + "\n";
map_page_format += s.content() + "\n";
[&man_page_format](const man_page::section& s){
man_page_format += s.title() + "\n";
man_page_format += s.content() + "\n\n";
}
);
// log::level = config.verbose;
if(clipp::parse(argc, argv, cli)){
log::level = config.verbose;
switch(action){
case action_e::install: package::install(config, package_titles, params); break;
case action_e::add: break;
case action_e::remove: package::remove(config, package_titles, params); break;
case action_e::update: package::update(config, package_titles, params); break;
case action_e::search: package::search(config, package_titles, params); break;
case action_e::p_export: package::export_to(input); break;
case action_e::list: package::list(config, params); break;
/* ...opts are the paths here */
case action_e::link: package::link(config, package_titles, params); break;
case action_e::clone: package::clone(config, package_titles, params); break;
case action_e::clean: package::clean_temporary(config, package_titles); break;
case action_e::config: config::handle_config(config, package_titles, opts); break;
case action_e::fetch: package::synchronize_database(config, package_titles); break;
case action_e::sync: package::synchronize_database(config, package_titles); break;
case action_e::remote: remote::handle_remote(config, args, opts); break;
case action_e::ui: log::info("ui not implemented yet"); break;
case action_e::help: log::println("{}", map_page_format); break;
case action_e::none: /* ...here to run with no command */ break;
case action_e::install: package::install(config, package_titles, params); break;
case action_e::add: package::add(config, package_titles);
case action_e::remove: package::remove(config, package_titles, params); break;
case action_e::update: package::update(config, package_titles, params); break;
case action_e::search: package::search(config, package_titles, params); break;
case action_e::p_export: package::export_to(params.args); break;
case action_e::list: package::list(config, params); break;
/* ...opts are the paths here */
case action_e::link: package::link(config, package_titles, params); break;
case action_e::clone: package::clone(config, package_titles, params); break;
case action_e::clean: package::clean_temporary(config, package_titles); break;
case action_e::config_get: config::print_properties(config, params.args); break;
case action_e::config_set: config::handle_config(config, package_titles, params.opts); break;
case action_e::fetch: package::synchronize_database(config, package_titles); break;
case action_e::sync: package::synchronize_database(config, package_titles); break;
case action_e::remote_list: remote::print_repositories(config); break;
case action_e::remote_add: remote::add_repository(config, params.args); break;
case action_e::remote_remove: remote::remove_respositories(config, params.args); break;
case action_e::ui: log::println("UI not implemented yet"); break;
case action_e::help: log::println("{}", man_page_format); break;
case action_e::none: /* ...here to run with no command */ break;
}
} else {
log::println("{}", map_page_format);
log::println("usage:\n{}", usage_lines(cli, argv[0]).str());
}
return error();
}

View file

@ -6,88 +6,48 @@
#include <readline/readline.h>
namespace gdpm::remote{
error handle_remote(
config::context& config,
const args_t& args,
const var_opts& opts
error add_repository(
config::context &config,
const args_t &args
){
/* Check if enough arguments are supplied */
size_t argc = args.size();
if (argc < 1){
print_repositories(config);
return error();
/* Check if enough args were provided. */
log::debug("arg count: {}\nargs: {}", args.size(), utils::join(args));
if (args.size() < 2){
return error(
constants::error::INVALID_ARG_COUNT,
"Requires a remote name and url argument"
);
}
/* Check which subcommand is supplied */
string sub_command = args.front();
if(sub_command == "add"){
if(args.size() < 3 || args.empty()){
error error(
constants::error::INVALID_ARG_COUNT,
"Invalid number of args."
);
log::error(error);
return error;
}
string name = args[1];
string url = args[2];
add_repositories(config, {{name, url}});
}
else if (sub_command == "remove") {
if(args.size() < 2 || args.empty()){
error error(
constants::error::INVALID_ARG_COUNT,
"Invalid number of args."
);
log::error(error);
return error;
}
remove_respositories(config, {args.begin()+1, args.end()});
}
// else if (sub_command == "set") set_repositories(config::context &context, const repository_map &repos)
else if (sub_command == "list") print_repositories(config);
else{
error error(
constants::error::UNKNOWN,
"Unknown sub-command. Try 'gdpm help remote' for options."
);
log::error(error);
return error;
}
/* Get the first two args */
log::println("{}", args[0]);
config.remote_sources.insert({args[0], args[1]});
return error();
}
void set_repositories(
error remove_respositories(
config::context& config,
const repository_map &repos
const args_t& args
){
config.remote_sources = repos;
}
log::debug("arg count: {}\nargs: {}", args.size(), utils::join(args));
if(args.size() < 1){
return error(
constants::error::INVALID_ARG_COUNT,
"Requires at least one remote name argument"
);
}
void add_repositories(
config::context& config,
const repository_map &repos
){
std::for_each(repos.begin(), repos.end(),
[&config](const string_pair& p){
config.remote_sources.insert(p);
}
);
}
void remove_respositories(
config::context& config,
const repo_names& names
){
for(auto it = names.begin(); it != names.end();){
for(auto it = args.begin(); it != args.end();){
if(config.remote_sources.contains(*it)){
log::println("{}", *it);
config.remote_sources.erase(*it);
}
it++;
}
return error();
}

View file

@ -1,6 +1,7 @@
#include "utils.hpp"
#include "config.hpp"
#include "constants.hpp"
#include "log.hpp"
@ -13,9 +14,12 @@
#include <fcntl.h>
#include <rapidjson/ostreamwrapper.h>
#include <rapidjson/writer.h>
#include <readline/chardefs.h>
#include <readline/readline.h>
#include <string>
#include <thread>
#include <unordered_map>
#include <zip.h>
namespace gdpm::utils{
@ -53,8 +57,38 @@ namespace gdpm::utils{
}
#endif
void to_lower(std::string& s){
std::transform(s.begin(), s.end(), s.begin(), tolower);
std::string to_lower(const std::string& s){
std::string copy = s;
std::transform(copy.begin(), copy.end(), copy.begin(), tolower);
return copy;
}
std::string trim(const std::string& s){
return trim_right(trim_left(s));
}
std::string trim_left(const std::string& s){
return trim_left(s, constants::WHITESPACE);
}
std::string trim_left(
const std::string& s,
const std::string& ref
){
size_t start = s.find_first_not_of(ref);
return (start == std::string::npos) ? "" : s.substr(start);
}
std::string trim_right(const std::string& s){
return trim_right(s, constants::WHITESPACE);
}
std::string trim_right(
const std::string& s,
const std::string& ref
){
size_t end = s.find_last_not_of(ref);
return (end == std::string::npos) ? "" : s.substr(0, end + 1);
}
std::vector<std::string> parse_lines(const std::string &s){
@ -97,7 +131,7 @@ namespace gdpm::utils{
const char *dest,
int verbose
){
const char *prog = "gpdm";
constexpr const char *prog = "gpdm";
struct zip *za;
struct zip_file *zf;
struct zip_stat sb;
@ -198,9 +232,31 @@ namespace gdpm::utils{
const std::string& delimiter
){
std::string o;
std::for_each(target.begin(), target.end(), [&o, &delimiter](const std::string& s){
o += s + delimiter;
});
std::for_each(
target.begin(),
target.end(),
[&o, &delimiter](const std::string& s){
o += s + delimiter;
}
);
o = trim_right(o, delimiter);
return o;
}
std::string join(
const std::unordered_map<std::string, std::string>& target,
const std::string& prefix,
const std::string& delimiter
){
std::string o;
std::for_each(
target.begin(),
target.end(),
[&o, &prefix, &delimiter](const std::pair<std::string, std::string>& p){
o += prefix + p.first + ": " + p.second + delimiter;
}
);
o = trim_right(o, delimiter);
return o;
}

10
tests/plugin.cpp Normal file
View file

@ -0,0 +1,10 @@
#include "plugin.hpp"
#include <doctest.h>
TEST_SUITE("Test example plugin"){
TEST_CASE("Test initialization"){
}
}