Bladeren bron

Declarative JSON data processing

default_compile_flags
vector-of-bool 4 jaren geleden
bovenliggende
commit
60b880fda6
2 gewijzigde bestanden met toevoegingen van 242 en 71 verwijderingen
  1. +58
    -71
      src/dds/package/manifest.cpp
  2. +184
    -0
      src/dds/util/json5_read.hpp

+ 58
- 71
src/dds/package/manifest.cpp Bestand weergeven

@@ -2,6 +2,7 @@

#include <dds/dym.hpp>
#include <dds/error/errors.hpp>
#include <dds/util/json5_read.hpp>
#include <dds/util/string.hpp>
#include <libman/parse.hpp>

@@ -80,80 +81,66 @@ package_manifest package_manifest::load_from_file(const fs::path& fpath) {
const auto& obj = data.as_object();
package_manifest ret;

/// Get the name
auto it = obj.find("name");
if (it == obj.end() || !it->second.is_string() || it->second.as_string().empty()) {
throw_user_error<errc::invalid_pkg_name>("'name' field in [{}] must be a non-empty string",
fpath.string());
}
ret.namespace_ = ret.pkg_id.name = it->second.as_string();

/// Get the version
it = obj.find("version");
if (it == obj.end() || !it->second.is_string()) {
throw_user_error<
errc::invalid_version_string>("'version' field in [{}] must be a version string",
fpath.string());
}
auto version_str = it->second.as_string();
ret.pkg_id.version = semver::version::parse(version_str);
using namespace j5_read::ops;
j5_read::destructure(
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(
[&](auto&& version_str_) {
auto& version = version_str_.as_string();
ret.pkg_id.version = semver::version::parse(version);
return j5_read::accept_t{};
},
"`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 j5_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);
}
return j5_read::accept_t{};
},
"`test_driver` must be a valid test driver name string")),
reject_key));

/// Get the namespace
it = obj.find("namespace");
if (it != obj.end()) {
if (!it->second.is_string() || it->second.as_string().empty()) {
throw_user_error<errc::invalid_pkg_manifest>(
"'namespace' attribute in [{}] must be a non-empty string", fpath.string());
}
ret.namespace_ = it->second.as_string();
}

/// Get the test driver
it = obj.find("test_driver");
if (it != obj.end()) {
if (!it->second.is_string()) {
throw_user_error<errc::invalid_pkg_manifest>(
"'test_driver' attribute in [{}] must be a non-empty string", fpath.string());
}
auto& test_driver_str = it->second.as_string();
if (test_driver_str == "Catch-Main") {
ret.test_driver = test_lib::catch_main;
} else if (test_driver_str == "Catch") {
ret.test_driver = test_lib::catch_;
} else {
auto dym = *did_you_mean(test_driver_str, {"Catch-Main", "Catch"});
throw_user_error<
errc::unknown_test_driver>("Unknown 'Test-Driver' '{}' (Did you mean '{}'?)",
test_driver_str,
dym);
}
if (ret.pkg_id.name.empty()) {
throw_user_error<errc::invalid_pkg_manifest>("The 'name' field is required.");
}

/// Get the dependencies
it = obj.find("depends");
if (it != obj.end()) {
if (!it->second.is_object()) {
throw_user_error<errc::invalid_pkg_manifest>(
"'depends' field must be an object mapping package name to version ranges");
}

for (const auto& [pkg_name, range_str_] : it->second.as_object()) {
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{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);
}
}
if (ret.namespace_.empty()) {
throw_user_error<errc::invalid_pkg_manifest>("The 'namespace'` field is required.");
}

return ret;

+ 184
- 0
src/dds/util/json5_read.hpp Bestand weergeven

@@ -0,0 +1,184 @@
#pragma once

#include <json5/data.hpp>

#include <tuple>
#include <variant>

namespace dds {

namespace j5_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... Hs>
then(Hs...) -> then<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... Ks>
object(Ks...) -> object<Ks...>;

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 destructure(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 j5_read

} // namespace dds

Laden…
Annuleren
Opslaan