chore(metadata-requirements): support folly::dynamic requirements

This commit is contained in:
Marcus Holland-Moritz 2023-12-14 09:10:38 +01:00
parent a2e44d13d5
commit c005b841c0
5 changed files with 315 additions and 8 deletions

View File

@ -619,6 +619,11 @@ if(WITH_TESTS)
target_link_libraries(block_merger_test gtest gtest_main)
list(APPEND TEST_TARGETS block_merger_test)
add_executable(dwarfs_metadata_requirements_test test/metadata_requirements_test.cpp)
target_link_libraries(dwarfs_metadata_requirements_test test_helpers
gtest gtest_main gmock_main)
list(APPEND TEST_TARGETS dwarfs_metadata_requirements_test)
add_executable(dwarfs_pcm_sample_transformer_test test/pcm_sample_transformer_test.cpp)
target_link_libraries(dwarfs_pcm_sample_transformer_test gtest gtest_main)
list(APPEND TEST_TARGETS dwarfs_pcm_sample_transformer_test)

View File

@ -22,6 +22,7 @@
#pragma once
#include <functional>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
@ -61,7 +62,8 @@ bool parse_metadata_requirements_set(T& container, folly::dynamic& req,
if (it->second[1].type() != folly::dynamic::ARRAY) {
throw std::runtime_error(
fmt::format("non-array type argument for requirement '{}'", name));
fmt::format("non-array type argument for requirement '{}', got '{}'",
name, it->second[1].typeName()));
}
for (auto v : it->second[1]) {
@ -138,6 +140,21 @@ class checked_metadata_requirement_base : public metadata_requirement_base {
virtual void check(Meta const& m) const = 0;
};
class dynamic_metadata_requirement_base {
public:
virtual ~dynamic_metadata_requirement_base() = default;
dynamic_metadata_requirement_base(std::string const& name)
: name_{name} {}
virtual void check(folly::dynamic const& m) const = 0;
std::string_view name() const { return name_; }
private:
std::string const name_;
};
template <typename Meta, typename T, typename U>
class typed_metadata_requirement_base
: public checked_metadata_requirement_base<Meta> {
@ -188,8 +205,9 @@ class metadata_requirement_set
protected:
void check_value(T const& value) const override {
if (set_ && set_->count(value) == 0) {
throw std::range_error(fmt::format("{} '{}' does not meet requirements",
this->name(), value));
throw std::range_error(
fmt::format("{} '{}' does not meet requirements [{}]", this->name(),
value, fmt::join(*set_, ", ")));
}
}
@ -288,4 +306,17 @@ class compression_metadata_requirements<void> {
}
};
template <>
class compression_metadata_requirements<folly::dynamic> {
public:
compression_metadata_requirements(std::string const& req);
compression_metadata_requirements(folly::dynamic const& req);
void check(std::string const& meta) const;
void check(folly::dynamic const& meta) const;
private:
std::vector<std::unique_ptr<detail::dynamic_metadata_requirement_base>> req_;
};
} // namespace dwarfs

View File

