Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
13 KB
Referenced Files
None
Subscribers
None
diff --git a/src/client/validator.hpp b/src/client/validator.hpp
new file mode 100644
index 0000000..28be8c4
--- /dev/null
+++ b/src/client/validator.hpp
@@ -0,0 +1,203 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <libkazv-config.hpp>
+
+#include <functional>
+
+#include <types.hpp>
+
+namespace Kazv
+{
+ /**
+ * Run validator against `input[k]` and add it to `ret`.
+ *
+ * A validator is a function taking a Jsonish value and
+ * return a pair of boolean (the status) and a Jsonish value (the output).
+ *
+ * If the status is true, the output will be added to `ret[k]`.
+ * If the status is false, the output will be ignored.
+ *
+ * Example:
+ * ```
+ * json j = json::object();
+ * Kazv::cast(
+ * j,
+ * json{{"foo", "1"}},
+ * "foo",
+ * [](const json &v) -> std::pair<bool, json> {
+ * return v.is_string()
+ * ? {true, v.template get<std::string>() + "x"}
+ * : {false, v};
+ * }
+ * ); // true, j = {{"foo", "1x"}}};
+ *
+ * json j2 = json::object();
+ * Kazv::cast(
+ * j,
+ * json{{"foo", 1}},
+ * "foo",
+ * [](const json &v) -> std::pair<bool, json> {
+ * return v.is_string()
+ * ? {true, v.template get<std::string>() + "x"}
+ * : {false, v};
+ * }
+ * ); // true, j2 = json::object();
+ * ```
+ *
+ * @param input The original json taken from user input.
+ * @param k The key (usually a property name of a json object) to process on.
+ * @param f The validator.
+ * @param ret The read-write reference of the returning json.
+ *
+ * @return Whether the cast is successful. The cast is successful if and only if
+ * the validator returns a status of true.
+ */
+ template<class Jsonish, class Key, class Validator>
+ bool cast(Jsonish &ret, const Jsonish &input, Key &&k, Validator &&f)
+ {
+ if (!input.contains(k)) {
+ return false;
+ }
+ auto [res, output] = std::invoke(std::forward<Validator>(f), input[k]);
+ if (!res) {
+ return false;
+ }
+ ret[k] = output;
+ return true;
+ }
+
+ /**
+ * Make an identity validator.
+ *
+ * The returned validator runs a predicate `f` against a value `val`.
+ * If the return value is true, return a pair of `true` and `val`.
+ * Otherwise, return a pair of `false` and `val`.
+ *
+ * @param f The predicate that judges the validity of a value.
+ * @return A validator.
+ */
+ template<class Func>
+ auto identValidate(Func &&f)
+ {
+ return [f=std::forward<Func>(f)](auto &&val) mutable {
+ auto res = std::invoke(std::forward<Func>(f), val);
+ return std::pair<bool, json>(res, std::forward<decltype(val)>(val));
+ };
+ }
+
+ /**
+ * Strategy used for castArray().
+ */
+ enum class CastArrayStrategy
+ {
+ /// If any item is invalid, ignore them and add all valid entries into the output.
+ IgnoreInvalid,
+ /// If any item is invalid, fail the entire cast.
+ FailAll,
+ };
+
+ /**
+ * Cast an array of items.
+ *
+ * Run the validator on each item of `input[k]` and accumulate the result
+ * into `ret`.
+ *
+ * Example:
+ * ```
+ * json j = json::object();
+ * Kazv::castArray(
+ * j,
+ * json{{"foo", {"1", "2", "3", 4}}},
+ * "foo",
+ * [](const json &v) -> std::pair<bool, json> {
+ * return v.is_string()
+ * ? {true, v.template get<std::string>() + "x"}
+ * : {false, v};
+ * },
+ * Kazv::CastArrayStrategy::IgnoreInvalid
+ * ); // true, j = {{"foo", {"1x", "2x", "3x"}}};
+ *
+ * json j2 = json::object();
+ * Kazv::castArray(
+ * j2,
+ * json{{"foo", {"1", "2", "3", 4}}},
+ * "foo",
+ * [](const json &v) -> std::pair<bool, json> {
+ * return v.is_string()
+ * ? {true, v.template get<std::string>() + "x"}
+ * : {false, v};
+ * },
+ * Kazv::CastArrayStrategy::FailAll
+ * ); // false, j2 = json::object();
+ * ```
+ *
+ * @param input The original json taken from user input.
+ * @param k The key (usually a property name of a json object) to process on.
+ * @param f The validator.
+ * @param ret The read-write reference of the returning json.
+ * @param strategy The strategy to use when handling invalid items.
+ * `CastArrayStrategy::IgnoreInvalid` makes the cast always successful
+ * (providing `input[k]` is an array), and all valid items will be added
+ * to `ret`.
+ * `CastArrayStrategy::FailAll` makes the cast successful only when all
+ * input items
+ *
+ * @return Whether the cast is successful. If `input[k]` is not an array,
+ * it be unsuccessful.
+ */
+ template<class Jsonish, class Key, class Validator>
+ bool castArray(Jsonish &ret, const Jsonish &input, Key &&k, Validator &&f, CastArrayStrategy strategy)
+ {
+ using JsonT = std::decay_t<Jsonish>;
+
+ if (!(input.contains(k) && input[k].is_array())) {
+ return false;
+ }
+
+ auto array = JsonT::array();
+
+ for (const auto &inputItem : input[k]) {
+ auto [res, outputItem] = std::invoke(f, inputItem);
+ if (!res) {
+ if (strategy == CastArrayStrategy::FailAll) {
+ return false;
+ }
+ } else {
+ array.push_back(std::move(outputItem));
+ }
+ }
+
+ ret[k] = std::move(array);
+
+ return true;
+ }
+
+ /**
+ * Replace a non-existent value with a default one.
+ *
+ * If `input` contains `k`, return `input` as-is.
+ * Otherwise return `input` but with `[k]` replaced with `defaultValue`.
+ *
+ * @param input The input value.
+ * @param k The key to process on.
+ * @param defaultValue The default value to replace with.
+ * @return `input` if `input` contains `k`, otherwise, `input` where `[k]`
+ * is replaced with `defaultValue`.
+ */
+ template<class Jsonish, class Key, class Jsonish2>
+ Jsonish makeDefaultValue(Jsonish &&input, Key &&k, Jsonish2 &&defaultValue)
+ {
+ if (input.contains(k)) {
+ return std::forward<Jsonish>(input);
+ } else {
+ Jsonish output = std::forward<Jsonish>(input);
+ output[std::forward<Key>(k)] = std::forward<Jsonish2>(defaultValue);
+ return output;
+ }
+ }
+}
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index b340e7a..1c50b44 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,74 +1,75 @@
include(CTest)
set(KAZVTEST_RESPATH ${CMAKE_CURRENT_SOURCE_DIR}/resources)
configure_file(kazvtest-respath.hpp.in kazvtest-respath.hpp)
add_executable(kazvtest
testmain.cpp
basejobtest.cpp
event-test.cpp
cursorutiltest.cpp
base/serialization-test.cpp
base/types-test.cpp
base/immer-utils-test.cpp
base/json-utils-test.cpp
client/client-test-util.cpp
client/discovery-test.cpp
client/sync-test.cpp
client/content-test.cpp
client/paginate-test.cpp
client/util-test.cpp
client/serialization-test.cpp
client/encrypted-file-test.cpp
client/sdk-test.cpp
client/thread-safety-test.cpp
client/room-test.cpp
client/random-generator-test.cpp
client/profile-test.cpp
client/kick-test.cpp
client/ban-test.cpp
client/join-test.cpp
client/keys-test.cpp
client/device-ops-test.cpp
client/send-test.cpp
client/encryption-test.cpp
client/redact-test.cpp
client/tagging-test.cpp
client/account-data-test.cpp
client/room/room-actions-test.cpp
client/room/local-echo-test.cpp
client/room/event-relationships-test.cpp
client/push-rules-desc-test.cpp
client/notification-handler-test.cpp
+ client/validator-test.cpp
kazvjobtest.cpp
event-emitter-test.cpp
crypto-test.cpp
crypto/deterministic-test.cpp
promise-test.cpp
store-test.cpp
file-desc-test.cpp
)
target_include_directories(
kazvtest
PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/client)
target_compile_definitions(kazvtest PRIVATE CATCH_CONFIG_ENABLE_ALL_STRINGMAKERS)
target_link_libraries(kazvtest
PRIVATE Catch2::Catch2WithMain
PRIVATE kazv
PRIVATE kazveventemitter
PRIVATE kazvjob
PRIVATE nlohmann_json::nlohmann_json
PRIVATE immer
PRIVATE lager
PRIVATE zug
PRIVATE kazvtestfixtures
)
add_test(NAME kazvtest COMMAND kazvtest "~[needs-internet]")
diff --git a/src/tests/client/validator-test.cpp b/src/tests/client/validator-test.cpp
new file mode 100644
index 0000000..55c1f49
--- /dev/null
+++ b/src/tests/client/validator-test.cpp
@@ -0,0 +1,169 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <validator.hpp>
+
+using namespace Kazv;
+
+static auto exampleJson = json{
+ {"a", "b"},
+ {"c", json::object({})},
+ {"d", json::array({1, 2, 3})},
+ {"e", {
+ {"f", "g"},
+ {"h", 1},
+ }},
+};
+
+TEST_CASE("cast()", "[client][validator]")
+{
+ auto result = json::object();
+
+ SECTION("when it passes the validator")
+ {
+ auto r = cast(result, exampleJson, "a",
+ [](const auto &) { return std::make_pair(true, "mew"); }
+ );
+ REQUIRE(r);
+ REQUIRE(result.at("a") == "mew");
+ }
+
+ SECTION("when it does not pass the validator")
+ {
+ auto r = cast(result, exampleJson, "a",
+ [](const auto &j) { return std::make_pair(false, j); }
+ );
+ REQUIRE(!r);
+ REQUIRE(!result.contains("a"));
+ }
+
+ SECTION("when the thing does not exist in input")
+ {
+ auto r = cast(result, exampleJson, "mewmewmew",
+ [](const auto &j) { return std::make_pair(true, j); }
+ );
+ REQUIRE(!r);
+ REQUIRE(!result.contains("mewmewmew"));
+ }
+}
+
+TEST_CASE("identValidate()", "[client][validator]")
+{
+ auto validator = identValidate([](const auto &j) {
+ return j.is_string();
+ });
+
+ REQUIRE(validator(exampleJson.at("a")) == std::make_pair(true, exampleJson.at("a")));
+ REQUIRE(std::get<0>(validator(exampleJson.at("c"))) == false);
+}
+
+TEST_CASE("castArray()", "[client][validator]")
+{
+ auto result = json::object();
+
+ SECTION("when all inputs pass the validator")
+ {
+ auto r = castArray(result, exampleJson, "d",
+ identValidate(&json::is_number), CastArrayStrategy::FailAll);
+ REQUIRE(r);
+ REQUIRE(result.at("d") == exampleJson.at("d"));
+
+ r = castArray(result, exampleJson, "d",
+ identValidate(&json::is_number), CastArrayStrategy::IgnoreInvalid);
+ REQUIRE(r);
+ REQUIRE(result.at("d") == exampleJson.at("d"));
+ }
+
+ SECTION("when some input does not pass the validator")
+ {
+ auto v = [](const json &j) -> std::pair<bool, json> {
+ return {
+ j.is_number() && j.template get<int>() % 2 == 0,
+ j.template get<int>() * 2,
+ };
+ };
+
+ auto r = castArray(result, exampleJson, "d",
+ v, CastArrayStrategy::FailAll);
+ REQUIRE(!r);
+ REQUIRE(!result.contains("d"));
+
+ r = castArray(result, exampleJson, "d",
+ v, CastArrayStrategy::IgnoreInvalid);
+ REQUIRE(r);
+ REQUIRE(result.at("d") == json::array({4}));
+ }
+
+ SECTION("when no input passes the validator")
+ {
+ auto pred = [](const json &) {
+ return false;
+ };
+
+ auto r = castArray(result, exampleJson, "d",
+ identValidate(pred), CastArrayStrategy::FailAll);
+ REQUIRE(!r);
+ REQUIRE(!result.contains("d"));
+
+ r = castArray(result, exampleJson, "d",
+ identValidate(pred), CastArrayStrategy::IgnoreInvalid);
+ REQUIRE(r);
+ REQUIRE(result.at("d") == json::array());
+ }
+
+ SECTION("when input is not an array")
+ {
+ auto pred = [](const json &) {
+ return true;
+ };
+
+ auto r = castArray(result, exampleJson, "c",
+ identValidate(pred), CastArrayStrategy::FailAll);
+ REQUIRE(!r);
+ REQUIRE(!result.contains("c"));
+
+ r = castArray(result, exampleJson, "c",
+ identValidate(pred), CastArrayStrategy::IgnoreInvalid);
+ REQUIRE(!r);
+ REQUIRE(!result.contains("c"));
+ }
+
+ SECTION("when input does not exist")
+ {
+ auto pred = [](const json &) {
+ return true;
+ };
+
+ auto r = castArray(result, exampleJson, "mewmewmew",
+ identValidate(pred), CastArrayStrategy::FailAll);
+ REQUIRE(!r);
+ REQUIRE(!result.contains("mewmewmew"));
+
+ r = castArray(result, exampleJson, "mewmewmew",
+ identValidate(pred), CastArrayStrategy::IgnoreInvalid);
+ REQUIRE(!r);
+ REQUIRE(!result.contains("mewmewmew"));
+ }
+}
+
+TEST_CASE("makeDefaultValue()", "[client][validator]")
+{
+ SECTION("if input contains k")
+ {
+ REQUIRE(makeDefaultValue(exampleJson, "c", "foo") == exampleJson);
+ }
+
+ SECTION("if input does not contain k")
+ {
+ auto expected = exampleJson;
+ expected["vvv"] = "foo";
+ REQUIRE(makeDefaultValue(exampleJson, "vvv", "foo") == expected);
+ }
+}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 2, 10:06 AM (54 m, 2 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1804381
Default Alt Text
(13 KB)

Event Timeline