diff options
-rw-r--r-- | Source/cmJSONHelpers.h | 130 | ||||
-rw-r--r-- | Tests/CMakeLib/testJSONHelpers.cxx | 73 |
2 files changed, 184 insertions, 19 deletions
diff --git a/Source/cmJSONHelpers.h b/Source/cmJSONHelpers.h index 0bb32f4..369c0b7 100644 --- a/Source/cmJSONHelpers.h +++ b/Source/cmJSONHelpers.h @@ -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); } -struct cmJSONHelperBuilder +#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; - } - T t; - if (!func(t, &(*value)[key], state)) { - success = false; +#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 } - 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> diff --git a/Tests/CMakeLib/testJSONHelpers.cxx b/Tests/CMakeLib/testJSONHelpers.cxx index 8dbfc5e..7de58bc 100644 --- a/Tests/CMakeLib/testJSONHelpers.cxx +++ b/Tests/CMakeLib/testJSONHelpers.cxx @@ -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>>>, + }); } |