@ -21,16 +21,21 @@
#include <algorithm>
#include <folly/json.h>
#include "dwarfs/compression_metadata_requirements.h"
namespace dwarfs::detail {
namespace dwarfs {
namespace detail {
void check_dynamic_common(folly::dynamic const& dyn,
std::string_view expected_type, size_t expected_size,
std::string_view name) {
if (dyn.type() != folly::dynamic::ARRAY) {
throw std::runtime_error(
fmt::format("found non-array type for requirement '{}'", name));
fmt::format("found non-array type for requirement '{}', got type '{}'",
name, dyn.typeName()));
}
if (dyn.empty()) {
throw std::runtime_error(
@ -56,8 +61,181 @@ void check_unsupported_metadata_requirements(folly::dynamic& req) {
}
std::sort(keys.begin(), keys.end());
throw std::runtime_error(fmt::format(
"unsupported metadata requirements: {}", folly::join(", ", keys)));
"unsupported metadata requirements: {}", fmt::join(keys, ", ")));
}
}
} // namespace dwarfs::detail
template <typename T>
class dynamic_metadata_requirement_set
: public dynamic_metadata_requirement_base {
public:
static_assert(std::is_same_v<T, std::string> || std::is_integral_v<T>);
dynamic_metadata_requirement_set(std::string const& name,
folly::dynamic const& req)
: dynamic_metadata_requirement_base{name} {
auto tmp = req;
if (!parse_metadata_requirements_set(set_, tmp, name,
detail::value_parser<T>)) {
throw std::runtime_error(
fmt::format("could not parse set requirement '{}'", name));
}
}
void check(folly::dynamic const& dyn) const override {
if constexpr (std::is_same_v<T, std::string>) {
if (!dyn.isString()) {
throw std::runtime_error(
fmt::format("non-string type for requirement '{}', got type '{}'",
name(), dyn.typeName()));
}
if (set_.find(dyn.asString()) == set_.end()) {
throw std::runtime_error(
fmt::format("{} '{}' does not meet requirements [{}]", name(),
dyn.asString(), fmt::join(ordered_set(), ", ")));
}
} else {
if (!dyn.isInt()) {
throw std::runtime_error(
fmt::format("non-integral type for requirement '{}', got type '{}'",
name(), dyn.typeName()));
}
if (set_.find(dyn.asInt()) == set_.end()) {
throw std::runtime_error(
fmt::format("{} '{}' does not meet requirements [{}]", name(),
dyn.asInt(), fmt::join(ordered_set(), ", ")));
}
}
}
private:
std::vector<T> ordered_set() const {
std::vector<T> result;
result.reserve(set_.size());
std::copy(set_.begin(), set_.end(), std::back_inserter(result));
std::sort(result.begin(), result.end());
return result;
}
std::unordered_set<T> set_;
};
class dynamic_metadata_requirement_range
: public dynamic_metadata_requirement_base {
public:
dynamic_metadata_requirement_range(std::string const& name,
folly::dynamic const& req)
: dynamic_metadata_requirement_base{name} {
auto tmp = req;
if (!parse_metadata_requirements_range(min_, max_, tmp, name,
detail::value_parser<int64_t>)) {
throw std::runtime_error(
fmt::format("could not parse range requirement '{}'", name));
}
}
void check(folly::dynamic const& dyn) const override {
if (!dyn.isInt()) {
throw std::runtime_error(
fmt::format("non-integral type for requirement '{}', got type '{}'",
name(), dyn.typeName()));
}
auto v = dyn.asInt();
if (v < min_ || v > max_) {
throw std::runtime_error(
fmt::format("{} '{}' does not meet requirements [{}, {}]", name(), v,
min_, max_));
}
}
private:
int64_t min_, max_;
};
} // namespace detail
compression_metadata_requirements<
folly::dynamic>::compression_metadata_requirements(std::string const& req)
: compression_metadata_requirements(folly::parseJson(req)) {}
compression_metadata_requirements<folly::dynamic>::
compression_metadata_requirements(folly::dynamic const& req) {
if (req.type() != folly::dynamic::OBJECT) {
throw std::runtime_error(
fmt::format("metadata requirements must be an object, got type '{}'",
req.typeName()));
}
for (auto const& [k, v] : req.items()) {
if (v.type() != folly::dynamic::ARRAY) {
throw std::runtime_error(
fmt::format("requirement '{}' must be an array, got type '{}'",
k.asString(), v.typeName()));
}
if (v.size() < 2) {
throw std::runtime_error(
fmt::format("requirement '{}' must be an array of at least 2 "
"elements, got only {}",
k.asString(), v.size()));
}
if (v[0].type() != folly::dynamic::STRING) {
throw std::runtime_error(fmt::format(
"type for requirement '{}' must be a string, got type '{}'",
k.asString(), v[0].typeName()));
}
if (v[0].asString() == "set") {
if (v[1].type() != folly::dynamic::ARRAY) {
throw std::runtime_error(fmt::format(
"set for requirement '{}' must be an array, got type '{}'",
k.asString(), v[1].typeName()));
}
if (v[1].empty()) {
throw std::runtime_error(fmt::format(
"set for requirement '{}' must not be empty", k.asString()));
}
if (v[1][0].isString()) {
req_.emplace_back(
std::make_unique<
detail::dynamic_metadata_requirement_set<std::string>>(
k.asString(), req));
} else {
req_.emplace_back(
std::make_unique<detail::dynamic_metadata_requirement_set<int64_t>>(
k.asString(), req));
}
} else if (v[0].asString() == "range") {
req_.emplace_back(
std::make_unique<detail::dynamic_metadata_requirement_range>(
k.asString(), req));
} else {
throw std::runtime_error(
fmt::format("unsupported requirement type '{}'", v[0].asString()));
}
}
}
void compression_metadata_requirements<folly::dynamic>::check(
folly::dynamic const& dyn) const {
for (auto const& r : req_) {
if (auto it = dyn.find(r->name()); it != dyn.items().end()) {
r->check(it->second);
} else {
throw std::runtime_error(
fmt::format("missing requirement '{}'", r->name()));
}
}
}
void compression_metadata_requirements<folly::dynamic>::check(
std::string const& metadata) const {
check(folly::parseJson(metadata));
}
} // namespace dwarfs

