Files
Adminive/backend/tests/safety_test.cpp
T
2026-08-07 00:01:16 +08:00

344 lines
13 KiB
C++

#include "adminive/adapters/nlohmann_json.hpp"
#include "adminive/http.hpp"
#include <cassert>
#include <cstdint>
#include <limits>
#include <optional>
#include <stdexcept>
#include <string>
#include <tuple>
#include <utility>
#include <variant>
namespace safety_test {
using Json = nlohmann::json;
struct Child_Config {
int editable_value{1};
int locked_value{2};
int required_value{3};
};
struct Root_Config {
Child_Config child;
};
struct Numeric_Config {
int signed_value{};
unsigned int unsigned_value{};
};
struct Optional_Config {
std::optional<int> count{3};
std::optional<std::string> note{"value"};
};
struct Multi_Error_Config {
int first{1};
int second{1};
};
struct Runtime_Model {
int value{1};
};
class Runtime_Config {
public:
Runtime_Config() = default;
Runtime_Config(const Runtime_Config&) = delete;
Runtime_Config& operator=(const Runtime_Config&) = delete;
int value() const noexcept {
return value_;
}
void apply(const Runtime_Model& model) {
value_ = model.value;
}
private:
int value_{1};
};
struct Serial_Device {
std::string port{"COM1"};
int baud_rate{115200};
};
struct Network_Device {
std::string host{"127.0.0.1"};
int port{9000};
};
class Token_Device {
public:
explicit Token_Device(std::string value) : token(std::move(value)) {}
std::string token;
};
using Device = std::variant<Serial_Device, Network_Device, Token_Device>;
struct Device_Config {
Device device{Serial_Device{}};
};
struct Invalid_Name_Config {
int value{};
};
struct Conflict_Device {
std::string type;
};
using Conflict_Variant = std::variant<Conflict_Device>;
struct Conflict_Config {
Conflict_Variant device{Conflict_Device{}};
};
}
namespace adminive {
template <>
struct Type_Descriptor<safety_test::Child_Config> {
static auto get() {
using T = safety_test::Child_Config;
return object<T>("child", "Child", ADMINIVE_FIELD(T, editable_value).editable().creatable().required(), ADMINIVE_FIELD(T, locked_value), ADMINIVE_FIELD(T, required_value).editable().creatable().required().visible_on("${$self.editable_value > 0}"));
}
};
template <>
struct Type_Descriptor<safety_test::Root_Config> {
static auto get() {
using T = safety_test::Root_Config;
return object<T>("root", "Root", ADMINIVE_FIELD(T, child).editable().creatable().required());
}
};
template <>
struct Type_Descriptor<safety_test::Numeric_Config> {
static auto get() {
using T = safety_test::Numeric_Config;
return object<T>("numeric", "Numeric", ADMINIVE_FIELD(T, signed_value).editable(), ADMINIVE_FIELD(T, unsigned_value).editable());
}
};
template <>
struct Type_Descriptor<safety_test::Optional_Config> {
static auto get() {
using T = safety_test::Optional_Config;
return object<T>("optional", "Optional", ADMINIVE_FIELD(T, count).editable(), ADMINIVE_FIELD(T, note).editable());
}
};
template <>
struct Type_Descriptor<safety_test::Multi_Error_Config> {
static auto get() {
using T = safety_test::Multi_Error_Config;
return object<T>("multi_error", "Multi Error", ADMINIVE_FIELD(T, first).editable(), ADMINIVE_FIELD(T, second).editable()).validator([](const T& value, Validation_Context& context) {
if(value.first < 0) {
context.error("first", "first must not be negative");
}
if(value.second < 0) {
context.error("second", "second must not be negative");
}
});
}
};
template <>
struct Object_Adapter<safety_test::Runtime_Config> {
using model_type = safety_test::Runtime_Model;
static model_type snapshot(const safety_test::Runtime_Config& value) {
return model_type{value.value()};
}
static void commit(safety_test::Runtime_Config& target, const model_type& value) {
target.apply(value);
}
};
template <>
struct Type_Descriptor<safety_test::Runtime_Model> {
static auto get() {
using T = safety_test::Runtime_Model;
return object<T>("runtime", "Runtime", ADMINIVE_FIELD(T, value).editable());
}
};
template <>
struct Type_Descriptor<safety_test::Serial_Device> {
static auto get() {
using T = safety_test::Serial_Device;
return object<T>("serial", "Serial", ADMINIVE_FIELD(T, port).editable().creatable().required(), ADMINIVE_FIELD(T, baud_rate).editable().creatable().required());
}
};
template <>
struct Type_Descriptor<safety_test::Network_Device> {
static auto get() {
using T = safety_test::Network_Device;
return object<T>("network", "Network", ADMINIVE_FIELD(T, host).editable().creatable().required(), ADMINIVE_FIELD(T, port).editable().creatable().required());
}
};
template <>
struct Object_Adapter<safety_test::Token_Device> {
using model_type = safety_test::Token_Device;
static model_type snapshot(const safety_test::Token_Device& value) {
return value;
}
static model_type create() {
return model_type("default-token");
}
static void commit(safety_test::Token_Device& target, model_type value) {
target = std::move(value);
}
};
template <>
struct Type_Descriptor<safety_test::Token_Device> {
static auto get() {
using T = safety_test::Token_Device;
return object<T>("token", "Token", ADMINIVE_FIELD(T, token).editable().creatable().required());
}
};
template <>
struct Polymorphic_Adapter<safety_test::Device, safety_test::Json> {
static constexpr std::string_view discriminator() noexcept {
return "type";
}
static constexpr std::string_view discriminator_label() noexcept {
return "Device Type";
}
static auto variants() {
return std::tuple(polymorphic_variant<safety_test::Serial_Device>("serial", "Serial"), polymorphic_variant<safety_test::Network_Device>("network", "Network"), polymorphic_variant<safety_test::Token_Device>("token", "Token"));
}
static safety_test::Json encode(const safety_test::Device& value) {
return std::visit([](const auto& item) {
safety_test::Json result = to_json<safety_test::Json>(item);
using T = std::remove_cvref_t<decltype(item)>;
if constexpr(std::same_as<T, safety_test::Serial_Device>) {
result["type"] = "serial";
} else if constexpr(std::same_as<T, safety_test::Network_Device>) {
result["type"] = "network";
} else {
result["type"] = "token";
}
return result;
}, value);
}
static void decode(safety_test::Device& target, const safety_test::Json& value, Write_Context context) {
const std::string type = value.at("type").get<std::string>();
if(type == "serial") {
decode_polymorphic_alternative<safety_test::Json, safety_test::Serial_Device>(target, value, context);
return;
}
if(type == "network") {
decode_polymorphic_alternative<safety_test::Json, safety_test::Network_Device>(target, value, context);
return;
}
if(type == "token") {
decode_polymorphic_alternative<safety_test::Json, safety_test::Token_Device>(target, value, context);
return;
}
throw Field_Validation_Error("type", "unknown device type");
}
};
template <>
struct Type_Descriptor<safety_test::Device_Config> {
static auto get() {
using T = safety_test::Device_Config;
return object<T>("device_config", "Device Config", ADMINIVE_FIELD(T, device).editable());
}
};
template <>
struct Type_Descriptor<safety_test::Invalid_Name_Config> {
static auto get() {
using T = safety_test::Invalid_Name_Config;
return object<T>("invalid_name", "Invalid Name", field<&T::value>("bad.name"));
}
};
template <>
struct Type_Descriptor<safety_test::Conflict_Device> {
static auto get() {
using T = safety_test::Conflict_Device;
return object<T>("conflict_device", "Conflict Device", ADMINIVE_FIELD(T, type).editable().creatable());
}
};
template <>
struct Polymorphic_Adapter<safety_test::Conflict_Variant, safety_test::Json> {
static constexpr std::string_view discriminator() noexcept {
return "type";
}
static constexpr std::string_view discriminator_label() noexcept {
return "Type";
}
static auto variants() {
return std::tuple(polymorphic_variant<safety_test::Conflict_Device>("conflict", "Conflict"));
}
static safety_test::Json encode(const safety_test::Conflict_Variant& value) {
safety_test::Json result = to_json<safety_test::Json>(std::get<safety_test::Conflict_Device>(value));
result["type"] = "conflict";
return result;
}
static void decode(safety_test::Conflict_Variant& target, const safety_test::Json& value, Write_Context context) {
decode_polymorphic_alternative<safety_test::Json, safety_test::Conflict_Device>(target, value, context);
}
};
template <>
struct Type_Descriptor<safety_test::Conflict_Config> {
static auto get() {
using T = safety_test::Conflict_Config;
return object<T>("conflict_config", "Conflict Config", ADMINIVE_FIELD(T, device).editable());
}
};
}
int main() {
using namespace safety_test;
Root_Config root;
auto update = adminive::apply_frontend_patch<Json>(root, Json{{"child", Json{{"editable_value", 8}}}});
assert(update.success);
assert(root.child.editable_value == 8);
assert(root.child.required_value == 3);
update = adminive::apply_frontend_patch<Json>(root, Json{{"child", Json{{"locked_value", 9}}}});
assert(!update.success);
assert(update.field_errors.contains("child.locked_value"));
assert(root.child.locked_value == 2);
Root_Config created;
update = adminive::apply_frontend_create<Json>(created, Json{{"child", Json{{"editable_value", 5}}}});
assert(!update.success);
assert(update.field_errors.contains("child.required_value"));
const auto root_form = adminive::to_amis_form_schema<Json>(root);
assert(root_form.at("body").at(0).at("body").at(2).at("visibleOn") == "${child.editable_value > 0}");
Numeric_Config numeric;
update = adminive::apply_frontend_patch<Json>(numeric, Json{{"signed_value", 1.5}});
assert(!update.success);
update = adminive::apply_frontend_patch<Json>(numeric, Json{{"unsigned_value", -1}});
assert(!update.success);
update = adminive::apply_frontend_patch<Json>(numeric, Json{{"signed_value", std::numeric_limits<std::uint64_t>::max()}});
assert(!update.success);
Optional_Config optional;
const auto optional_form = adminive::to_amis_form_schema<Json>(optional);
assert(optional_form.at("body").at(0).at("clearable") == true);
assert(optional_form.at("body").at(1).at("clearable") == true);
update = adminive::apply_frontend_patch<Json>(optional, Json{{"count", nullptr}, {"note", nullptr}});
assert(update.success);
assert(!optional.count);
assert(!optional.note);
Multi_Error_Config multi;
update = adminive::apply_frontend_patch<Json>(multi, Json{{"first", -1}, {"second", -2}});
assert(!update.success);
assert(update.field_errors.contains("first"));
assert(update.field_errors.contains("second"));
Runtime_Config runtime;
bool rollback_called{};
adminive::Resource_Transaction<Runtime_Model> transaction;
transaction.commit = [](const Runtime_Model&, const adminive::Request_Context&) {
throw std::runtime_error("external commit failed");
};
transaction.rollback = [&rollback_called](const Runtime_Model& original, const adminive::Request_Context&) {
assert(original.value == 1);
rollback_called = true;
};
adminive::Resource_Service<Runtime_Config, Json> service(runtime, "/runtime", std::move(transaction));
const auto response = service.update_response(R"({"value":7})");
assert(response.status == 500);
assert(runtime.value() == 1);
assert(rollback_called);
Device_Config devices;
update = adminive::apply_frontend_patch<Json>(devices, Json{{"device", Json{{"type", "serial"}, {"baud_rate", 9600}}}});
assert(update.success);
assert(std::get<Serial_Device>(devices.device).port == "COM1");
assert(std::get<Serial_Device>(devices.device).baud_rate == 9600);
update = adminive::apply_frontend_patch<Json>(devices, Json{{"device", Json{{"type", "network"}, {"host", "10.0.0.1"}, {"port", 7000}}}});
assert(update.success);
assert(std::holds_alternative<Network_Device>(devices.device));
update = adminive::apply_frontend_patch<Json>(devices, Json{{"device", Json{{"type", "token"}, {"token", "abc"}}}});
assert(update.success);
assert(std::holds_alternative<Token_Device>(devices.device));
assert(std::get<Token_Device>(devices.device).token == "abc");
bool invalid_name_rejected{};
try {
static_cast<void>(adminive::to_descriptor_json<Json, Invalid_Name_Config>());
} catch(const std::invalid_argument&) {
invalid_name_rejected = true;
}
assert(invalid_name_rejected);
bool conflict_rejected{};
try {
static_cast<void>(adminive::to_descriptor_json<Json, Conflict_Config>());
} catch(const std::invalid_argument&) {
conflict_rejected = true;
}
assert(conflict_rejected);
return 0;
}