#include "adminive/adapters/nlohmann_json.hpp" #include "adminive/http.hpp" #include "adminive_test.hpp" #include #include #include #include #include #include #include #include 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 count{3}; std::optional 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; struct Device_Config { Device device{Serial_Device{}}; }; struct Invalid_Name_Config { int value{}; }; struct Conflict_Device { std::string type; }; using Conflict_Variant = std::variant; struct Conflict_Config { Conflict_Variant device{Conflict_Device{}}; }; } namespace adminive { template <> struct Type_Descriptor { static auto get() { using T = safety_test::Child_Config; return object("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 { static auto get() { using T = safety_test::Root_Config; return object("root", "Root", ADMINIVE_FIELD(T, child).editable().creatable().required()); } }; template <> struct Type_Descriptor { static auto get() { using T = safety_test::Numeric_Config; return object("numeric", "Numeric", ADMINIVE_FIELD(T, signed_value).editable(), ADMINIVE_FIELD(T, unsigned_value).editable()); } }; template <> struct Type_Descriptor { static auto get() { using T = safety_test::Optional_Config; return object("optional", "Optional", ADMINIVE_FIELD(T, count).editable(), ADMINIVE_FIELD(T, note).editable()); } }; template <> struct Type_Descriptor { static auto get() { using T = safety_test::Multi_Error_Config; return object("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 { 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 { static auto get() { using T = safety_test::Runtime_Model; return object("runtime", "Runtime", ADMINIVE_FIELD(T, value).editable()); } }; template <> struct Type_Descriptor { static auto get() { using T = safety_test::Serial_Device; return object("serial", "Serial", ADMINIVE_FIELD(T, port).editable().creatable().required(), ADMINIVE_FIELD(T, baud_rate).editable().creatable().required()); } }; template <> struct Type_Descriptor { static auto get() { using T = safety_test::Network_Device; return object("network", "Network", ADMINIVE_FIELD(T, host).editable().creatable().required(), ADMINIVE_FIELD(T, port).editable().creatable().required()); } }; template <> struct Object_Adapter { 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 { static auto get() { using T = safety_test::Token_Device; return object("token", "Token", ADMINIVE_FIELD(T, token).editable().creatable().required()); } }; template <> struct Polymorphic_Adapter { 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("serial", "Serial"), polymorphic_variant("network", "Network"), polymorphic_variant("token", "Token")); } static safety_test::Json encode(const safety_test::Device& value) { return std::visit([](const auto& item) { safety_test::Json result = to_json(item); using T = std::remove_cvref_t; if constexpr(std::same_as) { result["type"] = "serial"; } else if constexpr(std::same_as) { 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(); if(type == "serial") { decode_polymorphic_alternative(target, value, context); return; } if(type == "network") { decode_polymorphic_alternative(target, value, context); return; } if(type == "token") { decode_polymorphic_alternative(target, value, context); return; } throw Field_Validation_Error("type", "unknown device type"); } }; template <> struct Type_Descriptor { static auto get() { using T = safety_test::Device_Config; return object("device_config", "Device Config", ADMINIVE_FIELD(T, device).editable()); } }; template <> struct Type_Descriptor { static auto get() { using T = safety_test::Invalid_Name_Config; return object("invalid_name", "Invalid Name", field<&T::value>("bad.name")); } }; template <> struct Type_Descriptor { static auto get() { using T = safety_test::Conflict_Device; return object("conflict_device", "Conflict Device", ADMINIVE_FIELD(T, type).editable().creatable()); } }; template <> struct Polymorphic_Adapter { 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("conflict", "Conflict")); } static safety_test::Json encode(const safety_test::Conflict_Variant& value) { safety_test::Json result = to_json(std::get(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(target, value, context); } }; template <> struct Type_Descriptor { static auto get() { using T = safety_test::Conflict_Config; return object("conflict_config", "Conflict Config", ADMINIVE_FIELD(T, device).editable()); } }; } int main() { using namespace safety_test; Root_Config root; auto update = adminive::apply_frontend_patch(root, Json{{"child", Json{{"editable_value", 8}}}}); ADMINIVE_CHECK(update.success); ADMINIVE_CHECK(root.child.editable_value == 8); ADMINIVE_CHECK(root.child.required_value == 3); update = adminive::apply_frontend_patch(root, Json{{"child", Json{{"locked_value", 9}}}}); ADMINIVE_CHECK(!update.success); ADMINIVE_CHECK(update.field_errors.contains("child.locked_value")); ADMINIVE_CHECK(root.child.locked_value == 2); Root_Config created; update = adminive::apply_frontend_create(created, Json{{"child", Json{{"editable_value", 5}}}}); ADMINIVE_CHECK(!update.success); ADMINIVE_CHECK(update.field_errors.contains("child.required_value")); const auto root_form = adminive::to_amis_form_schema(root, adminive::describe_edit_view()); ADMINIVE_CHECK(root_form.at("body").at(0).at("body").at(2).at("visibleOn") == "${child.editable_value > 0}"); Numeric_Config numeric; update = adminive::apply_frontend_patch(numeric, Json{{"signed_value", 1.5}}); ADMINIVE_CHECK(!update.success); update = adminive::apply_frontend_patch(numeric, Json{{"unsigned_value", -1}}); ADMINIVE_CHECK(!update.success); update = adminive::apply_frontend_patch(numeric, Json{{"signed_value", std::numeric_limits::max()}}); ADMINIVE_CHECK(!update.success); Optional_Config optional; const auto optional_form = adminive::to_amis_form_schema(optional, adminive::describe_edit_view()); ADMINIVE_CHECK(optional_form.at("body").at(0).at("clearable") == true); ADMINIVE_CHECK(optional_form.at("body").at(1).at("clearable") == true); update = adminive::apply_frontend_patch(optional, Json{{"count", nullptr}, {"note", nullptr}}); ADMINIVE_CHECK(update.success); ADMINIVE_CHECK(!optional.count); ADMINIVE_CHECK(!optional.note); Multi_Error_Config multi; update = adminive::apply_frontend_patch(multi, Json{{"first", -1}, {"second", -2}}); ADMINIVE_CHECK(!update.success); ADMINIVE_CHECK(update.field_errors.contains("first")); ADMINIVE_CHECK(update.field_errors.contains("second")); Runtime_Config runtime; bool rollback_called{}; adminive::Resource_Transaction 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&) { ADMINIVE_CHECK(original.value == 1); rollback_called = true; }; adminive::Resource_Service service(runtime, "/runtime", std::move(transaction)); const auto response = service.update_response(R"({"value":7})"); ADMINIVE_CHECK(response.status == 500); ADMINIVE_CHECK(runtime.value() == 1); ADMINIVE_CHECK(rollback_called); Device_Config devices; update = adminive::apply_frontend_patch(devices, Json{{"device", Json{{"type", "serial"}, {"baud_rate", 9600}}}}); ADMINIVE_CHECK(update.success); ADMINIVE_CHECK(std::get(devices.device).port == "COM1"); ADMINIVE_CHECK(std::get(devices.device).baud_rate == 9600); update = adminive::apply_frontend_patch(devices, Json{{"device", Json{{"type", "network"}, {"host", "10.0.0.1"}, {"port", 7000}}}}); ADMINIVE_CHECK(update.success); ADMINIVE_CHECK(std::holds_alternative(devices.device)); update = adminive::apply_frontend_patch(devices, Json{{"device", Json{{"type", "token"}, {"token", "abc"}}}}); ADMINIVE_CHECK(update.success); ADMINIVE_CHECK(std::holds_alternative(devices.device)); ADMINIVE_CHECK(std::get(devices.device).token == "abc"); bool invalid_name_rejected{}; try { static_cast(adminive::to_descriptor_json()); } catch(const std::invalid_argument&) { invalid_name_rejected = true; } ADMINIVE_CHECK(invalid_name_rejected); bool conflict_rejected{}; try { static_cast(adminive::to_descriptor_json()); } catch(const std::invalid_argument&) { conflict_rejected = true; } ADMINIVE_CHECK(conflict_rejected); return 0; }