| @@ -7,5 +7,6 @@ Uses: nlohmann/json | |||
| Uses: neo/sqlite3 | |||
| Uses: neo/fun | |||
| Uses: semver/semver | |||
| Uses: vob/semester | |||
| Uses: pubgrub/pubgrub | |||
| Uses: vob/json5 | |||
| @@ -10,6 +10,7 @@ | |||
| "neo/fun", | |||
| "semver/semver", | |||
| "pubgrub/pubgrub", | |||
| "vob/json5" | |||
| "vob/json5", | |||
| "vob/semester", | |||
| ] | |||
| } | |||
| @@ -10,6 +10,7 @@ Depends: neo-sqlite3 0.2.2 | |||
| Depends: neo-fun 0.1.0 | |||
| Depends: semver 0.2.1 | |||
| Depends: pubgrub 0.2.0 | |||
| Depends: json5 0.1.2 | |||
| Depends: vob-json5 0.1.5-dev | |||
| Depends: semester 0.1.0 | |||
| Test-Driver: Catch-Main | |||
| Test-Driver: Catch-Main | |||
| @@ -12,7 +12,8 @@ | |||
| "neo-fun": "0.1.0", | |||
| "semver": "0.2.1", | |||
| "pubgrub": "0.2.0", | |||
| "json5": "0.1.2" | |||
| "vob-json5": "0.1.5-dev", | |||
| "semester": "*" | |||
| }, | |||
| "test_driver": "Catch-Main" | |||
| } | |||
| @@ -3,11 +3,11 @@ | |||
| #include <dds/dym.hpp> | |||
| #include <dds/error/errors.hpp> | |||
| #include <dds/util/algo.hpp> | |||
| #include <dds/util/json5_read.hpp> | |||
| #include <libman/parse.hpp> | |||
| #include <json5/parse_data.hpp> | |||
| #include <range/v3/view/transform.hpp> | |||
| #include <semester/decomp.hpp> | |||
| #include <spdlog/spdlog.h> | |||
| using namespace dds; | |||
| @@ -42,24 +42,58 @@ library_manifest library_manifest::load_from_file(path_ref fpath) { | |||
| } | |||
| library_manifest lib; | |||
| using namespace json_read::ops; | |||
| json_read::decompose( // | |||
| data.as_object(), | |||
| object(key("name", require_string(put_into{lib.name}, "`name` must be a string")), | |||
| key("uses", | |||
| array_each{require_string( | |||
| [&](auto&& uses) { | |||
| lib.uses.push_back(lm::split_usage_string(uses.as_string())); | |||
| return json_read::accept_t{}; | |||
| }, | |||
| "All `uses` items must be strings")}), | |||
| key("links", | |||
| array_each{require_string( | |||
| [&](auto&& links) { | |||
| lib.links.push_back(lm::split_usage_string(links.as_string())); | |||
| return json_read::accept_t{}; | |||
| }, | |||
| "All `links` items must be strings")}))); | |||
| using namespace semester::decompose_ops; | |||
| auto res = semester::decompose( // | |||
| data, | |||
| try_seq{require_type<json5::data::mapping_type>{ | |||
| "The root of the library manifest must be an object (mapping)"}, | |||
| mapping{ | |||
| if_key{"name", | |||
| require_type<std::string>{"`name` must be a string"}, | |||
| put_into{lib.name}}, | |||
| if_key{"uses", | |||
| require_type<json5::data::array_type>{ | |||
| "`uses` must be an array of usage requirements"}, | |||
| for_each{ | |||
| require_type<std::string>{"`uses` elements must be strings"}, | |||
| [&](auto&& uses) { | |||
| lib.uses.push_back(lm::split_usage_string(uses.as_string())); | |||
| return semester::dc_accept; | |||
| }, | |||
| }}, | |||
| if_key{"links", | |||
| require_type<json5::data::array_type>{ | |||
| "`links` must be an array of usage requirements"}, | |||
| for_each{ | |||
| require_type<std::string>{"`links` elements must be strings"}, | |||
| [&](auto&& links) { | |||
| lib.links.push_back(lm::split_usage_string(links.as_string())); | |||
| return semester::dc_accept; | |||
| }, | |||
| }}, | |||
| }}); | |||
| auto rej = std::get_if<semester::dc_reject_t>(&res); | |||
| if (rej) { | |||
| throw_user_error<errc::invalid_lib_manifest>(rej->message); | |||
| } | |||
| // using namespace json_read::ops; | |||
| // json_read::decompose( // | |||
| // data.as_object(), | |||
| // object(key("name", require_string(put_into{lib.name}, "`name` must be a string")), | |||
| // key("uses", | |||
| // array_each{require_string( | |||
| // [&](auto&& uses) { | |||
| // lib.uses.push_back(lm::split_usage_string(uses.as_string())); | |||
| // return json_read::accept_t{}; | |||
| // }, | |||
| // "All `uses` items must be strings")}), | |||
| // key("links", | |||
| // array_each{require_string( | |||
| // [&](auto&& links) { | |||
| // lib.links.push_back(lm::split_usage_string(links.as_string())); | |||
| // return json_read::accept_t{}; | |||
| // }, | |||
| // "All `links` items must be strings")}))); | |||
| if (lib.name.empty()) { | |||
| throw_user_error<errc::invalid_lib_manifest>( | |||
| @@ -2,13 +2,13 @@ | |||
| #include <dds/dym.hpp> | |||
| #include <dds/error/errors.hpp> | |||
| #include <dds/util/json5_read.hpp> | |||
| #include <dds/util/string.hpp> | |||
| #include <libman/parse.hpp> | |||
| #include <range/v3/view/split.hpp> | |||
| #include <range/v3/view/split_when.hpp> | |||
| #include <range/v3/view/transform.hpp> | |||
| #include <semester/decomp.hpp> | |||
| #include <spdlog/spdlog.h> | |||
| #include <json5/parse_data.hpp> | |||
| @@ -78,62 +78,82 @@ package_manifest package_manifest::load_from_file(const fs::path& fpath) { | |||
| throw_user_error<errc::invalid_pkg_manifest>("Root value must be an object"); | |||
| } | |||
| const auto& obj = data.as_object(); | |||
| package_manifest ret; | |||
| using namespace json_read::ops; | |||
| json_read::decompose( | |||
| obj, | |||
| object( | |||
| key("name", require_string(put_into{ret.pkg_id.name}, "`name` must be a string")), | |||
| key("namespace", | |||
| require_string(put_into{ret.namespace_}, "`namespace` must be a string")), | |||
| ignore_key{"$schema"}, | |||
| key("version", | |||
| require_string( | |||
| using namespace semester::decompose_ops; | |||
| auto res = semester::decompose( // | |||
| data, | |||
| try_seq{ | |||
| require_type<json5::data::mapping_type>{ | |||
| "The root of a package manifest must be an object (mapping)"}, | |||
| mapping{ | |||
| if_key{"$schema", just_accept}, | |||
| if_key{ | |||
| "name", | |||
| require_type<std::string>{"`name` must be a string"}, | |||
| put_into{ret.pkg_id.name}, | |||
| }, | |||
| if_key{ | |||
| "namespace", | |||
| require_type<std::string>{"`namespace` must be a string"}, | |||
| put_into{ret.namespace_}, | |||
| }, | |||
| if_key{ | |||
| "version", | |||
| require_type<std::string>{"`version` must be a string"}, | |||
| [&](auto&& version_str_) { | |||
| auto& version = version_str_.as_string(); | |||
| ret.pkg_id.version = semver::version::parse(version); | |||
| return json_read::accept_t{}; | |||
| return semester::dc_accept; | |||
| }, | |||
| "`version` must be a string")), | |||
| key("depends", object([&](auto key, auto&& range_str_) { | |||
| auto pkg_name = std::string(key); | |||
| if (!range_str_.is_string()) { | |||
| throw_user_error<errc::invalid_pkg_manifest>( | |||
| "Dependency for '{}' must be a range string", pkg_name); | |||
| } | |||
| try { | |||
| auto rng = semver::range::parse_restricted(range_str_.as_string()); | |||
| dependency dep{std::string(pkg_name), {rng.low(), rng.high()}}; | |||
| ret.dependencies.push_back(std::move(dep)); | |||
| } catch (const semver::invalid_range&) { | |||
| throw_user_error<errc::invalid_version_range_string>( | |||
| "Invalid version range string '{}' in dependency declaration for '{}'", | |||
| range_str_.as_string(), | |||
| pkg_name); | |||
| } | |||
| return json_read::accept_t{}; | |||
| })), | |||
| key("test_driver", | |||
| require_string( | |||
| [&](auto&& test_driver_str_) { | |||
| auto& test_driver = test_driver_str_.as_string(); | |||
| if (test_driver == "Catch-Main") { | |||
| ret.test_driver = test_lib::catch_main; | |||
| } else if (test_driver == "Catch") { | |||
| ret.test_driver = test_lib::catch_; | |||
| } else { | |||
| auto dym = *did_you_mean(test_driver, {"Catch-Main", "Catch"}); | |||
| throw_user_error<errc::unknown_test_driver>( | |||
| "Unknown 'test_driver' '{}' (Did you mean '{}'?)", | |||
| test_driver, | |||
| dym); | |||
| }, | |||
| if_key{ | |||
| "depends", | |||
| require_type<json5::data::mapping_type>{ | |||
| "`depends` must be a mapping between package names and version ranges"}, | |||
| mapping{[&](auto pkg_name, auto&& range_str_) { | |||
| if (!range_str_.is_string()) { | |||
| throw_user_error<errc::invalid_pkg_manifest>( | |||
| "Dependency for '{}' must be a range string", pkg_name); | |||
| } | |||
| return json_read::accept_t{}; | |||
| }, | |||
| "`test_driver` must be a valid test driver name string")), | |||
| reject_key)); | |||
| try { | |||
| auto rng = semver::range::parse_restricted(range_str_.as_string()); | |||
| dependency dep{std::string(pkg_name), {rng.low(), rng.high()}}; | |||
| ret.dependencies.push_back(std::move(dep)); | |||
| } catch (const semver::invalid_range&) { | |||
| throw_user_error<errc::invalid_version_range_string>( | |||
| "Invalid version range string '{}' in dependency declaration for " | |||
| "'{}'", | |||
| range_str_.as_string(), | |||
| pkg_name); | |||
| } | |||
| return semester::dc_accept; | |||
| }}, | |||
| }, | |||
| if_key{"test_driver", | |||
| require_type<std::string>{"`test_driver` must be a string"}, | |||
| [&](auto&& test_driver_str_) { | |||
| auto& test_driver = test_driver_str_.as_string(); | |||
| if (test_driver == "Catch-Main") { | |||
| ret.test_driver = test_lib::catch_main; | |||
| } else if (test_driver == "Catch") { | |||
| ret.test_driver = test_lib::catch_; | |||
| } else { | |||
| auto dym = *did_you_mean(test_driver, {"Catch-Main", "Catch"}); | |||
| throw_user_error<errc::unknown_test_driver>( | |||
| "Unknown 'test_driver' '{}' (Did you mean '{}'?)", | |||
| test_driver, | |||
| dym); | |||
| } | |||
| return semester::dc_accept; | |||
| }}, | |||
| [&](auto key, auto&&) { | |||
| return semester::dc_reject_t{ | |||
| fmt::format("Unknown key `{}` in package manifest", key)}; | |||
| }}}); | |||
| auto rej = std::get_if<semester::dc_reject_t>(&res); | |||
| if (rej) { | |||
| throw_user_error<errc::invalid_pkg_manifest>(rej->message); | |||
| } | |||
| if (ret.pkg_id.name.empty()) { | |||
| throw_user_error<errc::invalid_pkg_manifest>("The 'name' field is required."); | |||
| @@ -1,199 +0,0 @@ | |||
| #pragma once | |||
| #include <json5/data.hpp> | |||
| #include <tuple> | |||
| #include <variant> | |||
| namespace dds { | |||
| namespace json_read { | |||
| struct reject_t { | |||
| std::string message; | |||
| }; | |||
| struct accept_t {}; | |||
| struct pass_t {}; | |||
| using result_var = std::variant<reject_t, accept_t, pass_t>; | |||
| inline namespace ops { | |||
| struct reject { | |||
| std::string_view message; | |||
| result_var operator()(const json5::data&) const noexcept { | |||
| return reject_t{std::string(message)}; | |||
| } | |||
| }; | |||
| template <typename... Handlers> | |||
| struct then { | |||
| std::tuple<Handlers...> _hs; | |||
| explicit then(Handlers... hs) | |||
| : _hs(std::move(hs)...) {} | |||
| result_var _handle(const json5::data&) noexcept { return pass_t{}; } | |||
| template <typename Head, typename... Tail> | |||
| result_var _handle(const json5::data& dat, Head&& h, Tail&&... tail) { | |||
| result_var res = h(dat); | |||
| if (!std::holds_alternative<pass_t>(res)) { | |||
| return res; | |||
| } | |||
| return _handle(dat, tail...); | |||
| } | |||
| result_var operator()(const json5::data& dat) { | |||
| return std::apply([&](auto&&... hs) { return _handle(dat, hs...); }, _hs); | |||
| } | |||
| }; | |||
| template <typename... KeyHandlers> | |||
| struct object { | |||
| std::tuple<KeyHandlers...> _keys; | |||
| explicit object(KeyHandlers... ks) | |||
| : _keys(ks...) {} | |||
| result_var _handle(std::string_view, const json5::data&) { return pass_t{}; } | |||
| template <typename Head, typename... Tail> | |||
| result_var _handle(std::string_view key, const json5::data& dat, Head cur, Tail... ts) { | |||
| result_var current = cur(key, dat); | |||
| if (std::holds_alternative<pass_t>(current)) { | |||
| return _handle(key, dat, ts...); | |||
| } | |||
| return current; | |||
| } | |||
| result_var operator()(const json5::data& dat) { | |||
| if (!dat.is_object()) { | |||
| return pass_t{}; | |||
| } | |||
| for (const auto& [key, val] : dat.as_object()) { | |||
| result_var res | |||
| = std::apply([&](auto... ks) { return _handle(key, val, ks...); }, _keys); | |||
| if (std::holds_alternative<accept_t>(res)) { | |||
| continue; | |||
| } | |||
| if (std::holds_alternative<reject_t>(res)) { | |||
| return res; | |||
| } | |||
| } | |||
| return accept_t{}; | |||
| } | |||
| }; | |||
| template <typename Handler> | |||
| struct array_each { | |||
| Handler _hs; | |||
| result_var operator()(const json5::data& arr) { | |||
| if (!arr.is_array()) { | |||
| return pass_t{}; | |||
| } | |||
| for (const auto& elem : arr.as_array()) { | |||
| result_var res = _hs(elem); | |||
| if (std::holds_alternative<reject_t>(res)) { | |||
| return res; | |||
| } | |||
| } | |||
| return accept_t{}; | |||
| } | |||
| }; | |||
| template <typename Handler> | |||
| array_each(Handler) -> array_each<Handler>; | |||
| template <typename Handler> | |||
| struct key { | |||
| std::string_view _key; | |||
| Handler _handle; | |||
| key(std::string_view k, Handler h) | |||
| : _key(k) | |||
| , _handle(h) {} | |||
| result_var operator()(std::string_view key, const json5::data& dat) { | |||
| if (key == _key) { | |||
| return _handle(dat); | |||
| } | |||
| return pass_t{}; | |||
| } | |||
| }; | |||
| inline struct reject_key_t { | |||
| result_var operator()(std::string_view key, const json5::data&) const noexcept { | |||
| return reject_t{"The key `" + std::string(key) + "` is invalid"}; | |||
| } | |||
| } reject_key; | |||
| struct ignore_key { | |||
| std::string_view key; | |||
| result_var operator()(std::string_view key, const json5::data&) const noexcept { | |||
| if (key == this->key) { | |||
| return accept_t{}; | |||
| } | |||
| return pass_t{}; | |||
| } | |||
| }; | |||
| template <typename T, typename Handler> | |||
| struct accept_type { | |||
| Handler _handle; | |||
| result_var operator()(const json5::data& d) { | |||
| if (!d.is<T>()) { | |||
| return pass_t{}; | |||
| } | |||
| return _handle(d); | |||
| } | |||
| }; | |||
| template <typename T, typename Handler> | |||
| auto accept(Handler h) { | |||
| return accept_type<T, Handler>{h}; | |||
| } | |||
| template <typename H> | |||
| auto if_string(H h) { | |||
| return accept<std::string>(h); | |||
| } | |||
| template <typename H> | |||
| auto require_string(H h, std::string_view msg) { | |||
| return then(if_string(h), reject{msg}); | |||
| } | |||
| template <typename T> | |||
| struct put_into { | |||
| T& _dest; | |||
| result_var operator()(const json5::data& d) { | |||
| _dest = d.as<T>(); | |||
| return accept_t{}; | |||
| } | |||
| }; | |||
| template <typename T> | |||
| put_into(T) -> put_into<T>; | |||
| } // namespace ops | |||
| template <typename Handler> | |||
| auto decompose(const json5::data& dat, Handler&& h) { | |||
| result_var res = h(dat); | |||
| if (std::holds_alternative<reject_t>(res)) { | |||
| throw std::runtime_error(std::get<reject_t>(res).message); | |||
| } | |||
| } | |||
| } // namespace json_read | |||
| } // namespace dds | |||
| @@ -5,7 +5,7 @@ C++-Compiler: g++-9 | |||
| # Range-v3 0.10.0 contains an accidental conversion warning | |||
| Flags: -D SPDLOG_COMPILED_LIB -Werror=return-type -Wno-conversion | |||
| C++-Flags: -fconcepts | |||
| Link-Flags: -static-libgcc -static-libstdc++ | |||
| # Debug: True | |||
| Optimize: True | |||
| # Link-Flags: -static-libgcc -static-libstdc++ | |||
| Debug: True | |||
| #Optimize: True | |||
| Compiler-Launcher: ccache | |||