cmJSONHelpers.h: Add FilteredObject helper

Iterate over the object's members and call a filter callable to decide what
to do with the current key/value. A filter returns one of the `FilterResult`
values. A container type is an associative or a sequence container of pairs
(key, value).

Refactor `MapFilter()` and `Map()` to use `FilteredObject()`. Moreover,
for C++ >= 17 implementation is more optimized depending on the given filter
object type and capable of detecting and properly calling the filter callable
using 1 or 3 arguments, up to totally eliminate any checking (even dummy)
in the generated code.

Supported container types, used to append key/value items, aren't limited to
`std::map` only and can be any associative container or a sequenced one with
pairs of key/value as elements.
This commit is contained in:
Alex Turbov
2024-08-14 20:15:34 +04:00
parent cd4210eb6d
commit 0c17cc9ef5
2 changed files with 184 additions and 19 deletions

View File

@@ -7,8 +7,10 @@
#include <algorithm>
#include <functional>
#include <iostream>
#include <iterator>
#include <map>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
@@ -60,9 +62,28 @@ ErrorGenerator INVALID_NAMED_OBJECT_KEY(
ObjectError errorType, const Json::Value::Members& extraFields);
}
#if __cplusplus >= 201703L
namespace details {
// A meta-function to check if a given callable type
// can be called with the only string ref arg.
template <typename F, typename Enable = void>
struct is_bool_filter
{
static constexpr bool value = false;
};
template <typename F>
struct is_bool_filter<F,
std::enable_if_t<std::is_same_v<
std::invoke_result_t<F, const std::string&>, bool>>>
{
static constexpr bool value = true;
};
}
#endif
struct cmJSONHelperBuilder
{
template <typename T>
class Object
{
@@ -323,13 +344,28 @@ struct cmJSONHelperBuilder
[](const T&) { return true; });
}
template <typename T, typename F, typename Filter>
static cmJSONHelper<std::map<std::string, T>> MapFilter(
enum class FilterResult
{
Continue, ///< A filter has accepted a given key (and value)
Skip, ///< A filter has rejected a given key (or value)
Error ///< A filter has found and reported an error
};
/// Iterate over the object's members and call a filter callable to
/// decide what to do with the current key/value.
/// A filter returns one of the `FilterResult` values.
/// A container type is an associative or a sequence
/// container of pairs (key, value).
template <typename Container, typename F, typename Filter>
static cmJSONHelper<Container> FilteredObject(
const JsonErrors::ErrorGenerator& error, F func, Filter filter)
{
return [error, func, filter](std::map<std::string, T>& out,
const Json::Value* value,
return [error, func, filter](Container& out, const Json::Value* value,
cmJSONState* state) -> bool {
// NOTE Some compile-time code path don't use `filter` at all.
// So, suppress "unused lambda capture" warning is needed.
static_cast<void>(filter);
if (!value) {
out.clear();
return true;
@@ -339,30 +375,94 @@ struct cmJSONHelperBuilder
return false;
}
out.clear();
auto outIt = std::inserter(out, out.end());
bool success = true;
for (auto const& key : value->getMemberNames()) {
state->push_stack(key, &(*value)[key]);
if (!filter(key)) {
state->pop_stack();
continue;
#if __cplusplus >= 201703L
if constexpr (std::is_same_v<Filter, std::true_type>) {
// Filtering functionality isn't needed at all...
} else if constexpr (details::is_bool_filter<Filter>::value) {
// A given `Filter` is `bool(const std::string&)` callable.
if (!filter(key)) {
state->pop_stack();
continue;
}
} else {
#endif
// A full-featured `Filter` has been given
auto res = filter(key, &(*value)[key], state);
if (res == FilterResult::Skip) {
state->pop_stack();
continue;
}
if (res == FilterResult::Error) {
state->pop_stack();
success = false;
break;
}
#if __cplusplus >= 201703L
}
T t;
if (!func(t, &(*value)[key], state)) {
success = false;
}
out.emplace(key, std::move(t));
#endif
typename Container::value_type::second_type t;
// ATTENTION Call the function first (for it's side-effects),
// then accumulate the result!
success = func(t, &(*value)[key], state) && success;
outIt = typename Container::value_type{ key, std::move(t) };
state->pop_stack();
}
return success;
};
}
template <typename T, typename F, typename Filter>
static cmJSONHelper<std::map<std::string, T>> MapFilter(
const JsonErrors::ErrorGenerator& error, F func, Filter filter)
{
// clang-format off
return FilteredObject<std::map<std::string, T>>(
error, func,
#if __cplusplus >= 201703L
// In C++ 17 a filter callable can be passed as is.
// Depending on its type `FilteredObject()` will call
// it with a key only (backward compatible behavior)
// or with 3 args supported by the full-featured
// filtering feature.
filter
#else
// For C++14 and below, to keep backward compatibility
// with CMake Presets code, `MapFilter()` can accept only
// `bool(const std::string&)` callables.
[filter](const std::string &key, const Json::Value * /*value*/,
cmJSONState * /*state*/) -> FilterResult {
// Simple adaptor to translate `bool` to `FilterResult`
return filter(key) ? FilterResult::Continue : FilterResult::Skip;
}
#endif
);
// clang-format on
}
template <typename T, typename F>
static cmJSONHelper<std::map<std::string, T>> Map(
const JsonErrors::ErrorGenerator& error, F func)
{
return MapFilter<T, F>(error, func,
[](const std::string&) { return true; });
// clang-format off
return FilteredObject<std::map<std::string, T>>(
error, func,
#if __cplusplus >= 201703L
// With C++ 17 and above, pass a marker type, that no
// filtering is needed at all.
std::true_type()
#else
// In C++ 14 and below, pass an always-true dummy functor.
[](const std::string& /*key*/, const Json::Value* /*value*/,
cmJSONState* /*state*/) -> FilterResult {
return FilterResult::Continue;
}
#endif
);
// clang-format on
}
template <typename T, typename F>

View File

@@ -1,5 +1,8 @@
#include <list>
#include <map>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include <cm/optional>
@@ -36,6 +39,8 @@ ErrorGenerator ErrorGeneratorBuilder(std::string errorMessage)
ErrorGenerator InvalidArray = ErrorGeneratorBuilder("Invalid Array");
ErrorGenerator MissingRequired = ErrorGeneratorBuilder("Missing Required");
ErrorGenerator InvalidMap = ErrorGeneratorBuilder("Invalid Map");
ErrorGenerator FaultyObjectProperty =
ErrorGeneratorBuilder("Faulty Object Property");
ErrorGenerator InvalidObject(JsonErrors::ObjectError /*errorType*/,
const Json::Value::Members& extraFields)
{
@@ -460,12 +465,72 @@ bool testRequired()
return true;
}
JSONHelperBuilder::FilterResult ObjectPropsFilter(const std::string& key,
const Json::Value* value,
cmJSONState* state)
{
if (key == "ignore") {
return JSONHelperBuilder::FilterResult::Skip;
}
if (value->isString() && value->asString() == "ignore") {
return JSONHelperBuilder::FilterResult::Skip;
}
if (key == "zerror") {
ErrorMessages::FaultyObjectProperty(value, state);
return JSONHelperBuilder::FilterResult::Error;
}
return JSONHelperBuilder::FilterResult::Continue;
}
template <typename Container>
bool testFilteredObject()
{
auto const FilteredObjectHelper =
JSONHelperBuilder::FilteredObject<Container>(
ErrorMessages::InvalidMap, StringHelper, ObjectPropsFilter);
Json::Value v(Json::objectValue);
cmJSONState state;
v["field1"] = "Hello";
v["field2"] = "world!";
v["ignore"] = "any";
v["any"] = "ignore";
v["zerror"] = "error";
Container m{ { "key", "default" } };
Container expected{ { "field1", "Hello" }, { "field2", "world!" } };
ASSERT_TRUE(!FilteredObjectHelper(m, &v, &state));
ASSERT_TRUE(m == expected);
auto error = state.GetErrorMessage();
ASSERT_TRUE(error == "\nFaulty Object Property");
return true;
}
}
int testJSONHelpers(int /*unused*/, char* /*unused*/[])
{
return runTests({ testInt, testUInt, testBool, testString, testObject,
testObjectInherited, testObjectNoExtra, testObjectOptional,
testVector, testVectorFilter, testMap, testMapFilter,
testOptional, testRequired });
return runTests({
testInt,
testUInt,
testBool,
testString,
testObject,
testObjectInherited,
testObjectNoExtra,
testObjectOptional,
testVector,
testVectorFilter,
testMap,
testMapFilter,
testOptional,
testRequired,
testFilteredObject<std::map<std::string, std::string>>,
testFilteredObject<std::unordered_map<std::string, std::string>>,
testFilteredObject<std::vector<std::pair<std::string, std::string>>>,
testFilteredObject<std::list<std::pair<std::string, std::string>>>,
});
}