332 lines
13 KiB
C++
332 lines
13 KiB
C++
#include "adminive/adapters/nlohmann_json.hpp"
|
|
#include "adminive/http.hpp"
|
|
#include "adminive_test.hpp"
|
|
#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}}}});
|
|
ADMINIVE_CHECK(update.success);
|
|
ADMINIVE_CHECK(root.child.editable_value == 8);
|
|
ADMINIVE_CHECK(root.child.required_value == 3);
|
|
update = adminive::apply_frontend_patch<Json>(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<Json>(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<Json>(root, adminive::describe_edit_view<Root_Config>());
|
|
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<Json>(numeric, Json{{"signed_value", 1.5}});
|
|
ADMINIVE_CHECK(!update.success);
|
|
update = adminive::apply_frontend_patch<Json>(numeric, Json{{"unsigned_value", -1}});
|
|
ADMINIVE_CHECK(!update.success);
|
|
update = adminive::apply_frontend_patch<Json>(numeric, Json{{"signed_value", std::numeric_limits<std::uint64_t>::max()}});
|
|
ADMINIVE_CHECK(!update.success);
|
|
Optional_Config optional;
|
|
const auto optional_form = adminive::to_amis_form_schema<Json>(optional, adminive::describe_edit_view<Optional_Config>());
|
|
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<Json>(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<Json>(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<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&) {
|
|
ADMINIVE_CHECK(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})");
|
|
ADMINIVE_CHECK(response.status == 500);
|
|
ADMINIVE_CHECK(runtime.value() == 1);
|
|
ADMINIVE_CHECK(rollback_called);
|
|
Device_Config devices;
|
|
update = adminive::apply_frontend_patch<Json>(devices, Json{{"device", Json{{"type", "serial"}, {"baud_rate", 9600}}}});
|
|
ADMINIVE_CHECK(update.success);
|
|
ADMINIVE_CHECK(std::get<Serial_Device>(devices.device).port == "COM1");
|
|
ADMINIVE_CHECK(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}}}});
|
|
ADMINIVE_CHECK(update.success);
|
|
ADMINIVE_CHECK(std::holds_alternative<Network_Device>(devices.device));
|
|
update = adminive::apply_frontend_patch<Json>(devices, Json{{"device", Json{{"type", "token"}, {"token", "abc"}}}});
|
|
ADMINIVE_CHECK(update.success);
|
|
ADMINIVE_CHECK(std::holds_alternative<Token_Device>(devices.device));
|
|
ADMINIVE_CHECK(std::get<Token_Device>(devices.device).token == "abc");
|
|
ADMINIVE_CHECK_THROWS_AS((adminive::to_descriptor_json<Json, Invalid_Name_Config>()), std::invalid_argument);
|
|
ADMINIVE_CHECK_THROWS_AS((adminive::to_descriptor_json<Json, Conflict_Config>()), std::invalid_argument);
|
|
return 0;
|
|
}
|