Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85833136
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
13 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline