diff --git a/CMakeLists.txt b/CMakeLists.txt index a6cee7a..f742965 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,8 @@ set(VersionString "0.0.5") project(walng CXX) +include(cmake/CMakeUtils.cmake) + if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Debug") endif() @@ -12,5 +14,7 @@ add_subdirectory(deps EXCLUDE_FROM_ALL) set(TargetName walng) +enable_testing() + add_subdirectory(code) add_subdirectory(man) diff --git a/README.md b/README.md index bdd3086..0784cf7 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ Download base16 (or base24) scheme you like from https://github.com/tinted-themi run walng: ```sh -curl -O -L https://raw.githubusercontent.com/tinted-theming/schemes/refs/heads/spec-0.11/base16/terracotta.yaml && ./walng --theme-file terracotta.yaml +curl -O -L https://raw.githubusercontent.com/tinted-theming/schemes/refs/heads/spec-0.11/base16/terracotta.yaml && ./walng --theme terracotta.yaml ``` or ```sh -walng --theme-url https://raw.githubusercontent.com/tinted-theming/schemes/refs/heads/spec-0.11/base16/terracotta.yaml +walng --theme https://raw.githubusercontent.com/tinted-theming/schemes/refs/heads/spec-0.11/base16/terracotta.yaml ``` diff --git a/cmake/CMakeUtils.cmake b/cmake/CMakeUtils.cmake new file mode 100644 index 0000000..29a3994 --- /dev/null +++ b/cmake/CMakeUtils.cmake @@ -0,0 +1,49 @@ +include(CMakeParseArguments) + +function(CMakeUtilsRemoveMatchesFromList) + set(options) + set(oneValueArgs) + set(multiValueArgs MATCHES) + cmake_parse_arguments(TQ_PARSED "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + foreach (TQ_LIST ${TQ_PARSED_UNPARSED_ARGUMENTS}) + foreach (TQ_ENTRY ${${TQ_LIST}}) + foreach (TQ_MATCH ${TQ_PARSED_MATCHES}) + if (${TQ_ENTRY} MATCHES ${TQ_MATCH}) + list(REMOVE_ITEM ${TQ_LIST} ${TQ_ENTRY}) + endif() + endforeach() + endforeach() + set(${TQ_LIST} ${${TQ_LIST}} PARENT_SCOPE) + endforeach() +endfunction() + +function(CMakeUtilsAddTestsFromSourceList) + set(options) + set(oneValueArgs PREFIX) + set(multiValueArgs LINK_LIBS COMPILE_OPTIONS COMPILE_DEFINITIONS COMPILE_FEATURES) + + cmake_parse_arguments(TQ_PARSED "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + foreach (TQ_LIST ${TQ_PARSED_UNPARSED_ARGUMENTS}) + foreach (TQ_ENTRY ${${TQ_LIST}}) + if (${TQ_ENTRY} MATCHES ".*_test.cpp") + string(REGEX REPLACE "^.*\/(.*)_test\.cpp$" "\\1" testName "${TQ_ENTRY}") + set(testName "${TQ_PARSED_PREFIX}-${testName}-test") + + add_executable(${testName} ${TQ_ENTRY}) + target_compile_features(${testName} PRIVATE ${TQ_PARSED_COMPILE_FEATURES}) + target_compile_options(${testName} PRIVATE ${TQ_PARSED_COMPILE_OPTIONS}) + target_compile_definitions(${testName} PRIVATE ${TQ_PARSED_COMPILE_DEFINITIONS}) + target_link_libraries(${testName} PRIVATE ${TQ_PARSED_LINK_LIBS}) + + add_test(${testName} ${testName}) + endif() + endforeach() + endforeach() +endfunction() + +function(CMakeUtilsExcludeTestsFromSourceList TQ_LIST) + list(FILTER ${TQ_LIST} EXCLUDE REGEX ".*_test.cpp") + set(${TQ_LIST} ${${TQ_LIST}} PARENT_SCOPE) +endfunction() diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index ca6a2b8..5ec6330 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -1,33 +1,35 @@ add_executable(${TargetName}) - target_compile_features(${TargetName} PRIVATE cxx_std_23) - set_target_properties(${TargetName} PROPERTIES CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF) - target_compile_options(${TargetName} PRIVATE -Wall -Wextra -Wattributes -Wpedantic -Wstrict-aliasing -Wcast-align -g -fmacro-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}/= ) - target_compile_definitions(${TargetName} PRIVATE -DWALNG_VERSION="${VersionString}" ) - target_link_libraries(${TargetName} PRIVATE cxxopts::cxxopts yaml-cpp::yaml-cpp 3rdparty::inja CURL::libcurl_static) file(GLOB_RECURSE Sources "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") file(GLOB_RECURSE Headers "${CMAKE_CURRENT_SOURCE_DIR}/*.h") +CMakeUtilsAddTestsFromSourceList(Sources + PREFIX ${TargetName} + COMPILE_FEATURES cxx_std_23 + COMPILE_OPTIONS -Wall -Wextra -g + LINK_LIBS doctest::doctest_with_main CURL::libcurl_static) + +CMakeUtilsExcludeTestsFromSourceList(Sources) + target_sources(${TargetName} PUBLIC ${Headers} ${Sources}) file(COPY config.yaml DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) include(GNUInstallDirs) - install(TARGETS ${TargetName} DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/code/main.cpp b/code/main.cpp index cd705d8..4574051 100644 --- a/code/main.cpp +++ b/code/main.cpp @@ -10,6 +10,7 @@ #include "Request.h" #include "generate.h" +#include "net/download.h" #include "utils.h" #include "version.h" @@ -25,10 +26,10 @@ std::expected extractFileNameFromUrl(s }); } -std::expected writeFile(std::filesystem::path const& path, std::string_view content) noexcept { +std::expected writeFile(std::filesystem::path const& path, std::string_view content) noexcept { FILE* file = ::fopen(path.c_str(), "w"); if (!file) { - return std::unexpected(std::system_error(errno, std::system_category(), "failed to open file for writting")); + return std::unexpected(std::error_code(errno, std::system_category())); } // TODO: check result @@ -38,41 +39,35 @@ std::expected writeFile(std::filesystem::path const& pa return {}; } -std::expected downloadFileOrGetFromCache(std::string const& url) noexcept { - using namespace walng; - - auto filename = extractFileNameFromUrl(url); - if (!filename) { - return std::unexpected(filename.error()); - } - - auto request = Request(); - if (auto const rc = request.prepare(url); !rc) { - return std::unexpected(rc.error()); - } - - auto response = request.perform(); +/// Download file. Return error or path where content stored +std::expected downloadFile(std::string const& url) noexcept { + auto response = walng::net::download(url); if (!response) { return std::unexpected(response.error()); } - auto cachePath = getCachePath(); + if (response->code != 200) { + return std::unexpected(std::error_code(ENOENT, std::system_category())); + } + + auto cachePath = walng::getCachePath(); if (!cachePath.has_value()) { return std::unexpected(cachePath.error()); } + auto const themesPath = cachePath.value() / "themes"; std::error_code ec; create_directories(themesPath, ec); if (ec) { - return std::unexpected(std::system_error(ec, "can't create themes cache dir")); + return std::unexpected(ec); } - auto const themeFilePath = themesPath / *filename; - if (auto const rc = writeFile(themeFilePath, response->body()); !rc) { + auto themeFilePath = themesPath / response->filename; + if (auto const rc = writeFile(themeFilePath, response->content); !rc) { return std::unexpected(response.error()); } - return themeFilePath; + return {std::move(themeFilePath)}; } int main(int argc, char* argv[]) { @@ -82,8 +77,7 @@ int main(int argc, char* argv[]) { // clang-format off options.add_options() ("config", "path to config file", cxxopts::value(), "PATH") - ("theme-file", "path to theme file", cxxopts::value(), "PATH") - ("theme-url", "url to theme file", cxxopts::value(), "URL") + ("theme", "path or url to theme file", cxxopts::value(), "PATH or URL") ("help", "prints the help and exit") ("version", "prints the version and exit") ; @@ -107,26 +101,24 @@ int main(int argc, char* argv[]) { } auto configPath = walng::getConfigPath(); if (!configPath.has_value()) { - throw configPath.error(); + throw std::system_error(configPath.error()); } return configPath.value() / "config.yaml"; }(); - if (result.count("theme-file") > 0) { - if (result.count("theme-url") > 0) { - throw std::runtime_error("argument `--theme-file` and `--theme-url` can't be set simultaneously"); - } - std::filesystem::path const themePath = result["theme-file"].as(); - walng::generate(configPath, themePath); - } else if (result.count("theme-url") > 0) { - std::string const& themeUrl = result["theme-url"].as(); - auto const downloadResult = downloadFileOrGetFromCache(themeUrl); - if (!downloadResult) { - throw downloadResult.error(); + if (result.count("theme") == 0) { + throw std::runtime_error("argument `--theme` should be set"); + } + auto const& theme = result["theme"].as(); + + if (theme.starts_with("http://") || theme.starts_with("https://")) { + if (auto rc = downloadFile(theme); rc) { + walng::generate(configPath, *rc); + } else { + throw std::system_error(rc.error()); } - walng::generate(configPath, downloadResult.value()); } else { - throw std::runtime_error("argument `--theme-file` or `--theme-url` should be set"); + walng::generate(configPath, theme); } } catch (std::exception const& e) { diff --git a/code/net/CurlEasyHandle.h b/code/net/CurlEasyHandle.h new file mode 100644 index 0000000..f385a6f --- /dev/null +++ b/code/net/CurlEasyHandle.h @@ -0,0 +1,117 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#pragma once + +#include +#include + +#include + +#include "error.h" + +namespace walng::net { + +class CurlEasyHandle { +private: + CURL* handle_ = nullptr; + +public: + CurlEasyHandle(CurlEasyHandle const&) = delete; + CurlEasyHandle& operator=(CurlEasyHandle const&) = delete; + + CurlEasyHandle() = default; + + CurlEasyHandle(CurlEasyHandle&& other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {} + + ~CurlEasyHandle() noexcept { + if (handle_) { + ::curl_easy_cleanup(handle_); + } + } + + CurlEasyHandle& operator=(CurlEasyHandle&& other) noexcept { + if (this != &other) { + this->~CurlEasyHandle(); + new (this) CurlEasyHandle(std::move(other)); + } + return *this; + } + + explicit CurlEasyHandle(CURL* handle) noexcept : handle_(handle) {} + + /// Create and init CurlEasyHandle + static std::expected create() noexcept { + auto handle = ::curl_easy_init(); + if (!handle) [[unlikely]] { + return std::unexpected(make_error_code(CurlInitError::EasyInit)); + } + return {CurlEasyHandle(handle)}; + } + + /// Return true on handle valid + [[nodiscard]] operator bool() const noexcept { + return handle_ != nullptr; + } + + /// Wrapper around @c curl_easy_reset + void reset() noexcept { + ::curl_easy_reset(handle_); + } + + /// Wrapper around @c curl_easy_setopt + template + std::expected setOption(CURLoption opt, T&& value) noexcept { + if constexpr (std::is_same_v, std::string>) { + return this->setOptionImpl(opt, value.c_str()); + } else if constexpr (std::is_same_v, std::string_view>) { + return this->setOptionImpl(opt, std::string(value).c_str()); + } else { + return this->setOptionImpl(opt, std::forward(value)); + } + } + + /// Wrapper around @c curl_easy_getinfo + template + std::expected info(CURLINFO info) const { + if constexpr (std::is_same_v) { + return this->infoImpl(info).transform([](char const* value) { + return std::string(value); + }); + } else if constexpr (std::is_same_v) { + return this->infoImpl(info).transform([](char const* value) { + return std::string_view(value); + }); + } else { + return this->infoImpl(info); + } + } + + /// Wrapper around @c curl_easy_perform + std::expected perform() noexcept { + if (auto const rc = ::curl_easy_perform(handle_); rc != CURLE_OK) [[unlikely]] { + return std::unexpected(make_error_code(rc)); + } + return {}; + } + +private: + template + std::expected setOptionImpl(CURLoption opt, T&& value) noexcept { + if (auto const rc = ::curl_easy_setopt(handle_, opt, std::forward(value)); rc != CURLE_OK) [[unlikely]] { + return std::unexpected(make_error_code(rc)); + } + return {}; + } + + template + std::expected infoImpl(CURLINFO info) const { + T result; + if (auto const rc = ::curl_easy_getinfo(handle_, info, &result); rc != CURLE_OK) [[unlikely]] { + return std::unexpected(make_error_code(rc)); + } + return {result}; + } +}; + +} // namespace walng::net diff --git a/code/net/CurlEasyHandle_test.cpp b/code/net/CurlEasyHandle_test.cpp new file mode 100644 index 0000000..5825739 --- /dev/null +++ b/code/net/CurlEasyHandle_test.cpp @@ -0,0 +1,26 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#include + +#include "CurlEasyHandle.h" + +namespace walng::net { + +TEST_CASE("CurlEasyHandle: move-semantic") { + CurlEasyHandle handle; + CHECK(!handle); + CHECK(!handle.setOption(CURLOPT_FOLLOWLOCATION, 1U)); + + auto result = CurlEasyHandle::create(); + CHECK(result); + CHECK(*result); + + handle = std::move(*result); + CHECK(handle); + CHECK(!result.value()); + + CHECK(handle.setOption(CURLOPT_FOLLOWLOCATION, 1U)); +} + +} // namespace walng::net diff --git a/code/net/CurlUrlHandle.h b/code/net/CurlUrlHandle.h new file mode 100644 index 0000000..b2191c3 --- /dev/null +++ b/code/net/CurlUrlHandle.h @@ -0,0 +1,90 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#pragma once + +#include +#include +#include + +#include + +#include "error.h" + +namespace walng::net { + +class CurlUrlHandle { +private: + CURLU* handle_ = nullptr; + +public: + CurlUrlHandle(CurlUrlHandle const&) = delete; + CurlUrlHandle& operator=(CurlUrlHandle const&) = delete; + + explicit CurlUrlHandle(CURLU* handle) noexcept : handle_(handle) {} + + CurlUrlHandle() = default; + + ~CurlUrlHandle() noexcept { + if (handle_) { + ::curl_url_cleanup(handle_); + } + } + + CurlUrlHandle(CurlUrlHandle&& other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {} + + CurlUrlHandle& operator=(CurlUrlHandle&& other) noexcept { + if (this != &other) { + this->~CurlUrlHandle(); + new (this) CurlUrlHandle(std::move(other)); + } + return *this; + } + + /// Create and init CurlUrlHandle + static std::expected create() noexcept { + auto handle = ::curl_url(); + if (!handle) [[unlikely]] { + return std::unexpected(make_error_code(CurlInitError::UrlInit)); + } + return {CurlUrlHandle(handle)}; + } + + /// Return true on handle valid + [[nodiscard]] operator bool() const noexcept { + return handle_ != nullptr; + } + + /// Wrapper around @c curl_url_get + std::expected part(CURLUPart part) const { + char* value = nullptr; + if (auto const rc = ::curl_url_get(handle_, part, &value, 0); rc != CURLUE_OK) [[unlikely]] { + return std::unexpected(make_error_code(rc)); + } + std::string result(value); + ::curl_free(value); + return {result}; + } + + /// Wrapper around @c curl_url_set + template + std::expected setPart(CURLUPart part, T&& value) { + if constexpr (std::is_same_v, std::string>) { + return this->setPartImpl(part, value.c_str()); + } else if constexpr (std::is_same_v, std::string_view>) { + return this->setPartImpl(part, std::string(value).c_str()); + } else { + return this->setPartImpl(part, value); + } + } + +private: + std::expected setPartImpl(CURLUPart part, char const* value) noexcept { + if (auto const rc = ::curl_url_set(handle_, part, value, 0); rc != CURLUE_OK) [[unlikely]] { + return std::unexpected(make_error_code(rc)); + } + return {}; + } +}; + +} // namespace walng::net diff --git a/code/net/CurlUrlHandle_test.cpp b/code/net/CurlUrlHandle_test.cpp new file mode 100644 index 0000000..99cb45d --- /dev/null +++ b/code/net/CurlUrlHandle_test.cpp @@ -0,0 +1,63 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#include + +#include "CurlUrlHandle.h" + +namespace walng::net { + +TEST_CASE("CurlUrlHandle: move-semantic") { + CurlUrlHandle handle; + CHECK(!handle); + + auto result = CurlUrlHandle::create(); + CHECK(result); + CHECK(*result); + + handle = std::move(*result); + CHECK(handle); + CHECK(!result.value()); +} + +TEST_CASE("CurlUrlHandle: ") { + auto result = CurlUrlHandle::create().and_then([](auto&& result) -> std::expected { + if (auto rc = result.setPart(CURLUPART_URL, "https://github.com/ksergey/walng/README.md"); !rc) { + return std::unexpected(rc.error()); + } + return result; + }); + + CHECK(result); + CurlUrlHandle handle = std::move(*result); + + if (auto rc = handle.part(CURLUPART_URL); rc) { + CHECK(rc.value() == "https://github.com/ksergey/walng/README.md"); + } else { + CHECK(false); + } + + if (auto rc = handle.part(CURLUPART_PATH); rc) { + CHECK(rc.value() == "/ksergey/walng/README.md"); + } else { + CHECK(false); + } + + if (auto rc = handle.setPart(CURLUPART_PATH, "/xyz"); !rc) { + CHECK(false); + } + + if (auto rc = handle.part(CURLUPART_PATH); rc) { + CHECK(rc.value() == "/xyz"); + } else { + CHECK(false); + } + + if (auto rc = handle.part(CURLUPART_URL); rc) { + CHECK(rc.value() == "https://github.com/xyz"); + } else { + CHECK(false); + } +} + +} // namespace walng::net diff --git a/code/net/download.cpp b/code/net/download.cpp new file mode 100644 index 0000000..d9cd35d --- /dev/null +++ b/code/net/download.cpp @@ -0,0 +1,98 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#include "download.h" + +#include + +#include "CurlEasyHandle.h" +#include "CurlUrlHandle.h" + +namespace walng::net { +namespace { + +size_t curlWriteFn(char const* data, size_t size, size_t nmemb, void* userdata) { + auto const response = static_cast(userdata); + auto const chunkSize = size * nmemb; + response->content.append(data, chunkSize); + return chunkSize; +} + +std::expected extractFilename(char const* url) { + CurlUrlHandle handle; + if (auto rc = CurlUrlHandle::create(); rc) { + handle = std::move(*rc); + } else { + return std::unexpected(rc.error()); + } + + if (auto rc = handle.setPart(CURLUPART_URL, url); !rc) { + return std::unexpected(rc.error()); + } + + std::filesystem::path path; + if (auto rc = handle.part(CURLUPART_PATH); rc) { + path = std::move(*rc); + } else { + return std::unexpected(rc.error()); + } + + return {path.filename()}; +} + +} // namespace + +auto download(char const* url, std::optional timeout) + -> std::expected { + CurlEasyHandle handle; + if (auto rc = CurlEasyHandle::create(); rc) { + handle = std::move(*rc); + } else { + return std::unexpected(rc.error()); + } + + Response response; + + if (auto rc = handle.setOption(CURLOPT_WRITEDATA, &response); !rc) { + return std::unexpected(rc.error()); + } + if (auto rc = handle.setOption(CURLOPT_WRITEFUNCTION, &curlWriteFn); !rc) { + return std::unexpected(rc.error()); + } + if (auto rc = handle.setOption(CURLOPT_FOLLOWLOCATION, 1L); !rc) { + return std::unexpected(rc.error()); + } + if (auto rc = handle.setOption(CURLOPT_URL, url); !rc) { + return std::unexpected(rc.error()); + } + if (timeout) { + if (auto rc = handle.setOption(CURLOPT_TIMEOUT_MS, static_cast(timeout->count())); !rc) { + return std::unexpected(rc.error()); + } + } + + if (auto rc = handle.perform(); !rc) { + return std::unexpected(rc.error()); + } + + if (auto rc = handle.info(CURLINFO_RESPONSE_CODE); rc) { + response.code = *rc; + } else { + std::print(stderr, "CURLINFO_RESPONSE_CODE falied: {}\n", rc.error().message()); + } + + if (auto rc = handle.info(CURLINFO_EFFECTIVE_URL); rc) { + char const* effectiveUrl = *rc; + if (auto rc = extractFilename(effectiveUrl); rc) { + response.filename = *rc; + } else { + std::print(stderr, "can't extract downloaded file filename: {}\n", rc.error().message()); + } + } else { + std::print(stderr, "CURLINFO_EFFECTIVE_URL falied: {}\n", rc.error().message()); + } + + return {std::move(response)}; +} + +} // namespace walng::net diff --git a/code/net/download.h b/code/net/download.h new file mode 100644 index 0000000..9041758 --- /dev/null +++ b/code/net/download.h @@ -0,0 +1,31 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace walng::net { + +struct Response { + long code = 0; + std::string content; + std::filesystem::path filename; +}; + +/// Download file at url +auto download(char const* url, std::optional timeout = std::nullopt) + -> std::expected; + +/// \overload +inline auto download(std::string const& url, std::optional timeout = std::nullopt) + -> std::expected { + return download(url.c_str(), timeout); +} + +} // namespace walng::net diff --git a/code/net/error.h b/code/net/error.h new file mode 100644 index 0000000..f04e3fc --- /dev/null +++ b/code/net/error.h @@ -0,0 +1,66 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#pragma once + +#include + +#include + +namespace walng::net { + +enum class CurlInitError { EasyInit, UrlInit }; + +struct CurlInitErrorCategory final : std::error_category { + char const* name() const noexcept override { + return "curl-init-error"; + } + std::string message(int ec) const override { + switch (static_cast(ec)) { + case CurlInitError::EasyInit: + return "failed to init easy handle"; + case CurlInitError::UrlInit: + return "failed to init url handle"; + default: + return "(unrecognized error)"; + } + } +}; + +CurlInitErrorCategory const theCurlInitErrorCategory{}; + +inline std::error_code make_error_code(CurlInitError e) noexcept { + return {static_cast(e), theCurlInitErrorCategory}; +} + +struct CurlEasyErrorCategory final : std::error_category { + char const* name() const noexcept override { + return "curl-easy-error"; + } + std::string message(int ec) const override { + return ::curl_easy_strerror(static_cast<::CURLcode>(ec)); + } +}; + +CurlEasyErrorCategory const theCurlEasyErrorCategory{}; + +inline std::error_code make_error_code(::CURLcode ec) { + return std::error_code(static_cast(ec), theCurlEasyErrorCategory); +} + +struct CurlUrlErrorCategory final : std::error_category { + char const* name() const noexcept override { + return "curl-url-error"; + } + std::string message(int ec) const override { + return ::curl_url_strerror(static_cast<::CURLUcode>(ec)); + } +}; + +CurlUrlErrorCategory const theCurlUrlErrorCategory{}; + +inline std::error_code make_error_code(::CURLUcode ec) { + return std::error_code(static_cast(ec), theCurlUrlErrorCategory); +} + +} // namespace walng::net diff --git a/code/net/error_test.cpp b/code/net/error_test.cpp new file mode 100644 index 0000000..92ed1b8 --- /dev/null +++ b/code/net/error_test.cpp @@ -0,0 +1,14 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#include + +#include "error.h" + +namespace walng::net { + +TEST_CASE("net-error: CurlInit") { + CHECK(make_error_code(CurlInitError::UrlInit).message() == "failed to init url handle"); +} + +} // namespace walng::net diff --git a/code/utils.cpp b/code/utils.cpp index a4e138c..5865b69 100644 --- a/code/utils.cpp +++ b/code/utils.cpp @@ -7,14 +7,14 @@ namespace walng { -std::expected getHomePath() { +std::expected getHomePath() { if (auto const result = ::secure_getenv("HOME"); result) { - return std::filesystem::path(result); + return {std::filesystem::path(result)}; } - return std::unexpected(std::system_error(ENOENT, std::system_category(), "HOME env variable not set")); + return std::unexpected(std::error_code(ENOENT, std::system_category())); } -std::expected getConfigPath() { +std::expected getConfigPath() { if (auto const result = ::secure_getenv("XDG_CONFIG_HOME"); result) { return std::filesystem::path(result) / "walng"; } @@ -23,7 +23,7 @@ std::expected getConfigPath() { }); } -std::expected getCachePath() { +std::expected getCachePath() { if (auto const result = ::secure_getenv("XDG_CACHE_HOME"); result) { return std::filesystem::path(result) / "walng"; } @@ -32,7 +32,7 @@ std::expected getCachePath() { }); } -std::expected makeTempFilePath() { +std::expected makeTempFilePath() { static constexpr std::string_view kAllowedChars = "abcdefghijklmnaoqrstuvwxyz1234567890"; std::random_device randomDevice; @@ -47,12 +47,12 @@ std::expected makeTempFilePath() { std::error_code ec; auto tempDirPath = std::filesystem::temp_directory_path(ec); if (ec) { - return std::unexpected(std::system_error(ec, "can't obtain temp directory path")); + return std::unexpected(ec); } return tempDirPath / std::string_view(tmpName.data(), tmpName.size()); } -std::expected expandTilda(std::filesystem::path& path) { +std::expected expandTilda(std::filesystem::path& path) { if (auto const& str = path.native(); str.starts_with("~/")) { auto homePath = getHomePath(); if (!homePath.has_value()) { diff --git a/code/utils.h b/code/utils.h index d993104..2bb90b8 100644 --- a/code/utils.h +++ b/code/utils.h @@ -10,26 +10,26 @@ namespace walng { /// Get path to $HOME directory -std::expected getHomePath(); +std::expected getHomePath(); /// Get path to \c walng config directory /// /// One of: /// - $XDG_CONFIG_HOME/walng /// - $HOME/.config/walng -std::expected getConfigPath(); +std::expected getConfigPath(); /// Get path to \c walng cache directory /// /// One of: /// - $XDG_CACHE_HOME/walng /// - $HOME/.cache/walng -std::expected getCachePath(); +std::expected getCachePath(); /// Make temporary file path -std::expected makeTempFilePath(); +std::expected makeTempFilePath(); /// Expand @c ~ to $HOME -std::expected expandTilda(std::filesystem::path& path); +std::expected expandTilda(std::filesystem::path& path); } // namespace walng diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 6f3430b..462462e 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -1,28 +1,48 @@ include(FetchContent) +# ------------------------------------------------------------------------------------------------- +# cxxopts +# ------------------------------------------------------------------------------------------------- + FetchContent_Declare(cxxopts URL https://github.com/jarro2783/cxxopts/archive/refs/tags/v3.2.0.tar.gz DOWNLOAD_EXTRACT_TIMESTAMP ON ) FetchContent_MakeAvailable(cxxopts) +# ------------------------------------------------------------------------------------------------- +# yaml-cpp +# ------------------------------------------------------------------------------------------------- + FetchContent_Declare(yaml-cpp URL https://github.com/jbeder/yaml-cpp/archive/2f86d13775d119edbb69af52e5f566fd65c6953b.zip DOWNLOAD_EXTRACT_TIMESTAMP ON ) FetchContent_MakeAvailable(yaml-cpp) +# ------------------------------------------------------------------------------------------------- +# json +# ------------------------------------------------------------------------------------------------- + FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz DOWNLOAD_EXTRACT_TIMESTAMP ON ) FetchContent_MakeAvailable(json) +# ------------------------------------------------------------------------------------------------- +# inja +# ------------------------------------------------------------------------------------------------- + add_library(inja INTERFACE) target_include_directories(inja INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(inja INTERFACE nlohmann_json::nlohmann_json) add_library(3rdparty::inja ALIAS inja) +# ------------------------------------------------------------------------------------------------- +# curl +# ------------------------------------------------------------------------------------------------- + set(BUILD_SHARED_LIBS OFF) set(BUILD_STATIC_LIBS ON) set(BUILD_CURL_EXE OFF) @@ -46,3 +66,15 @@ FetchContent_Declare(curl DOWNLOAD_EXTRACT_TIMESTAMP ON ) FetchContent_MakeAvailable(curl) + +# ------------------------------------------------------------------------------------------------- +# doctest +# ------------------------------------------------------------------------------------------------- + +FetchContent_Declare(doctest + URL https://github.com/doctest/doctest/archive/refs/tags/v2.4.12.tar.gz + EXCLUDE_FROM_ALL + DOWNLOAD_EXTRACT_TIMESTAMP ON +) +FetchContent_MakeAvailable(doctest) +