328 lines
12 KiB
C++
328 lines
12 KiB
C++
#include "adminive/adapters/boost_pfr.hpp"
|
|
#include "adminive/adapters/nlohmann_json.hpp"
|
|
#include "adminive/http.hpp"
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <variant>
|
|
namespace advanced_test {
|
|
using Json = nlohmann::json;
|
|
struct Nested_Config {
|
|
int count{1};
|
|
};
|
|
struct Runtime_Model {
|
|
Nested_Config nested;
|
|
std::string name{"default"};
|
|
};
|
|
class Runtime_Config {
|
|
public:
|
|
Runtime_Config() = default;
|
|
Runtime_Config(const Runtime_Config&) = delete;
|
|
Runtime_Config& operator=(const Runtime_Config&) = delete;
|
|
int count() const noexcept {
|
|
return count_;
|
|
}
|
|
const std::string& name() const noexcept {
|
|
return name_;
|
|
}
|
|
void apply(const Runtime_Model& value) {
|
|
count_ = value.nested.count;
|
|
name_ = value.name;
|
|
++apply_count_;
|
|
}
|
|
int apply_count() const noexcept {
|
|
return apply_count_;
|
|
}
|
|
private:
|
|
int count_{1};
|
|
int apply_count_{};
|
|
std::string name_{"default"};
|
|
};
|
|
struct Base_A {
|
|
int alpha{3};
|
|
};
|
|
struct Base_B {
|
|
std::string beta{"base"};
|
|
};
|
|
struct Inherited_Config : Base_A, Base_B {};
|
|
struct Color_Code {
|
|
std::string value{"#112233"};
|
|
};
|
|
struct Control_Config {
|
|
Color_Code color;
|
|
};
|
|
struct Serial_Device {
|
|
std::string port{"COM1"};
|
|
int baud_rate{115200};
|
|
Color_Code color;
|
|
};
|
|
struct Network_Device {
|
|
std::string host{"127.0.0.1"};
|
|
int port{9000};
|
|
};
|
|
using Device = std::variant<Serial_Device, Network_Device>;
|
|
struct Device_Config {
|
|
Device device{Serial_Device{}};
|
|
};
|
|
struct Overview_Status {
|
|
adminive::Status_Value<std::string> state{"running", "#16a34a"};
|
|
int workers{4};
|
|
};
|
|
struct Secret_Config {
|
|
std::string secret{"token"};
|
|
std::string visible{"public"};
|
|
};
|
|
struct Duplicate_Config {
|
|
int first{};
|
|
int second{};
|
|
};
|
|
struct Invalid_Sort_Config {
|
|
int value{};
|
|
};
|
|
}
|
|
namespace adminive {
|
|
template <>
|
|
struct Boost_Pfr_Name_Adapter<advanced_test::Base_A> {
|
|
template <std::size_t Index>
|
|
static constexpr std::string_view name() noexcept {
|
|
static_assert(Index == 0);
|
|
return "alpha";
|
|
}
|
|
};
|
|
template <>
|
|
struct Boost_Pfr_Name_Adapter<advanced_test::Base_B> {
|
|
template <std::size_t Index>
|
|
static constexpr std::string_view name() noexcept {
|
|
static_assert(Index == 0);
|
|
return "beta";
|
|
}
|
|
};
|
|
template <>
|
|
struct Object_Adapter<advanced_test::Runtime_Config> {
|
|
using model_type = advanced_test::Runtime_Model;
|
|
static model_type snapshot(const advanced_test::Runtime_Config& value) {
|
|
model_type result;
|
|
result.nested.count = value.count();
|
|
result.name = value.name();
|
|
return result;
|
|
}
|
|
static void commit(advanced_test::Runtime_Config& target, model_type value) {
|
|
target.apply(value);
|
|
}
|
|
};
|
|
template <>
|
|
struct Reflection_Adapter<advanced_test::Inherited_Config> : Boost_Pfr_Base_Reflection_Adapter<advanced_test::Inherited_Config, advanced_test::Base_A, advanced_test::Base_B> {};
|
|
template <class Json>
|
|
struct Value_Adapter<advanced_test::Color_Code, Json> {
|
|
using value_type = std::string;
|
|
static const std::string& read(const advanced_test::Color_Code& value) noexcept {
|
|
return value.value;
|
|
}
|
|
static void write(advanced_test::Color_Code& target, std::string value) {
|
|
target.value = std::move(value);
|
|
}
|
|
};
|
|
template <>
|
|
struct Control_Adapter<advanced_test::Color_Code, advanced_test::Json> {
|
|
static advanced_test::Json make_control(const advanced_test::Json& field, Control_Context context) {
|
|
advanced_test::Json result;
|
|
result["type"] = "input-color";
|
|
result["name"] = make_field_name(context.prefix, field.at("name").get<std::string>());
|
|
result["label"] = field.at("label");
|
|
result["clearable"] = true;
|
|
return result;
|
|
}
|
|
static advanced_test::Json make_column(const advanced_test::Json& field) {
|
|
return advanced_test::Json{{"type", "tpl"}, {"name", field.at("name")}, {"label", field.at("list_label")}, {"tpl", "<span style=\"color:${color}\">${color}</span>"}};
|
|
}
|
|
};
|
|
template <>
|
|
struct Type_Descriptor<advanced_test::Nested_Config> {
|
|
static auto get() {
|
|
using T = advanced_test::Nested_Config;
|
|
return object<T>("nested", "Nested", ADMINIVE_FIELD(T, count).editable()).validator([](const T& value) {
|
|
if(value.count < 1) {
|
|
throw Field_Validation_Error("count", "count must be positive");
|
|
}
|
|
});
|
|
}
|
|
};
|
|
template <>
|
|
struct Type_Descriptor<advanced_test::Runtime_Model> {
|
|
static auto get() {
|
|
using T = advanced_test::Runtime_Model;
|
|
return object<T>("runtime", "Runtime", ADMINIVE_FIELD(T, nested).editable(), ADMINIVE_FIELD(T, name).editable());
|
|
}
|
|
};
|
|
template <>
|
|
struct Type_Descriptor<advanced_test::Inherited_Config> {
|
|
static auto get() {
|
|
using T = advanced_test::Inherited_Config;
|
|
return reflected_object<T>("inherited", "Inherited");
|
|
}
|
|
};
|
|
template <>
|
|
struct Type_Descriptor<advanced_test::Control_Config> {
|
|
static auto get() {
|
|
using T = advanced_test::Control_Config;
|
|
return object<T>("control", "Control", ADMINIVE_FIELD(T, color).editable().list_label("颜色"));
|
|
}
|
|
};
|
|
template <>
|
|
struct Type_Descriptor<advanced_test::Serial_Device> {
|
|
static auto get() {
|
|
using T = advanced_test::Serial_Device;
|
|
return object<T>("serial", "Serial", ADMINIVE_FIELD(T, port).editable().creatable(), ADMINIVE_FIELD(T, baud_rate).editable().creatable(), ADMINIVE_FIELD(T, color).editable().creatable());
|
|
}
|
|
};
|
|
template <>
|
|
struct Type_Descriptor<advanced_test::Network_Device> {
|
|
static auto get() {
|
|
using T = advanced_test::Network_Device;
|
|
return object<T>("network", "Network", ADMINIVE_FIELD(T, host).editable().creatable(), ADMINIVE_FIELD(T, port).editable().creatable());
|
|
}
|
|
};
|
|
template <>
|
|
struct Polymorphic_Adapter<advanced_test::Device, advanced_test::Json> {
|
|
static constexpr std::string_view discriminator() noexcept {
|
|
return "type";
|
|
}
|
|
static constexpr std::string_view discriminator_label() noexcept {
|
|
return "设备类型";
|
|
}
|
|
static auto variants() {
|
|
return std::tuple(
|
|
polymorphic_variant<advanced_test::Serial_Device>("serial", "串口设备"),
|
|
polymorphic_variant<advanced_test::Network_Device>("network", "网络设备")
|
|
);
|
|
}
|
|
static advanced_test::Json encode(const advanced_test::Device& value) {
|
|
return std::visit([](const auto& device) {
|
|
advanced_test::Json result = to_json<advanced_test::Json>(device);
|
|
using T = std::remove_cvref_t<decltype(device)>;
|
|
result["type"] = std::same_as<T, advanced_test::Serial_Device> ? "serial" : "network";
|
|
return result;
|
|
}, value);
|
|
}
|
|
static void decode(advanced_test::Device& target, const advanced_test::Json& value, Write_Context context) {
|
|
const std::string type = value.at("type").get<std::string>();
|
|
if(type == "serial") {
|
|
decode_polymorphic_alternative<advanced_test::Json, advanced_test::Serial_Device>(target, value, context);
|
|
return;
|
|
}
|
|
if(type == "network") {
|
|
decode_polymorphic_alternative<advanced_test::Json, advanced_test::Network_Device>(target, value, context);
|
|
return;
|
|
}
|
|
throw Field_Validation_Error("type", "unknown device type");
|
|
}
|
|
};
|
|
template <>
|
|
struct Type_Descriptor<advanced_test::Device_Config> {
|
|
static auto get() {
|
|
using T = advanced_test::Device_Config;
|
|
return object<T>("devices", "Devices", ADMINIVE_FIELD(T, device).editable());
|
|
}
|
|
};
|
|
template <>
|
|
struct Type_Descriptor<advanced_test::Overview_Status> {
|
|
static auto get() {
|
|
using T = advanced_test::Overview_Status;
|
|
return object<T>("overview", "运行状态", ADMINIVE_FIELD(T, state).label("服务状态"), ADMINIVE_FIELD(T, workers).label("工作线程"));
|
|
}
|
|
};
|
|
template <>
|
|
struct Type_Descriptor<advanced_test::Secret_Config> {
|
|
static auto get() {
|
|
using T = advanced_test::Secret_Config;
|
|
return object<T>("secret", "Secret", ADMINIVE_FIELD(T, secret).sensitive().editable(), ADMINIVE_FIELD(T, visible).editable());
|
|
}
|
|
};
|
|
template <>
|
|
struct Type_Descriptor<advanced_test::Duplicate_Config> {
|
|
static auto get() {
|
|
using T = advanced_test::Duplicate_Config;
|
|
return object<T>("duplicate", "Duplicate", field<&T::first>("same"), field<&T::second>("same"));
|
|
}
|
|
};
|
|
template <>
|
|
struct Type_Descriptor<advanced_test::Invalid_Sort_Config> {
|
|
static auto get() {
|
|
using T = advanced_test::Invalid_Sort_Config;
|
|
return object<T>("invalid_sort", "Invalid Sort", ADMINIVE_FIELD(T, value)).default_sort("value");
|
|
}
|
|
};
|
|
}
|
|
int main() {
|
|
using namespace advanced_test;
|
|
Runtime_Config runtime;
|
|
int transaction_count{};
|
|
adminive::Resource_Transaction<Runtime_Model> transaction;
|
|
transaction.prepare = [](const Runtime_Model& candidate, const adminive::Request_Context&) {
|
|
if(candidate.name == "blocked") {
|
|
throw adminive::Field_Validation_Error("name", "name is blocked");
|
|
}
|
|
};
|
|
transaction.commit = [&transaction_count](const Runtime_Model&, const adminive::Request_Context&) {
|
|
++transaction_count;
|
|
};
|
|
adminive::Resource_Service<Runtime_Config, Json> service(runtime, "/config", std::move(transaction));
|
|
const auto invalid = service.update_response(R"({"nested":{"count":0}})");
|
|
assert(invalid.status == 422);
|
|
assert(invalid.body.at("field_errors").contains("nested.count"));
|
|
const auto rejected_commit = service.update_response(R"({"name":"blocked"})");
|
|
assert(rejected_commit.status == 422);
|
|
assert(rejected_commit.body.at("field_errors").at("name") == "name is blocked");
|
|
assert(runtime.name() == "default");
|
|
const auto valid = service.update_response(R"({"nested":{"count":8},"name":"updated"})");
|
|
assert(valid.status == 200);
|
|
assert(runtime.count() == 8);
|
|
assert(runtime.name() == "updated");
|
|
assert(runtime.apply_count() == 1);
|
|
assert(transaction_count == 1);
|
|
const auto inherited = adminive::to_descriptor_json<Json, Inherited_Config>();
|
|
assert(inherited.at("fields").at(0).at("name") == "alpha");
|
|
assert(inherited.at("fields").at(1).at("name") == "beta");
|
|
Control_Config controls;
|
|
const auto control_form = adminive::to_amis_form_schema<Json>(controls);
|
|
assert(control_form.at("body").at(0).at("type") == "input-color");
|
|
Device_Config devices;
|
|
const auto device_form = adminive::to_amis_form_schema<Json>(devices);
|
|
assert(device_form.at("body").at(0).at("type") == "fieldset");
|
|
assert(device_form.at("body").at(0).at("body").at(0).at("type") == "select");
|
|
assert(device_form.at("body").at(0).at("body").at(1).at("body").at(2).at("type") == "input-color");
|
|
const auto polymorphic_patch = Json{{"device", Json{{"type", "network"}, {"host", "10.0.0.1"}, {"port", 7000}, {"baud_rate", 9600}}}};
|
|
const auto polymorphic_update = adminive::apply_frontend_patch<Json>(devices, polymorphic_patch);
|
|
assert(polymorphic_update.success);
|
|
assert(std::holds_alternative<Network_Device>(devices.device));
|
|
assert(std::get<Network_Device>(devices.device).host == "10.0.0.1");
|
|
const auto status_descriptor = adminive::to_status_descriptor_json<Json, Overview_Status>();
|
|
const auto status_service = adminive::make_amis_status_service<Json>(status_descriptor, "/status", 1000);
|
|
assert(status_service.at("body").at("title") == "运行状态");
|
|
assert(status_service.at("body").at("body").at("body").size() == 2);
|
|
const auto descriptor_without_defaults = adminive::to_descriptor_json<Json, Secret_Config>();
|
|
assert(!descriptor_without_defaults.at("fields").at(0).contains("default"));
|
|
const auto descriptor_with_defaults = adminive::to_descriptor_json_with_defaults<Json, Secret_Config>();
|
|
assert(!descriptor_with_defaults.at("fields").at(0).contains("default"));
|
|
assert(descriptor_with_defaults.at("fields").at(1).at("default") == "public");
|
|
Secret_Config secret;
|
|
const auto frontend_secret = adminive::to_frontend_json<Json>(secret);
|
|
assert(!frontend_secret.contains("secret"));
|
|
assert(frontend_secret.at("visible") == "public");
|
|
bool duplicate_rejected{};
|
|
try {
|
|
static_cast<void>(adminive::to_descriptor_json<Json, Duplicate_Config>());
|
|
} catch(const std::invalid_argument&) {
|
|
duplicate_rejected = true;
|
|
}
|
|
assert(duplicate_rejected);
|
|
bool invalid_sort_rejected{};
|
|
try {
|
|
static_cast<void>(adminive::to_descriptor_json<Json, Invalid_Sort_Config>());
|
|
} catch(const std::invalid_argument&) {
|
|
invalid_sort_rejected = true;
|
|
}
|
|
assert(invalid_sort_rejected);
|
|
return 0;
|
|
}
|