View File

@ -0,0 +1,93 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/**
* \author Marcus Holland-Moritz (github@mhxnet.de)
* \copyright Copyright (c) Marcus Holland-Moritz
*
* This file is part of dwarfs.
*
* dwarfs is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* dwarfs is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with dwarfs. If not, see <https://www.gnu.org/licenses/>.
*/
#include <stdexcept>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "dwarfs/compression_metadata_requirements.h"
using namespace dwarfs;
TEST(metadata_requirements, dynamic_test) {
std::string requirements = R"({
"compression": ["set", ["lz4", "zstd"]],
"block_size": ["range", 16, 1024]
})";
std::unique_ptr<compression_metadata_requirements<folly::dynamic>> req;
ASSERT_NO_THROW(
req = std::make_unique<compression_metadata_requirements<folly::dynamic>>(
requirements));
{
std::string metadata = R"({
"compression": "lz4",
"block_size": 256
})";
EXPECT_NO_THROW(req->check(metadata));
}
{
std::string metadata = R"({
"compression": "lz4",
"foo": "bar",
"block_size": 256
})";
EXPECT_NO_THROW(req->check(metadata));
}
{
std::string metadata = R"({
"compression": "lzma",
"block_size": 256
})";
EXPECT_THAT(
[&]() { req->check(metadata); },
ThrowsMessage<std::runtime_error>(testing::HasSubstr(
"compression 'lzma' does not meet requirements [lz4, zstd]")));
}
{
std::string metadata = R"({
"block_size": 256
})";
EXPECT_THAT([&]() { req->check(metadata); },
ThrowsMessage<std::runtime_error>(
testing::HasSubstr("missing requirement 'compression'")));
}
{
std::string metadata = R"({
"compression": "zstd",
"block_size": 8
})";
EXPECT_THAT([&]() { req->check(metadata); },
ThrowsMessage<std::runtime_error>(testing::HasSubstr(
"block_size '8' does not meet requirements [16, 1024]")));
}
}

View File

@ -85,7 +85,7 @@ TEST(pcmaudio_categorizer, requirements) {
EXPECT_THAT(
ent.output,
MatchesRegex(
R"(^\[WAV\] ".*": endianness 'little' does not meet requirements$)"));
R"(^\[WAV\] ".*": endianness 'little' does not meet requirements \[\]$)"));
EXPECT_TRUE(frag.empty());