分离 json magic_enum 依赖

This commit is contained in:
2026-08-06 20:33:28 +08:00
parent 21eacf2f26
commit f060662ffd
16 changed files with 2094 additions and 762 deletions
+27 -36
View File
@@ -5,45 +5,36 @@ set(adminive_backend_root "${CMAKE_CURRENT_LIST_DIR}")
set(adminive_include_dir "${adminive_backend_root}/include")
set(adminive_third_party_include_dir "${adminive_backend_root}/third_party/include")
set(adminive_frontend_dist_dir "${adminive_backend_root}/../frontend_dist")
set(adminive_config_file "${adminive_backend_root}/../config/adminive.json")
file(TO_CMAKE_PATH "${adminive_frontend_dist_dir}" adminive_frontend_dist_dir)
file(TO_CMAKE_PATH "${adminive_config_file}" adminive_config_file)
add_library(Adminive INTERFACE)
if(MSVC)
target_compile_options(Adminive INTERFACE /utf-8 /permissive- /Zc:__cplusplus)
endif()
add_library(Adminive::Adminive ALIAS Adminive)
target_include_directories(Adminive INTERFACE
"${adminive_include_dir}"
"${adminive_third_party_include_dir}"
)
add_library(Adminive::Core ALIAS Adminive)
target_include_directories(Adminive INTERFACE "${adminive_include_dir}")
target_compile_features(Adminive INTERFACE cxx_std_20)
if(WIN32)
target_compile_definitions(Adminive INTERFACE
WINVER=0x0A00
_WIN32_WINNT=0x0A00
)
target_link_libraries(Adminive INTERFACE ws2_32)
endif()
set(adminive_server_sources
"${adminive_backend_root}/src/config_store.cpp"
"${adminive_backend_root}/src/device_tables.cpp"
"${adminive_backend_root}/src/example.cpp"
"${adminive_backend_root}/src/main.cpp"
"${adminive_backend_root}/src/server_app.cpp"
)
add_executable(Adminive_Server ${adminive_server_sources})
target_include_directories(Adminive_Server PRIVATE "${adminive_backend_root}/src")
target_compile_definitions(Adminive_Server PRIVATE
ADMINIVE_FRONTEND_DIST_DIR="${adminive_frontend_dist_dir}"
ADMINIVE_CONFIG_FILE="${adminive_config_file}"
)
target_link_libraries(Adminive_Server PRIVATE Adminive::Adminive)
if(BUILD_TESTING)
add_executable(Adminive_Test
"${adminive_backend_root}/src/config_store.cpp"
"${adminive_backend_root}/src/device_tables.cpp"
"${adminive_backend_root}/src/example.cpp"
"${adminive_backend_root}/tests/test.cpp"
)
add_library(Adminive_Http INTERFACE)
add_library(Adminive::Http ALIAS Adminive_Http)
target_link_libraries(Adminive_Http INTERFACE Adminive::Core)
target_include_directories(Adminive_Http INTERFACE "${adminive_third_party_include_dir}")
if (WIN32)
target_compile_definitions(Adminive_Http INTERFACE WINVER=0x0A00 _WIN32_WINNT=0x0A00)
target_link_libraries(Adminive_Http INTERFACE ws2_32)
endif ()
add_library(Adminive_Default INTERFACE)
add_library(Adminive::Default ALIAS Adminive_Default)
target_link_libraries(Adminive_Default INTERFACE Adminive::Http)
target_include_directories(Adminive_Default INTERFACE "${adminive_third_party_include_dir}")
add_executable(Adminive_Server "${adminive_backend_root}/src/main.cpp")
target_compile_definitions(Adminive_Server PRIVATE ADMINIVE_FRONTEND_DIST_DIR="${adminive_frontend_dist_dir}")
target_link_libraries(Adminive_Server PRIVATE Adminive::Default)
if (BUILD_TESTING)
add_executable(Adminive_Test "${adminive_backend_root}/tests/test.cpp")
target_include_directories(Adminive_Test PRIVATE "${adminive_backend_root}/src")
target_link_libraries(Adminive_Test PRIVATE Adminive::Adminive)
target_link_libraries(Adminive_Test PRIVATE Adminive::Default)
add_test(NAME Adminive_Test COMMAND Adminive_Test)
endif()
add_executable(Adminive_Core_Adapter_Test "${adminive_backend_root}/tests/core_adapter_test.cpp")
target_link_libraries(Adminive_Core_Adapter_Test PRIVATE Adminive::Core)
add_test(NAME Adminive_Core_Adapter_Test COMMAND Adminive_Core_Adapter_Test)
endif ()
+154
View File
@@ -0,0 +1,154 @@
#pragma once
#include <concepts>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
namespace adminive {
template <class T>
struct Reflection_Adapter;
template <class Json>
struct Json_Adapter;
template <class Json>
concept Json_Type = requires(Json value, const Json const_value, std::string key, std::string_view text, std::size_t index) {
{ Json_Adapter<Json>::object() } -> std::same_as<Json>;
{ Json_Adapter<Json>::array() } -> std::same_as<Json>;
{ Json_Adapter<Json>::parse(text) } -> std::same_as<Json>;
{ Json_Adapter<Json>::dump(const_value, 2) } -> std::same_as<std::string>;
{ Json_Adapter<Json>::is_object(const_value) } -> std::same_as<bool>;
{ Json_Adapter<Json>::is_array(const_value) } -> std::same_as<bool>;
{ Json_Adapter<Json>::is_string(const_value) } -> std::same_as<bool>;
{ Json_Adapter<Json>::is_number(const_value) } -> std::same_as<bool>;
{ Json_Adapter<Json>::is_boolean(const_value) } -> std::same_as<bool>;
{ Json_Adapter<Json>::number(const_value) } -> std::convertible_to<long double>;
{ Json_Adapter<Json>::contains(const_value, text) } -> std::same_as<bool>;
{ Json_Adapter<Json>::size(const_value) } -> std::convertible_to<std::size_t>;
{ Json_Adapter<Json>::keys(const_value) } -> std::same_as<std::vector<std::string>>;
{ Json_Adapter<Json>::at(value, text) } -> std::same_as<Json&>;
{ Json_Adapter<Json>::at(const_value, text) } -> std::same_as<const Json&>;
{ Json_Adapter<Json>::at(value, index) } -> std::same_as<Json&>;
{ Json_Adapter<Json>::at(const_value, index) } -> std::same_as<const Json&>;
Json_Adapter<Json>::set(value, text, Json{});
Json_Adapter<Json>::append(value, Json{});
Json_Adapter<Json>::erase(value, text);
};
template <class Json>
Json json_object() {
return Json_Adapter<Json>::object();
}
template <class Json>
Json json_array() {
return Json_Adapter<Json>::array();
}
template <class Json, class T>
Json json_scalar(T&& value) {
return Json_Adapter<Json>::make(std::forward<T>(value));
}
template <class Json, class T>
void json_set(Json& object, std::string_view name, T&& value) {
if constexpr(std::same_as<std::remove_cvref_t<T>, Json>) {
Json_Adapter<Json>::set(object, name, std::forward<T>(value));
} else {
Json_Adapter<Json>::set(object, name, json_scalar<Json>(std::forward<T>(value)));
}
}
template <class Json, class T>
void json_append(Json& array, T&& value) {
if constexpr(std::same_as<std::remove_cvref_t<T>, Json>) {
Json_Adapter<Json>::append(array, std::forward<T>(value));
} else {
Json_Adapter<Json>::append(array, json_scalar<Json>(std::forward<T>(value)));
}
}
template <class Json, class T>
T json_get(const Json& value) {
return Json_Adapter<Json>::template get<T>(value);
}
template <class Json>
Json parse_json(std::string_view value) {
return Json_Adapter<Json>::parse(value);
}
template <class Json>
std::string dump_json(const Json& value, int indent = -1) {
return Json_Adapter<Json>::dump(value, indent);
}
template <class T, class Json>
struct Value_Adapter {
using value_type = std::remove_cvref_t<T>;
static const value_type& read(const value_type& value) noexcept {
return value;
}
static void write(value_type& target, value_type value) {
target = std::move(value);
}
};
template <class T, class Json>
using Adapted_Value_Type = typename Value_Adapter<std::remove_cvref_t<T>, Json>::value_type;
template <class T, class Json>
concept Value_Adapter_With_Encode = requires(const std::remove_cvref_t<T>& value) {
{ Value_Adapter<std::remove_cvref_t<T>, Json>::encode(value) } -> std::same_as<Json>;
};
template <class T, class Json>
concept Value_Adapter_With_Decode = requires(std::remove_cvref_t<T>& target, const Json& value) {
Value_Adapter<std::remove_cvref_t<T>, Json>::decode(target, value);
};
template <class T, class Json>
concept Value_Adapter_With_Type_Name = requires {
{ Value_Adapter<std::remove_cvref_t<T>, Json>::type_name } -> std::convertible_to<std::string_view>;
};
template <class T, class Json>
concept Value_Adapter_With_Schema = requires(Json& value) {
Value_Adapter<std::remove_cvref_t<T>, Json>::append_schema(value);
};
template <class T, class Json>
decltype(auto) read_adapted_value(const T& value) {
return Value_Adapter<std::remove_cvref_t<T>, Json>::read(value);
}
template <class T, class Json>
void write_adapted_value(T& target, Adapted_Value_Type<T, Json> value) {
Value_Adapter<std::remove_cvref_t<T>, Json>::write(target, std::move(value));
}
template <class T, class Json>
concept Value_Adapter_With_Minimum = requires {
{ Value_Adapter<std::remove_cvref_t<T>, Json>::minimum };
};
template <class T, class Json>
concept Value_Adapter_With_Maximum = requires {
{ Value_Adapter<std::remove_cvref_t<T>, Json>::maximum };
};
template <class T, class Json>
concept Value_Adapter_With_Multiple_Of = requires {
{ Value_Adapter<std::remove_cvref_t<T>, Json>::multiple_of };
};
template <class T, class Json>
concept Value_Adapter_With_Validation_Message = requires {
{ Value_Adapter<std::remove_cvref_t<T>, Json>::validation_message } -> std::convertible_to<std::string_view>;
};
template <class Enum>
struct Enum_Adapter;
template <class Enum>
concept Enum_Type = std::is_enum_v<Enum> && requires(Enum value, std::string_view name) {
{ Enum_Adapter<Enum>::values() };
{ Enum_Adapter<Enum>::name(value) } -> std::convertible_to<std::string_view>;
{ Enum_Adapter<Enum>::cast(name) } -> std::same_as<std::optional<Enum>>;
};
template <Enum_Type Enum>
auto enum_values() {
return Enum_Adapter<Enum>::values();
}
template <Enum_Type Enum>
std::string_view enum_name(Enum value) {
return Enum_Adapter<Enum>::name(value);
}
template <Enum_Type Enum>
std::optional<Enum> enum_cast(std::string_view value) {
return Enum_Adapter<Enum>::cast(value);
}
template <class Enum>
concept Enum_Adapter_With_Underlying_Cast = Enum_Type<Enum> && requires(std::underlying_type_t<Enum> value) {
{ Enum_Adapter<Enum>::cast(value) } -> std::same_as<std::optional<Enum>>;
};
}
@@ -0,0 +1,26 @@
#pragma once
#include "adminive/adapter.hpp"
#include "magic_enum/magic_enum.hpp"
#include <optional>
#include <string_view>
#include <type_traits>
#include <vector>
namespace adminive {
template <class Enum>
requires std::is_enum_v<Enum>
struct Enum_Adapter<Enum> {
static std::vector<Enum> values() {
const auto source = magic_enum::enum_values<Enum>();
return std::vector<Enum>(source.begin(), source.end());
}
static std::string_view name(Enum value) {
return magic_enum::enum_name(value);
}
static std::optional<Enum> cast(std::string_view value) {
return magic_enum::enum_cast<Enum>(value);
}
static std::optional<Enum> cast(std::underlying_type_t<Enum> value) {
return magic_enum::enum_cast<Enum>(value);
}
};
}
@@ -0,0 +1,90 @@
#pragma once
#include "adminive/adapter.hpp"
#include "nlohmann/json.hpp"
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace adminive {
template <>
struct Json_Adapter<nlohmann::json> {
using Json = nlohmann::json;
static Json object() {
return Json::object();
}
static Json array() {
return Json::array();
}
static Json parse(std::string_view value) {
return Json::parse(value.begin(), value.end());
}
static std::string dump(const Json& value, int indent) {
return value.dump(indent);
}
static bool is_object(const Json& value) noexcept {
return value.is_object();
}
static bool is_array(const Json& value) noexcept {
return value.is_array();
}
static bool is_string(const Json& value) noexcept {
return value.is_string();
}
static bool is_number(const Json& value) noexcept {
return value.is_number();
}
static bool is_boolean(const Json& value) noexcept {
return value.is_boolean();
}
static long double number(const Json& value) {
return value.get<long double>();
}
static bool contains(const Json& value, std::string_view name) {
return value.contains(std::string(name));
}
static std::size_t size(const Json& value) noexcept {
return value.size();
}
static std::vector<std::string> keys(const Json& value) {
std::vector<std::string> result;
result.reserve(value.size());
for(auto iterator = value.begin(); iterator != value.end(); ++iterator) {
result.push_back(iterator.key());
}
return result;
}
static Json& at(Json& value, std::string_view name) {
return value.at(std::string(name));
}
static const Json& at(const Json& value, std::string_view name) {
return value.at(std::string(name));
}
static Json& at(Json& value, std::size_t index) {
return value.at(index);
}
static const Json& at(const Json& value, std::size_t index) {
return value.at(index);
}
static void set(Json& value, std::string_view name, Json item) {
value[std::string(name)] = std::move(item);
}
static void append(Json& value, Json item) {
value.push_back(std::move(item));
}
static void erase(Json& value, std::string_view name) {
value.erase(std::string(name));
}
template <class T>
static Json make(T&& value) {
if constexpr(std::same_as<std::remove_cvref_t<T>, std::string_view>) {
return Json(std::string(value));
} else {
return Json(std::forward<T>(value));
}
}
template <class T>
static T get(const Json& value) {
return value.get<T>();
}
};
}
+1 -1
View File
@@ -1,8 +1,8 @@
#pragma once
#include "adminive/adapter.hpp"
#include "adminive/amis.hpp"
#include "adminive/concepts.hpp"
#include "adminive/descriptor.hpp"
#include "adminive/http.hpp"
#include "adminive/json.hpp"
#include "adminive/status.hpp"
#include "adminive/value.hpp"
+293 -117
View File
@@ -13,191 +13,367 @@ inline std::string make_field_name(std::string_view prefix, std::string_view nam
}
return std::string(prefix) + "." + std::string(name);
}
inline Json make_amis_form_actions(std::string submit_label = "Apply") {
return Json::array({Json{{"type", "reset"}, {"label", "Reset"}}, Json{{"type", "submit"}, {"label", std::move(submit_label)}, {"level", "primary"}}});
template <Json_Type Json>
Json make_amis_form_actions(std::string submit_label = "Apply") {
Json reset = json_object<Json>();
json_set(reset, "type", "reset");
json_set(reset, "label", "Reset");
Json submit = json_object<Json>();
json_set(submit, "type", "submit");
json_set(submit, "label", std::move(submit_label));
json_set(submit, "level", "primary");
Json result = json_array<Json>();
json_append(result, std::move(reset));
json_append(result, std::move(submit));
return result;
}
inline Json make_amis_control(const Json& field, std::string_view permission, std::string_view prefix = {}) {
const std::string name = field.at("name").get<std::string>();
template <Json_Type Json>
Json make_amis_control(const Json& field, std::string_view permission, std::string_view prefix = {}) {
const std::string name = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "name"));
const std::string full_name = make_field_name(prefix, name);
const std::string value_type = field.at("value_type").get<std::string>();
if(value_type == "object" && field.contains("children")) {
Json body = Json::array();
for(const auto& child : field.at("children")) {
if(child.at("visible").get<bool>()) {
body.push_back(make_amis_control(child, permission, full_name));
const std::string value_type = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "value_type"));
if(value_type == "object" && Json_Adapter<Json>::contains(field, "children")) {
Json body = json_array<Json>();
const auto& children = Json_Adapter<Json>::at(field, "children");
for(std::size_t index = 0; index < Json_Adapter<Json>::size(children); ++index) {
const auto& child = Json_Adapter<Json>::at(children, index);
if(json_get<Json, bool>(Json_Adapter<Json>::at(child, "visible"))) {
json_append(body, make_amis_control<Json>(child, permission, full_name));
}
}
Json control{{"type", "fieldset"}, {"title", field.at("label")}, {"body", std::move(body)}, {"collapsable", true}, {"collapsed", false}};
if(field.contains("description")) {
control["description"] = field.at("description");
Json control = json_object<Json>();
json_set(control, "type", "fieldset");
json_set(control, "title", Json_Adapter<Json>::at(field, "label"));
json_set(control, "body", std::move(body));
json_set(control, "collapsable", true);
json_set(control, "collapsed", false);
if(Json_Adapter<Json>::contains(field, "description")) {
json_set(control, "description", Json_Adapter<Json>::at(field, "description"));
}
if(field.contains("visible_on")) {
control["visibleOn"] = field.at("visible_on");
if(Json_Adapter<Json>::contains(field, "visible_on")) {
json_set(control, "visibleOn", Json_Adapter<Json>::at(field, "visible_on"));
}
return control;
}
Json control{{"name", full_name}, {"label", field.at("label")}};
if(field.contains("description")) {
control["description"] = field.at("description");
Json control = json_object<Json>();
json_set(control, "name", full_name);
json_set(control, "label", Json_Adapter<Json>::at(field, "label"));
if(Json_Adapter<Json>::contains(field, "description")) {
json_set(control, "description", Json_Adapter<Json>::at(field, "description"));
}
if(field.contains("visible_on")) {
control["visibleOn"] = field.at("visible_on");
if(Json_Adapter<Json>::contains(field, "visible_on")) {
json_set(control, "visibleOn", Json_Adapter<Json>::at(field, "visible_on"));
}
if(!field.at(permission).get<bool>()) {
control["type"] = "static";
if(!json_get<Json, bool>(Json_Adapter<Json>::at(field, permission))) {
json_set(control, "type", "static");
return control;
}
if(field.contains("widget")) {
control["type"] = field.at("widget");
if(Json_Adapter<Json>::contains(field, "widget")) {
json_set(control, "type", Json_Adapter<Json>::at(field, "widget"));
} else if(value_type == "boolean") {
control["type"] = "switch";
json_set(control, "type", "switch");
} else if(value_type == "integer" || value_type == "number") {
control["type"] = "input-number";
json_set(control, "type", "input-number");
} else if(value_type == "enum") {
control["type"] = "select";
json_set(control, "type", "select");
} else {
control["type"] = "input-text";
json_set(control, "type", "input-text");
}
if(control.at("type") == "input-date") {
control["valueFormat"] = "YYYY-MM-DD";
control["displayFormat"] = "YYYY-MM-DD";
control["clearable"] = true;
} else if(control.at("type") == "input-color") {
control["clearable"] = true;
const std::string control_type = json_get<Json, std::string>(Json_Adapter<Json>::at(control, "type"));
if(control_type == "input-date") {
json_set(control, "valueFormat", "YYYY-MM-DD");
json_set(control, "displayFormat", "YYYY-MM-DD");
json_set(control, "clearable", true);
} else if(control_type == "input-color") {
json_set(control, "clearable", true);
}
if(field.at("required").get<bool>()) {
control["required"] = true;
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "required"))) {
json_set(control, "required", true);
}
if(field.contains("minimum")) {
control["min"] = field.at("minimum");
if(Json_Adapter<Json>::contains(field, "minimum")) {
json_set(control, "min", Json_Adapter<Json>::at(field, "minimum"));
}
if(field.contains("maximum")) {
control["max"] = field.at("maximum");
if(Json_Adapter<Json>::contains(field, "maximum")) {
json_set(control, "max", Json_Adapter<Json>::at(field, "maximum"));
}
if(field.contains("multiple_of")) {
control["step"] = field.at("multiple_of");
if(Json_Adapter<Json>::contains(field, "multiple_of")) {
json_set(control, "step", Json_Adapter<Json>::at(field, "multiple_of"));
}
if(field.contains("validation_message")) {
control["validationErrors"] = Json{{"isInt", field.at("validation_message")}};
if(Json_Adapter<Json>::contains(field, "validation_message")) {
Json errors = json_object<Json>();
json_set(errors, "isInt", Json_Adapter<Json>::at(field, "validation_message"));
json_set(control, "validationErrors", std::move(errors));
}
if(field.contains("options")) {
control["options"] = field.at("options");
if(Json_Adapter<Json>::contains(field, "options")) {
json_set(control, "options", Json_Adapter<Json>::at(field, "options"));
}
return control;
}
inline Json make_amis_form_body(const Json& descriptor, std::string_view permission) {
Json body = Json::array();
for(const auto& field : descriptor.at("fields")) {
if(field.at("visible").get<bool>()) {
body.push_back(make_amis_control(field, permission));
template <Json_Type Json>
Json make_amis_form_body(const Json& descriptor, std::string_view permission) {
Json body = json_array<Json>();
const auto& fields = Json_Adapter<Json>::at(descriptor, "fields");
for(std::size_t index = 0; index < Json_Adapter<Json>::size(fields); ++index) {
const auto& field = Json_Adapter<Json>::at(fields, index);
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "visible"))) {
json_append(body, make_amis_control<Json>(field, permission));
}
}
return body;
}
inline Json make_amis_column(const Json& field) {
Json column{{"name", field.at("name")}, {"label", field.at("list_label")}};
if(field.contains("order")) {
column["order"] = field.at("order");
template <Json_Type Json>
Json make_amis_column(const Json& field) {
Json column = json_object<Json>();
json_set(column, "name", Json_Adapter<Json>::at(field, "name"));
json_set(column, "label", Json_Adapter<Json>::at(field, "list_label"));
if(Json_Adapter<Json>::contains(field, "order")) {
json_set(column, "order", Json_Adapter<Json>::at(field, "order"));
}
if(field.contains("sortable")) {
column["sortable"] = field.at("sortable");
if(Json_Adapter<Json>::contains(field, "sortable")) {
json_set(column, "sortable", Json_Adapter<Json>::at(field, "sortable"));
}
const std::string value_type = field.at("value_type");
const std::string value_type = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "value_type"));
if(value_type == "boolean") {
column["type"] = "status";
} else if(value_type == "enum" && field.contains("options")) {
column["type"] = "mapping";
Json map = Json::object();
for(const auto& option : field.at("options")) {
map[option.at("value").get<std::string>()] = option.at("label");
json_set(column, "type", "status");
} else if(value_type == "enum" && Json_Adapter<Json>::contains(field, "options")) {
json_set(column, "type", "mapping");
Json map = json_object<Json>();
const auto& options = Json_Adapter<Json>::at(field, "options");
for(std::size_t index = 0; index < Json_Adapter<Json>::size(options); ++index) {
const auto& option = Json_Adapter<Json>::at(options, index);
json_set(map, json_get<Json, std::string>(Json_Adapter<Json>::at(option, "value")), Json_Adapter<Json>::at(option, "label"));
}
column["map"] = std::move(map);
json_set(column, "map", std::move(map));
}
return column;
}
inline Json make_amis_status_service(std::string status_api) {
const Json status_view{{"type", "tpl"}, {"tpl", R"(<div class="adminive-status-grid"><div><strong>Service</strong><span>${service_state}</span></div><div><strong>Server time</strong><span>${server_time}</span></div><div><strong>Uptime</strong><span>${uptime_seconds} s</span></div><div><strong>Polling sequence</strong><span>${poll_sequence}</span></div><div><strong>Radio states</strong><span>${radio_state_count}</span></div><div><strong>Listen port</strong><span>${listen_port}</span></div></div>)"}};
const Json panel{{"type", "panel"}, {"title", "Backend Status"}, {"body", status_view}};
return Json{{"type", "service"}, {"name", "backend_status"}, {"api", Json{{"method", "get"}, {"url", std::move(status_api)}}}, {"initFetch", true}, {"interval", 2000}, {"silentPolling", true}, {"showErrorMsg", true}, {"body", panel}};
template <Json_Type Json>
Json make_amis_status_service(std::string status_api) {
Json view = json_object<Json>();
json_set(view, "type", "tpl");
json_set(view, "tpl", R"(<div class="adminive-status-grid"><div><strong>Service</strong><span>${service_state}</span></div><div><strong>Server time</strong><span>${server_time}</span></div><div><strong>Uptime</strong><span>${uptime_seconds} s</span></div><div><strong>Polling sequence</strong><span>${poll_sequence}</span></div><div><strong>Radio states</strong><span>${radio_state_count}</span></div><div><strong>Listen port</strong><span>${listen_port}</span></div></div>)");
Json panel = json_object<Json>();
json_set(panel, "type", "panel");
json_set(panel, "title", "Backend Status");
json_set(panel, "body", std::move(view));
Json api = json_object<Json>();
json_set(api, "method", "get");
json_set(api, "url", std::move(status_api));
Json result = json_object<Json>();
json_set(result, "type", "service");
json_set(result, "name", "backend_status");
json_set(result, "api", std::move(api));
json_set(result, "initFetch", true);
json_set(result, "interval", 2000);
json_set(result, "silentPolling", true);
json_set(result, "showErrorMsg", true);
json_set(result, "body", std::move(panel));
return result;
}
inline Json make_amis_object_status_body(const Json& descriptor) {
Json body = Json::array();
for(const auto& field : descriptor.at("fields")) {
const std::string name = field.at("name").get<std::string>();
const std::string label = field.at("label").get<std::string>();
const bool color = field.at("color").get<bool>();
template <Json_Type Json>
Json make_amis_object_status_body(const Json& descriptor) {
Json body = json_array<Json>();
const auto& fields = Json_Adapter<Json>::at(descriptor, "fields");
for(std::size_t index = 0; index < Json_Adapter<Json>::size(fields); ++index) {
const auto& field = Json_Adapter<Json>::at(fields, index);
const std::string name = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "name"));
const std::string label = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "label"));
const bool color = json_get<Json, bool>(Json_Adapter<Json>::at(field, "color"));
std::string tpl = "<div class=\"adminive-object-status-row\"><span class=\"adminive-object-status-label\">" + label + "</span><strong";
if(color) {
tpl += " style=\"color: ${" + name + ".color}\"";
}
tpl += ">${" + name + ".value}</strong></div>";
body.push_back(Json{{"type", "tpl"}, {"tpl", std::move(tpl)}});
Json item = json_object<Json>();
json_set(item, "type", "tpl");
json_set(item, "tpl", std::move(tpl));
json_append(body, std::move(item));
}
return Json{{"type", "container"}, {"className", "adminive-object-status"}, {"body", std::move(body)}};
Json result = json_object<Json>();
json_set(result, "type", "container");
json_set(result, "className", "adminive-object-status");
json_set(result, "body", std::move(body));
return result;
}
inline Json make_amis_object_status_service(const Json& descriptor, std::string status_api, std::uint64_t interval) {
return Json{{"type", "service"}, {"api", Json{{"method", "get"}, {"url", std::move(status_api)}}}, {"initFetch", true}, {"interval", interval}, {"silentPolling", true}, {"showErrorMsg", true}, {"body", make_amis_object_status_body(descriptor)}};
template <Json_Type Json>
Json make_amis_object_status_service(const Json& descriptor, std::string status_api, std::uint64_t interval) {
Json api = json_object<Json>();
json_set(api, "method", "get");
json_set(api, "url", std::move(status_api));
Json result = json_object<Json>();
json_set(result, "type", "service");
json_set(result, "api", std::move(api));
json_set(result, "initFetch", true);
json_set(result, "interval", interval);
json_set(result, "silentPolling", true);
json_set(result, "showErrorMsg", true);
json_set(result, "body", make_amis_object_status_body<Json>(descriptor));
return result;
}
inline Json make_amis_object_status_column(const Json& descriptor, std::string status_api, std::uint64_t interval, std::string action_label) {
const Json pop_over{{"trigger", "click"}, {"position", "left-top"}, {"showIcon", false}, {"body", make_amis_object_status_service(descriptor, std::move(status_api), interval)}};
return Json{{"type", "tpl"}, {"label", descriptor.at("label")}, {"tpl", "<span class=\"text-primary\">" + std::move(action_label) + "</span>"}, {"popOver", pop_over}};
template <Json_Type Json>
Json make_amis_object_status_column(const Json& descriptor, std::string status_api, std::uint64_t interval, std::string action_label) {
Json pop_over = json_object<Json>();
json_set(pop_over, "trigger", "click");
json_set(pop_over, "position", "left-top");
json_set(pop_over, "showIcon", false);
json_set(pop_over, "body", make_amis_object_status_service<Json>(descriptor, std::move(status_api), interval));
Json result = json_object<Json>();
json_set(result, "type", "tpl");
json_set(result, "label", Json_Adapter<Json>::at(descriptor, "label"));
json_set(result, "tpl", "<span class=\"text-primary\">" + std::move(action_label) + "</span>");
json_set(result, "popOver", std::move(pop_over));
return result;
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
Json to_amis_form_schema(const T& value, std::string submit_api = {}, std::string submit_label = "Apply") {
const Json descriptor = to_descriptor_json<T>();
Json result{{"type", "form"}, {"title", descriptor.at("label")}, {"data", to_frontend_json(value)}, {"body", make_amis_form_body(descriptor, "editable")}, {"actions", make_amis_form_actions(std::move(submit_label))}, {"affixFooter", true}};
const Json descriptor = to_descriptor_json<Json, T>();
Json result = json_object<Json>();
json_set(result, "type", "form");
json_set(result, "title", Json_Adapter<Json>::at(descriptor, "label"));
json_set(result, "data", to_frontend_json<Json>(value));
json_set(result, "body", make_amis_form_body<Json>(descriptor, "editable"));
json_set(result, "actions", make_amis_form_actions<Json>(std::move(submit_label)));
json_set(result, "affixFooter", true);
if(!submit_api.empty()) {
result["api"] = Json{{"method", "post"}, {"url", std::move(submit_api)}};
Json api = json_object<Json>();
json_set(api, "method", "post");
json_set(api, "url", std::move(submit_api));
json_set(result, "api", std::move(api));
}
return result;
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = nullptr, std::uint64_t status_interval = 2000) {
const Json descriptor = to_descriptor_json<T>();
const Json& list = descriptor.at("list");
const std::string crud_id = descriptor.at("name").get<std::string>() + "_crud";
Json columns = Json::array({Json{{"name", "id"}, {"label", list.at("id_label")}, {"sortable", true}}});
const Json descriptor = to_descriptor_json<Json, T>();
const Json& list = Json_Adapter<Json>::at(descriptor, "list");
const std::string crud_id = json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "name")) + "_crud";
Json columns = json_array<Json>();
Json id_column = json_object<Json>();
json_set(id_column, "name", "id");
json_set(id_column, "label", Json_Adapter<Json>::at(list, "id_label"));
json_set(id_column, "sortable", true);
json_append(columns, std::move(id_column));
std::vector<Json> list_fields;
for(const auto& field : descriptor.at("fields")) {
if(field.at("readable").get<bool>() && field.at("list_visible").get<bool>() && field.at("value_type") != "object") {
const auto& fields = Json_Adapter<Json>::at(descriptor, "fields");
for(std::size_t index = 0; index < Json_Adapter<Json>::size(fields); ++index) {
const auto& field = Json_Adapter<Json>::at(fields, index);
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "readable")) && json_get<Json, bool>(Json_Adapter<Json>::at(field, "list_visible")) && json_get<Json, std::string>(Json_Adapter<Json>::at(field, "value_type")) != "object") {
list_fields.push_back(field);
}
}
std::stable_sort(list_fields.begin(), list_fields.end(), [](const Json& left, const Json& right) {
return left.at("order").get<int>() < right.at("order").get<int>();
return json_get<Json, int>(Json_Adapter<Json>::at(left, "order")) < json_get<Json, int>(Json_Adapter<Json>::at(right, "order"));
});
for(const auto& field : list_fields) {
columns.push_back(make_amis_column(field));
json_append(columns, make_amis_column<Json>(field));
}
if(status_descriptor) {
columns.push_back(make_amis_object_status_column(*status_descriptor, base_api + "/${id}/status", status_interval, list.at("view_status_label").get<std::string>()));
json_append(columns, make_amis_object_status_column<Json>(*status_descriptor, base_api + "/${id}/status", status_interval, json_get<Json, std::string>(Json_Adapter<Json>::at(list, "view_status_label"))));
}
const std::string create_label = list.at("create_label").get<std::string>();
const std::string edit_label = list.at("edit_label").get<std::string>();
const Json create_form{{"type", "form"}, {"api", Json{{"method", "post"}, {"url", base_api}}}, {"reload", crud_id}, {"body", make_amis_form_body(descriptor, "creatable")}, {"actions", make_amis_form_actions(create_label)}};
const Json edit_form{{"type", "form"}, {"api", Json{{"method", "put"}, {"url", base_api + "/${id}"}}}, {"reload", crud_id}, {"body", make_amis_form_body(descriptor, "editable")}, {"actions", make_amis_form_actions(list.at("confirm_label").get<std::string>())}};
const Json create_button{{"type", "button"}, {"label", create_label}, {"level", "primary"}, {"actionType", "dialog"}, {"dialog", Json{{"title", create_label + " " + descriptor.at("label").get<std::string>()}, {"body", create_form}}}};
Json actions = Json::array();
actions.push_back(Json{{"type", "button"}, {"label", edit_label}, {"level", "link"}, {"actionType", "dialog"}, {"dialog", Json{{"title", edit_label + " " + descriptor.at("label").get<std::string>()}, {"body", edit_form}}}});
actions.push_back(Json{{"type", "button"}, {"label", list.at("delete_label")}, {"level", "link"}, {"className", "text-danger"}, {"actionType", "ajax"}, {"confirmText", list.at("delete_confirm")}, {"api", Json{{"method", "delete"}, {"url", base_api + "/${id}"}}}, {"reload", crud_id}});
columns.push_back(Json{{"type", "operation"}, {"label", list.at("actions_label")}, {"buttons", std::move(actions)}});
const Json columns_toggler{{"type", "columns-toggler"}, {"draggable", list.at("column_reorderable")}};
Json crud{{"type", "crud"}, {"id", crud_id}, {"api", Json{{"method", "get"}, {"url", base_api}}}, {"syncLocation", false}, {"primaryField", "id"}, {"headerToolbar", Json::array({columns_toggler, "reload", create_button})}, {"columns", std::move(columns)}};
const std::string default_order_by = list.at("default_order_by").get<std::string>();
const std::string create_label = json_get<Json, std::string>(Json_Adapter<Json>::at(list, "create_label"));
const std::string edit_label = json_get<Json, std::string>(Json_Adapter<Json>::at(list, "edit_label"));
Json create_api = json_object<Json>();
json_set(create_api, "method", "post");
json_set(create_api, "url", base_api);
Json create_form = json_object<Json>();
json_set(create_form, "type", "form");
json_set(create_form, "api", std::move(create_api));
json_set(create_form, "reload", crud_id);
json_set(create_form, "body", make_amis_form_body<Json>(descriptor, "creatable"));
json_set(create_form, "actions", make_amis_form_actions<Json>(create_label));
Json edit_api = json_object<Json>();
json_set(edit_api, "method", "put");
json_set(edit_api, "url", base_api + "/${id}");
Json edit_form = json_object<Json>();
json_set(edit_form, "type", "form");
json_set(edit_form, "api", std::move(edit_api));
json_set(edit_form, "reload", crud_id);
json_set(edit_form, "body", make_amis_form_body<Json>(descriptor, "editable"));
json_set(edit_form, "actions", make_amis_form_actions<Json>(json_get<Json, std::string>(Json_Adapter<Json>::at(list, "confirm_label"))));
Json create_dialog = json_object<Json>();
json_set(create_dialog, "title", create_label + " " + json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "label")));
json_set(create_dialog, "body", std::move(create_form));
Json create_button = json_object<Json>();
json_set(create_button, "type", "button");
json_set(create_button, "label", create_label);
json_set(create_button, "level", "primary");
json_set(create_button, "actionType", "dialog");
json_set(create_button, "dialog", std::move(create_dialog));
Json edit_dialog = json_object<Json>();
json_set(edit_dialog, "title", edit_label + " " + json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "label")));
json_set(edit_dialog, "body", std::move(edit_form));
Json edit_button = json_object<Json>();
json_set(edit_button, "type", "button");
json_set(edit_button, "label", edit_label);
json_set(edit_button, "level", "link");
json_set(edit_button, "actionType", "dialog");
json_set(edit_button, "dialog", std::move(edit_dialog));
Json delete_api = json_object<Json>();
json_set(delete_api, "method", "delete");
json_set(delete_api, "url", base_api + "/${id}");
Json delete_button = json_object<Json>();
json_set(delete_button, "type", "button");
json_set(delete_button, "label", Json_Adapter<Json>::at(list, "delete_label"));
json_set(delete_button, "level", "link");
json_set(delete_button, "className", "text-danger");
json_set(delete_button, "actionType", "ajax");
json_set(delete_button, "confirmText", Json_Adapter<Json>::at(list, "delete_confirm"));
json_set(delete_button, "api", std::move(delete_api));
json_set(delete_button, "reload", crud_id);
Json buttons = json_array<Json>();
json_append(buttons, std::move(edit_button));
json_append(buttons, std::move(delete_button));
Json operation = json_object<Json>();
json_set(operation, "type", "operation");
json_set(operation, "label", Json_Adapter<Json>::at(list, "actions_label"));
json_set(operation, "buttons", std::move(buttons));
json_append(columns, std::move(operation));
Json toggler = json_object<Json>();
json_set(toggler, "type", "columns-toggler");
json_set(toggler, "draggable", Json_Adapter<Json>::at(list, "column_reorderable"));
Json toolbar = json_array<Json>();
json_append(toolbar, std::move(toggler));
json_append(toolbar, "reload");
json_append(toolbar, std::move(create_button));
Json crud_api = json_object<Json>();
json_set(crud_api, "method", "get");
json_set(crud_api, "url", base_api);
Json crud = json_object<Json>();
json_set(crud, "type", "crud");
json_set(crud, "id", crud_id);
json_set(crud, "api", std::move(crud_api));
json_set(crud, "syncLocation", false);
json_set(crud, "primaryField", "id");
json_set(crud, "headerToolbar", std::move(toolbar));
json_set(crud, "columns", std::move(columns));
const std::string default_order_by = json_get<Json, std::string>(Json_Adapter<Json>::at(list, "default_order_by"));
if(!default_order_by.empty()) {
crud["defaultParams"] = Json{{"orderBy", default_order_by}, {"orderDir", list.at("default_order_dir")}};
Json params = json_object<Json>();
json_set(params, "orderBy", default_order_by);
json_set(params, "orderDir", Json_Adapter<Json>::at(list, "default_order_dir"));
json_set(crud, "defaultParams", std::move(params));
}
if(list.at("user_reorderable").get<bool>()) {
crud["draggable"] = true;
crud["saveOrderApi"] = Json{{"method", "post"}, {"url", base_api + "/order"}};
if(json_get<Json, bool>(Json_Adapter<Json>::at(list, "user_reorderable"))) {
json_set(crud, "draggable", true);
Json order_api = json_object<Json>();
json_set(order_api, "method", "post");
json_set(order_api, "url", base_api + "/order");
json_set(crud, "saveOrderApi", std::move(order_api));
}
return Json{{"type", "page"}, {"title", list.at("management_label")}, {"body", std::move(crud)}};
Json result = json_object<Json>();
json_set(result, "type", "page");
json_set(result, "title", Json_Adapter<Json>::at(list, "management_label"));
json_set(result, "body", std::move(crud));
return result;
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
Json to_amis_crud_status_schema(std::string base_api, std::string status_api, const Json* object_status_descriptor = nullptr, std::uint64_t object_status_interval = 2000) {
Json result = to_amis_crud_schema<T>(std::move(base_api), object_status_descriptor, object_status_interval);
Json crud = std::move(result.at("body"));
result["body"] = Json::array({make_amis_status_service(std::move(status_api)), std::move(crud)});
Json result = to_amis_crud_schema<Json, T>(std::move(base_api), object_status_descriptor, object_status_interval);
Json crud = std::move(Json_Adapter<Json>::at(result, "body"));
Json body = json_array<Json>();
json_append(body, make_amis_status_service<Json>(std::move(status_api)));
json_append(body, std::move(crud));
json_set(result, "body", std::move(body));
return result;
}
}
+150 -45
View File
@@ -1,6 +1,6 @@
#pragma once
#include "adminive/adapter.hpp"
#include "adminive/concepts.hpp"
#include "magic_enum/magic_enum.hpp"
#include <cctype>
#include <concepts>
#include <map>
@@ -17,6 +17,10 @@ template <class T>
concept Described_Type = requires {
{ Type_Descriptor<std::remove_cvref_t<T>>::get() };
};
template <class T>
concept Reflected_Type = requires {
{ Reflection_Adapter<std::remove_cvref_t<T>>::field_count } -> std::convertible_to<std::size_t>;
};
template <auto Member>
struct Member_Pointer_Traits;
template <class Owner, class Member_Type, Member_Type Owner::*Member>
@@ -25,12 +29,40 @@ struct Member_Pointer_Traits<Member> {
using member_type = Member_Type;
static constexpr auto value = Member;
};
template <auto Member>
struct Member_Field_Accessor {
using owner_type = typename Member_Pointer_Traits<Member>::owner_type;
using member_type = typename Member_Pointer_Traits<Member>::member_type;
template <class Object>
static decltype(auto) get(Object& object) noexcept {
return object.*Member;
}
template <class Object>
static decltype(auto) get(const Object& object) noexcept {
return object.*Member;
}
};
template <class T, std::size_t Index>
struct Reflected_Field_Accessor {
using owner_type = T;
using reference_type = decltype(Reflection_Adapter<T>::template get<Index>(std::declval<T&>()));
using const_reference_type = decltype(Reflection_Adapter<T>::template get<Index>(std::declval<const T&>()));
using member_type = std::remove_cvref_t<reference_type>;
static_assert(std::is_lvalue_reference_v<reference_type>);
static_assert(std::is_lvalue_reference_v<const_reference_type>);
static decltype(auto) get(T& object) noexcept(noexcept(Reflection_Adapter<T>::template get<Index>(object))) {
return Reflection_Adapter<T>::template get<Index>(object);
}
static decltype(auto) get(const T& object) noexcept(noexcept(Reflection_Adapter<T>::template get<Index>(object))) {
return Reflection_Adapter<T>::template get<Index>(object);
}
};
inline std::string make_label(std::string_view name) {
std::string result;
result.reserve(name.size());
bool uppercase = true;
for(const char value : name) {
if(value == '_') {
for (const char value : name) {
if (value == '_') {
result.push_back(' ');
uppercase = true;
continue;
@@ -40,90 +72,104 @@ inline std::string make_label(std::string_view name) {
}
return result;
}
template <auto Member>
class Field_Descriptor {
template <class Derived, class Accessor>
class Field_Descriptor_Base {
public:
using owner_type = typename Member_Pointer_Traits<Member>::owner_type;
using member_type = typename Member_Pointer_Traits<Member>::member_type;
static constexpr auto member = Member;
explicit Field_Descriptor(std::string name) : name_(std::move(name)), label_(make_label(name_)) {}
Field_Descriptor(std::string name, std::string label) : name_(std::move(name)), label_(std::move(label)) {}
Field_Descriptor editable(bool value = true) const {
auto result = *this;
using owner_type = typename Accessor::owner_type;
using member_type = typename Accessor::member_type;
explicit Field_Descriptor_Base(std::string name) : name_(std::move(name)), label_(make_label(name_)) {}
Field_Descriptor_Base(std::string name, std::string label) : name_(std::move(name)), label_(std::move(label)) {}
Derived editable(bool value = true) const {
auto result = derived();
result.editable_ = value;
return result;
}
Field_Descriptor creatable(bool value = true) const {
auto result = *this;
Derived creatable(bool value = true) const {
auto result = derived();
result.creatable_ = value;
return result;
}
Field_Descriptor readable(bool value = true) const {
auto result = *this;
Derived readable(bool value = true) const {
auto result = derived();
result.readable_ = value;
return result;
}
Field_Descriptor visible(bool value = true) const {
auto result = *this;
Derived visible(bool value = true) const {
auto result = derived();
result.visible_ = value;
return result;
}
Field_Descriptor required(bool value = true) const {
auto result = *this;
Derived required(bool value = true) const {
auto result = derived();
result.required_ = value;
return result;
}
Field_Descriptor list_visible(bool value = true) const {
auto result = *this;
Derived list_visible(bool value = true) const {
auto result = derived();
result.list_visible_ = value;
return result;
}
Field_Descriptor label(std::string value) const {
auto result = *this;
Derived label(std::string value) const {
auto result = derived();
result.label_ = std::move(value);
return result;
}
Field_Descriptor description(std::string value) const {
auto result = *this;
Derived description(std::string value) const {
auto result = derived();
result.description_ = std::move(value);
return result;
}
Field_Descriptor widget(std::string value) const {
auto result = *this;
Derived widget(std::string value) const {
auto result = derived();
result.widget_ = std::move(value);
return result;
}
Field_Descriptor visible_on(std::string value) const {
auto result = *this;
Derived visible_on(std::string value) const {
auto result = derived();
result.visible_on_ = std::move(value);
return result;
}
Field_Descriptor list_label(std::string value) const {
auto result = *this;
Derived list_label(std::string value) const {
auto result = derived();
result.list_label_ = std::move(value);
return result;
}
Field_Descriptor order(int value) const {
auto result = *this;
Derived order(int value) const {
auto result = derived();
result.order_ = value;
return result;
}
Field_Descriptor sortable(bool value = true) const {
auto result = *this;
Derived sortable(bool value = true) const {
auto result = derived();
result.sortable_ = value;
return result;
}
template <auto Value>
requires std::is_enum_v<member_type> && std::same_as<decltype(Value), member_type>
Field_Descriptor enum_label(std::string label) const {
const auto name = magic_enum::enum_name(Value);
if(name.empty()) {
throw std::invalid_argument("enum value is outside the magic_enum reflection range");
requires Enum_Type<std::remove_cv_t<decltype(Value)>>
Derived enum_label(std::string label) const {
const auto name = enum_name(Value);
if (name.empty()) {
throw std::invalid_argument("enum adapter returned an empty value name");
}
auto result = *this;
auto result = derived();
result.enum_labels_.insert_or_assign(std::string(name), std::move(label));
return result;
}
Derived enum_label(std::string value_name, std::string label) const {
auto result = derived();
result.enum_labels_.insert_or_assign(std::move(value_name), std::move(label));
return result;
}
template <class Object>
requires std::convertible_to<std::remove_reference_t<Object>*, owner_type*>
decltype(auto) get(Object& object) const noexcept(noexcept(Accessor::get(object))) {
return Accessor::get(object);
}
template <class Object>
requires std::convertible_to<const std::remove_reference_t<Object>*, const owner_type*>
decltype(auto) get(const Object& object) const noexcept(noexcept(Accessor::get(object))) {
return Accessor::get(object);
}
const std::string& name() const noexcept {
return name_;
}
@@ -169,6 +215,10 @@ public:
bool is_list_visible() const noexcept {
return list_visible_;
}
protected:
const Derived& derived() const noexcept {
return static_cast<const Derived&>(*this);
}
private:
std::string name_;
std::string label_;
@@ -186,10 +236,26 @@ private:
int order_{-1};
bool sortable_{};
};
template <auto Member>
class Field_Descriptor : public Field_Descriptor_Base<Field_Descriptor<Member>, Member_Field_Accessor<Member>> {
public:
using Base = Field_Descriptor_Base<Field_Descriptor<Member>, Member_Field_Accessor<Member>>;
using Base::Base;
static constexpr auto member = Member;
};
template <class T, std::size_t Index>
class Reflected_Field_Descriptor : public Field_Descriptor_Base<Reflected_Field_Descriptor<T, Index>, Reflected_Field_Accessor<T, Index>> {
public:
using Base = Field_Descriptor_Base<Reflected_Field_Descriptor<T, Index>, Reflected_Field_Accessor<T, Index>>;
using Base::Base;
static constexpr std::size_t index = Index;
};
template <class T>
struct Is_Field_Descriptor : std::false_type {};
template <auto Member>
struct Is_Field_Descriptor<Field_Descriptor<Member>> : std::true_type {};
template <class T, std::size_t Index>
struct Is_Field_Descriptor<Reflected_Field_Descriptor<T, Index>> : std::true_type {};
template <class T>
concept Field_Descriptor_Type = Is_Field_Descriptor<std::remove_cvref_t<T>>::value;
template <auto Member>
@@ -200,6 +266,16 @@ template <auto Member>
auto field(std::string name, std::string label) {
return Field_Descriptor<Member>(std::move(name), std::move(label));
}
template <Reflected_Type T, std::size_t Index>
auto reflected_field() {
static_assert(Index < static_cast<std::size_t>(Reflection_Adapter<T>::field_count));
return Reflected_Field_Descriptor<T, Index>(std::string(Reflection_Adapter<T>::template name<Index>()));
}
template <Reflected_Type T, std::size_t Index>
auto reflected_field(std::string label) {
static_assert(Index < static_cast<std::size_t>(Reflection_Adapter<T>::field_count));
return Reflected_Field_Descriptor<T, Index>(std::string(Reflection_Adapter<T>::template name<Index>()), std::move(label));
}
struct No_Object_Validator {
template <class T>
void operator()(const T&) const noexcept {}
@@ -212,7 +288,7 @@ struct Object_List_Options {
std::string actions_label{"Actions"};
std::string confirm_label{"Confirm Changes"};
std::string delete_confirm{"Delete this record?"};
std::string view_status_label{"View status"};
std::string view_status_label{"View status"};
std::string management_label;
std::string default_order_by;
std::string default_order_dir{"asc"};
@@ -319,15 +395,42 @@ private:
};
template <class T, Field_Descriptor_Type... Fields>
auto object(std::string name, Fields... fields) {
static_assert((std::same_as<typename Fields::owner_type, T> && ...));
static_assert((std::convertible_to<T*, typename Fields::owner_type*> && ...));
const auto label = make_label(name);
return Object_Descriptor<T, No_Object_Validator, Fields...>(std::move(name), label, No_Object_Validator{}, std::tuple<Fields...>(std::move(fields)...));
}
template <class T, Field_Descriptor_Type... Fields>
auto object(std::string name, std::string label, Fields... fields) {
static_assert((std::same_as<typename Fields::owner_type, T> && ...));
static_assert((std::convertible_to<T*, typename Fields::owner_type*> && ...));
return Object_Descriptor<T, No_Object_Validator, Fields...>(std::move(name), std::move(label), No_Object_Validator{}, std::tuple<Fields...>(std::move(fields)...));
}
struct Identity_Field_Customizer {
template <std::size_t Index, Field_Descriptor_Type Field>
auto operator()(Field value) const {
return value;
}
};
template <Reflected_Type T, class Customizer, std::size_t... Indexes>
auto reflected_object_with_impl(std::string name, std::string label, Customizer customizer, std::index_sequence<Indexes...>) {
return object<T>(std::move(name), std::move(label), customizer.template operator()<Indexes>(reflected_field<T, Indexes>())...);
}
template <Reflected_Type T, class Customizer>
auto reflected_object_with(std::string name, Customizer customizer) {
const auto label = make_label(name);
return reflected_object_with_impl<T>(std::move(name), label, std::move(customizer), std::make_index_sequence<static_cast<std::size_t>(Reflection_Adapter<T>::field_count)>{});
}
template <Reflected_Type T, class Customizer>
auto reflected_object_with(std::string name, std::string label, Customizer customizer) {
return reflected_object_with_impl<T>(std::move(name), std::move(label), std::move(customizer), std::make_index_sequence<static_cast<std::size_t>(Reflection_Adapter<T>::field_count)>{});
}
template <Reflected_Type T>
auto reflected_object(std::string name) {
return reflected_object_with<T>(std::move(name), Identity_Field_Customizer{});
}
template <Reflected_Type T>
auto reflected_object(std::string name, std::string label) {
return reflected_object_with<T>(std::move(name), std::move(label), Identity_Field_Customizer{});
}
template <Described_Type T>
auto describe() {
return Type_Descriptor<std::remove_cvref_t<T>>::get();
@@ -335,3 +438,5 @@ auto describe() {
}
#define ADMINIVE_FIELD(Type, Member) ::adminive::field<&Type::Member>(#Member)
#define ADMINIVE_FIELD_LABEL(Type, Member, Label) ::adminive::field<&Type::Member>(#Member, Label)
#define ADMINIVE_REFLECTED_FIELD(Type, Index) ::adminive::reflected_field<Type, Index>()
#define ADMINIVE_REFLECTED_FIELD_LABEL(Type, Index, Label) ::adminive::reflected_field<Type, Index>(Label)
+92 -71
View File
@@ -13,14 +13,30 @@
#include <utility>
#include <vector>
namespace adminive {
inline void write_http_json(httplib::Response& response, const Json& value) {
response.set_content(value.dump(2), "application/json; charset=utf-8");
template <Json_Type Json>
void write_http_json(httplib::Response& response, const Json& value) {
response.set_content(dump_json(value, 2), "application/json; charset=utf-8");
}
inline void write_http_error(httplib::Response& response, int status, const Update_Result& result) {
template <Json_Type Json, class Data>
Json make_http_result(int status, std::string message, Data&& data) {
Json result = json_object<Json>();
json_set(result, "status", status);
json_set(result, "msg", std::move(message));
json_set(result, "data", std::forward<Data>(data));
return result;
}
template <Json_Type Json>
void write_http_error(httplib::Response& response, int status, const Update_Result& update) {
response.status = status;
write_http_json(response, Json{{"status", status}, {"msg", result.message}, {"data", Json::object()}, {"errors", result.field_errors}});
Json result = make_http_result<Json>(status, update.message, json_object<Json>());
Json errors = json_object<Json>();
for(const auto& [name, message] : update.field_errors) {
json_set(errors, name, message);
}
json_set(result, "errors", std::move(errors));
write_http_json(response, result);
}
template <Described_Type T>
template <Described_Type T, Json_Type Json>
requires std::copy_constructible<T> && std::assignable_from<T&, T>
class Http_Resource {
public:
@@ -28,37 +44,37 @@ public:
Http_Resource(T& value, std::string path, Commit_Function commit = {}, std::mutex* shared_mutex = nullptr) : value_(value), path_(std::move(path)), commit_(std::move(commit)), shared_mutex_(shared_mutex) {}
Json amis_schema() const {
std::scoped_lock lock(resource_mutex());
return to_amis_form_schema(value_, path_ + "/data", describe<T>().list_options().confirm_label);
return to_amis_form_schema<Json>(value_, path_ + "/data", describe<T>().list_options().confirm_label);
}
void bind(httplib::Server& server) {
server.Get(path_ + "/descriptor", [this](const httplib::Request&, httplib::Response& response) {
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", to_descriptor_json<T>()}});
write_http_json(response, make_http_result<Json>(0, "", to_descriptor_json<Json, T>()));
});
server.Get(path_ + "/data", [this](const httplib::Request&, httplib::Response& response) {
std::scoped_lock lock(resource_mutex());
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", to_frontend_json(value_)}});
write_http_json(response, make_http_result<Json>(0, "", to_frontend_json<Json>(value_)));
});
server.Get(path_ + "/amis", [this](const httplib::Request&, httplib::Response& response) {
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", amis_schema()}});
write_http_json(response, make_http_result<Json>(0, "", amis_schema()));
});
server.Post(path_ + "/data", [this](const httplib::Request& request, httplib::Response& response) {
try {
const Json patch = Json::parse(request.body);
const Json patch = parse_json<Json>(request.body);
std::scoped_lock lock(resource_mutex());
T candidate = value_;
const auto result = apply_frontend_patch(candidate, patch);
const auto result = apply_frontend_patch<Json>(candidate, patch);
if(!result.success) {
write_http_error(response, 422, result);
write_http_error<Json>(response, 422, result);
return;
}
if(commit_) {
commit_(candidate);
}
value_ = std::move(candidate);
write_http_json(response, Json{{"status", 0}, {"msg", result.message}, {"data", to_frontend_json(value_)}});
write_http_json(response, make_http_result<Json>(0, result.message, to_frontend_json<Json>(value_)));
} catch(const std::exception& error) {
response.status = 400;
write_http_json(response, Json{{"status", 400}, {"msg", error.what()}, {"data", Json::object()}});
write_http_json(response, make_http_result<Json>(400, error.what(), json_object<Json>()));
}
});
}
@@ -72,7 +88,7 @@ private:
std::mutex* shared_mutex_{};
mutable std::mutex mutex_;
};
template <Described_Type T>
template <Described_Type T, Json_Type Json>
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
class Http_Collection_Resource {
public:
@@ -83,16 +99,15 @@ public:
}
template <auto Function>
void register_status(std::uint64_t interval = 2000) {
static_assert(std::is_member_function_pointer_v<decltype(Function)>);
using Traits = Member_Function_Traits<Function>;
using Owner = typename Traits::owner_type;
using Status = std::remove_cvref_t<typename Traits::result_type>;
static_assert(std::same_as<Owner, T>);
using Function_Type = decltype(Function);
static_assert(std::is_member_function_pointer_v<Function_Type>);
static_assert(std::is_invocable_v<Function_Type, T&>);
using Status = std::remove_cvref_t<std::invoke_result_t<Function_Type, T&>>;
static_assert(Described_Type<Status>);
status_descriptor_ = to_status_descriptor_json<Status>();
status_descriptor_ = to_status_descriptor_json<Json, Status>();
status_interval_ = interval;
status_reader_ = [](T& value) {
return to_status_json(std::invoke(Function, value));
return to_status_json<Json>(std::invoke(Function, value));
};
}
std::size_t size() const {
@@ -101,29 +116,29 @@ public:
}
Json items_json() const {
std::scoped_lock lock(mutex_);
Json items = Json::array();
Json items = json_array<Json>();
for(const auto& entry : entries_) {
items.push_back(encode_entry(entry));
json_append(items, encode_entry(entry));
}
return items;
}
Json amis_schema() const {
const Json* descriptor = status_reader_ ? &status_descriptor_ : nullptr;
if(overview_status_api_.empty()) {
return to_amis_crud_schema<T>(path_, descriptor, status_interval_);
return to_amis_crud_schema<Json, T>(path_, descriptor, status_interval_);
}
return to_amis_crud_status_schema<T>(path_, overview_status_api_, descriptor, status_interval_);
return to_amis_crud_status_schema<Json, T>(path_, overview_status_api_, descriptor, status_interval_);
}
void bind(httplib::Server& server) {
server.Get(path_ + "/descriptor", [this](const httplib::Request&, httplib::Response& response) {
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", to_descriptor_json<T>()}});
write_http_json(response, make_http_result<Json>(0, "", to_descriptor_json<Json, T>()));
});
server.Get(path_ + "/amis", [this](const httplib::Request&, httplib::Response& response) {
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", amis_schema()}});
write_http_json(response, make_http_result<Json>(0, "", amis_schema()));
});
if(status_reader_) {
server.Get(path_ + "/status/descriptor", [this](const httplib::Request&, httplib::Response& response) {
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", status_descriptor_}});
write_http_json(response, make_http_result<Json>(0, "", status_descriptor_));
});
server.Get(status_pattern(), [this](const httplib::Request& request, httplib::Response& response) {
const auto id = read_route_id(request);
@@ -133,7 +148,7 @@ public:
write_not_found(response);
return;
}
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", status_reader_(iterator->value)}});
write_http_json(response, make_http_result<Json>(0, "", status_reader_(iterator->value)));
});
}
server.Get(path_, [this](const httplib::Request& request, httplib::Response& response) {
@@ -148,11 +163,14 @@ public:
apply_request_sort(request, ordered_entries);
const std::size_t offset = std::min((page - 1) * per_page, ordered_entries.size());
const std::size_t end = std::min(offset + per_page, ordered_entries.size());
Json items = Json::array();
Json items = json_array<Json>();
for(std::size_t index = offset; index < end; ++index) {
items.push_back(encode_entry(*ordered_entries[index]));
json_append(items, encode_entry(*ordered_entries[index]));
}
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", Json{{"items", std::move(items)}, {"total", ordered_entries.size()}}}});
Json data = json_object<Json>();
json_set(data, "items", std::move(items));
json_set(data, "total", ordered_entries.size());
write_http_json(response, make_http_result<Json>(0, "", std::move(data)));
});
server.Get(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) {
const auto id = read_route_id(request);
@@ -162,37 +180,37 @@ public:
write_not_found(response);
return;
}
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", encode_entry(*iterator)}});
write_http_json(response, make_http_result<Json>(0, "", encode_entry(*iterator)));
});
server.Post(path_, [this](const httplib::Request& request, httplib::Response& response) {
try {
Json input = Json::parse(request.body);
input.erase("id");
Json input = parse_json<Json>(request.body);
Json_Adapter<Json>::erase(input, "id");
T value{};
const auto result = apply_frontend_create(value, input);
const auto result = apply_frontend_create<Json>(value, input);
if(!result.success) {
write_http_error(response, 422, result);
write_http_error<Json>(response, 422, result);
return;
}
std::scoped_lock lock(mutex_);
entries_.push_back(Entry{next_id_++, std::move(value)});
write_http_json(response, Json{{"status", 0}, {"msg", "created"}, {"data", encode_entry(entries_.back())}});
write_http_json(response, make_http_result<Json>(0, "created", encode_entry(entries_.back())));
} catch(const std::exception& error) {
response.status = 400;
write_http_json(response, Json{{"status", 400}, {"msg", error.what()}, {"data", Json::object()}});
write_http_json(response, make_http_result<Json>(400, error.what(), json_object<Json>()));
}
});
if(describe<T>().list_options().user_reorderable_) {
server.Post(path_ + "/order", [this](const httplib::Request& request, httplib::Response& response) {
try {
const Json input = Json::parse(request.body);
const auto ids = read_order_ids(input.at("ids"));
const Json input = parse_json<Json>(request.body);
const auto ids = read_order_ids(Json_Adapter<Json>::at(input, "ids"));
std::scoped_lock lock(mutex_);
reorder(ids);
write_http_json(response, Json{{"status", 0}, {"msg", "reordered"}, {"data", Json::object()}});
write_http_json(response, make_http_result<Json>(0, "reordered", json_object<Json>()));
} catch(const std::exception& error) {
response.status = 400;
write_http_json(response, Json{{"status", 400}, {"msg", error.what()}, {"data", Json::object()}});
write_http_json(response, make_http_result<Json>(400, error.what(), json_object<Json>()));
}
});
}
@@ -211,7 +229,7 @@ public:
return;
}
entries_.erase(iterator);
write_http_json(response, Json{{"status", 0}, {"msg", "deleted"}, {"data", Json::object()}});
write_http_json(response, make_http_result<Json>(0, "deleted", json_object<Json>()));
});
}
private:
@@ -233,23 +251,23 @@ private:
return value;
}
static int compare_json_values(const Json& left, const Json& right) {
if(left.is_number() && right.is_number()) {
const long double left_value = left.get<long double>();
const long double right_value = right.get<long double>();
if(Json_Adapter<Json>::is_number(left) && Json_Adapter<Json>::is_number(right)) {
const long double left_value = Json_Adapter<Json>::number(left);
const long double right_value = Json_Adapter<Json>::number(right);
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
}
if(left.is_string() && right.is_string()) {
const auto& left_value = left.get_ref<const std::string&>();
const auto& right_value = right.get_ref<const std::string&>();
if(Json_Adapter<Json>::is_string(left) && Json_Adapter<Json>::is_string(right)) {
const auto left_value = json_get<Json, std::string>(left);
const auto right_value = json_get<Json, std::string>(right);
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
}
if(left.is_boolean() && right.is_boolean()) {
const bool left_value = left.get<bool>();
const bool right_value = right.get<bool>();
if(Json_Adapter<Json>::is_boolean(left) && Json_Adapter<Json>::is_boolean(right)) {
const bool left_value = json_get<Json, bool>(left);
const bool right_value = json_get<Json, bool>(right);
return left_value == right_value ? 0 : left_value ? 1 : -1;
}
const std::string left_value = left.dump();
const std::string right_value = right.dump();
const std::string left_value = dump_json(left);
const std::string right_value = dump_json(right);
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
}
static bool is_sortable_field(const std::string& name) {
@@ -273,13 +291,14 @@ private:
}
static std::vector<std::uint64_t> read_order_ids(const Json& value) {
std::vector<std::uint64_t> result;
if(value.is_array()) {
for(const auto& item : value) {
result.push_back(item.is_string() ? std::stoull(item.get<std::string>()) : item.get<std::uint64_t>());
if(Json_Adapter<Json>::is_array(value)) {
for(std::size_t index = 0; index < Json_Adapter<Json>::size(value); ++index) {
const auto& item = Json_Adapter<Json>::at(value, index);
result.push_back(Json_Adapter<Json>::is_string(item) ? std::stoull(json_get<Json, std::string>(item)) : json_get<Json, std::uint64_t>(item));
}
return result;
}
std::string text = value.get<std::string>();
const std::string text = json_get<Json, std::string>(value);
std::size_t begin{};
while(begin < text.size()) {
const auto end = text.find(',', begin);
@@ -308,7 +327,9 @@ private:
if(order_by == "id") {
comparison = left->id < right->id ? -1 : left->id > right->id ? 1 : 0;
} else {
comparison = compare_json_values(to_frontend_json(left->value).at(order_by), to_frontend_json(right->value).at(order_by));
const auto left_json = to_frontend_json<Json>(left->value);
const auto right_json = to_frontend_json<Json>(right->value);
comparison = compare_json_values(Json_Adapter<Json>::at(left_json, order_by), Json_Adapter<Json>::at(right_json, order_by));
}
return descending ? comparison > 0 : comparison < 0;
});
@@ -325,13 +346,13 @@ private:
});
}
static Json encode_entry(const Entry& entry) {
Json result = to_frontend_json(entry.value);
result["id"] = entry.id;
Json result = to_frontend_json<Json>(entry.value);
json_set(result, "id", entry.id);
return result;
}
static void write_not_found(httplib::Response& response) {
response.status = 404;
write_http_json(response, Json{{"status", 404}, {"msg", "record not found"}, {"data", Json::object()}});
write_http_json(response, make_http_result<Json>(404, "record not found", json_object<Json>()));
}
void reorder(const std::vector<std::uint64_t>& ids) {
std::vector<Entry> remaining = std::move(entries_);
@@ -353,8 +374,8 @@ private:
}
void update(const httplib::Request& request, httplib::Response& response) {
try {
Json patch = Json::parse(request.body);
patch.erase("id");
Json patch = parse_json<Json>(request.body);
Json_Adapter<Json>::erase(patch, "id");
const auto id = read_route_id(request);
std::scoped_lock lock(mutex_);
const auto iterator = find_entry(id);
@@ -362,20 +383,20 @@ private:
write_not_found(response);
return;
}
const auto result = apply_frontend_patch(iterator->value, patch);
const auto result = apply_frontend_patch<Json>(iterator->value, patch);
if(!result.success) {
write_http_error(response, 422, result);
write_http_error<Json>(response, 422, result);
return;
}
write_http_json(response, Json{{"status", 0}, {"msg", result.message}, {"data", encode_entry(*iterator)}});
write_http_json(response, make_http_result<Json>(0, result.message, encode_entry(*iterator)));
} catch(const std::exception& error) {
response.status = 400;
write_http_json(response, Json{{"status", 400}, {"msg", error.what()}, {"data", Json::object()}});
write_http_json(response, make_http_result<Json>(400, error.what(), json_object<Json>()));
}
}
std::string path_;
std::string overview_status_api_;
Json status_descriptor_;
Json status_descriptor_{};
std::function<Json(T&)> status_reader_;
std::uint64_t status_interval_{2000};
std::vector<Entry> entries_;
+210 -154
View File
@@ -1,20 +1,28 @@
#pragma once
#include "adminive/adapter.hpp"
#include "adminive/descriptor.hpp"
#include "magic_enum/magic_enum.hpp"
#include "nlohmann/json.hpp"
#include <concepts>
#include <map>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
namespace adminive {
using Json = nlohmann::json;
struct Update_Result {
bool success{};
std::string message;
std::map<std::string, std::string> field_errors;
template <Json_Type Json>
Json to_json() const {
return Json{{"success", success}, {"message", message}, {"field_errors", field_errors}};
Json result = json_object<Json>();
json_set(result, "success", success);
json_set(result, "message", message);
Json errors = json_object<Json>();
for(const auto& [name, error] : field_errors) {
json_set(errors, name, error);
}
json_set(result, "field_errors", std::move(errors));
return result;
}
};
class Json_Assignment_Error : public std::invalid_argument {
@@ -31,281 +39,329 @@ enum class Write_Mode {
create,
update
};
template <Described_Type T>
template <Json_Type Json, Described_Type T>
Json to_json(const T& value);
template <Described_Type T>
template <Json_Type Json, Described_Type T>
Json to_frontend_json(const T& value);
template <Described_Type T>
template <Json_Type Json, Described_Type T>
requires std::default_initializable<T>
Json to_descriptor_json();
template <Described_Type T>
template <Json_Type Json, Described_Type T>
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
Update_Result assign_json(T& target, const Json& value);
template <class T>
template <Json_Type Json, class T>
void assign_json_value(T& target, const Json& value);
template <class T>
template <Json_Type Json, class T>
Json encode_json_value(const T& value) {
using Value = std::remove_cvref_t<T>;
if constexpr (Range_Value_Type<Value> || Validated_Value_Type<Value>) {
return encode_json_value(value.value());
} else if constexpr (std::is_enum_v<Value>) {
const auto name = magic_enum::enum_name(value);
if (!name.empty()) {
return std::string(name);
using Storage = std::remove_cvref_t<T>;
using Value = Adapted_Value_Type<Storage, Json>;
if constexpr(Value_Adapter_With_Encode<Storage, Json>) {
return Value_Adapter<Storage, Json>::encode(value);
} else if constexpr(!std::same_as<Storage, Value>) {
return encode_json_value<Json>(read_adapted_value<Storage, Json>(value));
} else if constexpr(std::is_enum_v<Value>) {
static_assert(Enum_Type<Value>, "enum type requires an adminive::Enum_Adapter specialization");
const auto name = enum_name(value);
if(!name.empty()) {
return json_scalar<Json>(std::string(name));
}
return static_cast<std::underlying_type_t<Value>>(value);
} else if constexpr (Described_Type<Value>) {
return to_json(value);
} else if constexpr (String_Key_Map_Type<Value>) {
Json result = Json::object();
for (const auto& [key, item] : value) {
result[std::string(key)] = encode_json_value(item);
return json_scalar<Json>(static_cast<std::underlying_type_t<Value>>(value));
} else if constexpr(Described_Type<Value>) {
return to_json<Json>(value);
} else if constexpr(String_Key_Map_Type<Value>) {
Json result = json_object<Json>();
for(const auto& [key, item] : value) {
json_set(result, std::string(key), encode_json_value<Json>(item));
}
return result;
} else if constexpr (Sequence_Type<Value>) {
Json result = Json::array();
for (const auto& item : value) {
result.push_back(encode_json_value(item));
} else if constexpr(Sequence_Type<Value>) {
Json result = json_array<Json>();
for(const auto& item : value) {
json_append(result, encode_json_value<Json>(item));
}
return result;
} else {
return value;
return json_scalar<Json>(value);
}
}
template <class T>
template <Json_Type Json, class T>
void assign_json_value(T& target, const Json& value) {
using Value = std::remove_cvref_t<T>;
if constexpr (Range_Value_Type<Value> || Validated_Value_Type<Value>) {
using Inner = typename Value::value_type;
target = value.get<Inner>();
} else if constexpr (std::is_enum_v<Value>) {
if (value.is_string()) {
const auto parsed = magic_enum::enum_cast<Value>(value.get<std::string>());
if (!parsed) {
throw std::invalid_argument("unknown enum value");
}
target = *parsed;
} else {
const auto parsed = magic_enum::enum_cast<Value>(value.get<std::underlying_type_t<Value>>());
if (!parsed) {
throw std::invalid_argument("unknown enum value");
}
target = *parsed;
using Storage = std::remove_cvref_t<T>;
using Value = Adapted_Value_Type<Storage, Json>;
if constexpr(Value_Adapter_With_Decode<Storage, Json>) {
Value_Adapter<Storage, Json>::decode(target, value);
} else if constexpr(!std::same_as<Storage, Value>) {
Value parsed{};
assign_json_value<Json>(parsed, value);
write_adapted_value<Storage, Json>(target, std::move(parsed));
} else if constexpr(std::is_enum_v<Value>) {
static_assert(Enum_Type<Value>, "enum type requires an adminive::Enum_Adapter specialization");
std::optional<Value> parsed;
if(Json_Adapter<Json>::is_string(value)) {
parsed = enum_cast<Value>(json_get<Json, std::string>(value));
} else if constexpr(Enum_Adapter_With_Underlying_Cast<Value>) {
parsed = Enum_Adapter<Value>::cast(json_get<Json, std::underlying_type_t<Value>>(value));
}
} else if constexpr (Described_Type<Value>) {
const auto result = assign_json(target, value);
if (!result.success) {
if(!parsed) {
throw std::invalid_argument("unknown enum value");
}
target = *parsed;
} else if constexpr(Described_Type<Value>) {
const auto result = assign_json<Json>(target, value);
if(!result.success) {
throw Json_Assignment_Error(result);
}
} else if constexpr (String_Key_Map_Type<Value>) {
if (!value.is_object()) {
} else if constexpr(String_Key_Map_Type<Value>) {
if(!Json_Adapter<Json>::is_object(value)) {
throw std::invalid_argument("value must be a JSON object");
}
Value parsed;
for (auto iterator = value.begin(); iterator != value.end(); ++iterator) {
for(const auto& key : Json_Adapter<Json>::keys(value)) {
typename Value::mapped_type item{};
assign_json_value(item, iterator.value());
parsed.emplace(iterator.key(), std::move(item));
assign_json_value<Json>(item, Json_Adapter<Json>::at(value, key));
parsed.emplace(key, std::move(item));
}
target = std::move(parsed);
} else if constexpr (Sequence_Type<Value>) {
if (!value.is_array()) {
} else if constexpr(Sequence_Type<Value>) {
if(!Json_Adapter<Json>::is_array(value)) {
throw std::invalid_argument("value must be a JSON array");
}
Value parsed;
for (const auto& source : value) {
for(std::size_t index = 0; index < Json_Adapter<Json>::size(value); ++index) {
typename Value::value_type item{};
assign_json_value(item, source);
assign_json_value<Json>(item, Json_Adapter<Json>::at(value, index));
parsed.push_back(std::move(item));
}
target = std::move(parsed);
} else {
target = value.get<Value>();
target = json_get<Json, Value>(value);
}
}
template <class T>
template <Json_Type Json, class T>
std::string value_type_name() {
using Value = Unwrapped_Value_Type<T>;
if constexpr (std::is_enum_v<Value>) {
using Storage = std::remove_cvref_t<T>;
using Value = Adapted_Value_Type<Storage, Json>;
if constexpr(Value_Adapter_With_Type_Name<Storage, Json>) {
return std::string(Value_Adapter<Storage, Json>::type_name);
} else if constexpr(std::is_enum_v<Value>) {
return "enum";
} else if constexpr (std::same_as<Value, bool>) {
} else if constexpr(std::same_as<Value, bool>) {
return "boolean";
} else if constexpr (std::integral<Value>) {
} else if constexpr(std::integral<Value>) {
return "integer";
} else if constexpr (std::floating_point<Value>) {
} else if constexpr(std::floating_point<Value>) {
return "number";
} else if constexpr (String_Type<Value>) {
} else if constexpr(String_Type<Value>) {
return "string";
} else if constexpr (Described_Type<Value>) {
} else if constexpr(Described_Type<Value>) {
return "object";
} else if constexpr (String_Key_Map_Type<Value>) {
} else if constexpr(String_Key_Map_Type<Value>) {
return "map";
} else if constexpr (Sequence_Type<Value>) {
} else if constexpr(Sequence_Type<Value>) {
return "array";
} else {
return "unknown";
}
}
template <class T>
template <Json_Type Json, class T>
void append_constraints(Json& result) {
using Value = std::remove_cvref_t<T>;
if constexpr (Range_Value_Type<Value>) {
result["minimum"] = Value::minimum;
result["maximum"] = Value::maximum;
append_constraints<typename Value::value_type>(result);
} else if constexpr (Validated_Value_Type<Value>) {
using Validator = typename Value::validator_type;
if constexpr (Multiple_Of_Validator<Validator>) {
result["multiple_of"] = Validator::multiple_of;
}
if constexpr (Validator_With_Message<Validator>) {
result["validation_message"] = std::string(Validator::message);
}
append_constraints<typename Value::value_type>(result);
using Storage = std::remove_cvref_t<T>;
using Value = Adapted_Value_Type<Storage, Json>;
if constexpr(Value_Adapter_With_Minimum<Storage, Json>) {
json_set(result, "minimum", Value_Adapter<Storage, Json>::minimum);
}
if constexpr(Value_Adapter_With_Maximum<Storage, Json>) {
json_set(result, "maximum", Value_Adapter<Storage, Json>::maximum);
}
if constexpr(Value_Adapter_With_Multiple_Of<Storage, Json>) {
json_set(result, "multiple_of", Value_Adapter<Storage, Json>::multiple_of);
}
if constexpr(Value_Adapter_With_Validation_Message<Storage, Json>) {
json_set(result, "validation_message", std::string(Value_Adapter<Storage, Json>::validation_message));
}
if constexpr(!std::same_as<Storage, Value>) {
append_constraints<Json, Value>(result);
}
if constexpr(Value_Adapter_With_Schema<Storage, Json>) {
Value_Adapter<Storage, Json>::append_schema(result);
}
}
template <class T>
template <Json_Type Json, class T>
Json enum_options(const std::map<std::string, std::string>& labels) {
using Value = Unwrapped_Value_Type<T>;
Json result = Json::array();
using Value = Adapted_Value_Type<T, Json>;
Json result = json_array<Json>();
if constexpr(std::is_enum_v<Value>) {
for(const auto value : magic_enum::enum_values<Value>()) {
const std::string name(magic_enum::enum_name(value));
static_assert(Enum_Type<Value>, "enum type requires an adminive::Enum_Adapter specialization");
for(const auto value : enum_values<Value>()) {
const std::string name(enum_name(value));
const auto iterator = labels.find(name);
result.push_back(Json{{"label", iterator == labels.end() ? make_label(name) : iterator->second}, {"value", name}});
Json option = json_object<Json>();
json_set(option, "label", iterator == labels.end() ? make_label(name) : iterator->second);
json_set(option, "value", name);
json_append(result, std::move(option));
}
}
return result;
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
requires std::default_initializable<T>
Json to_descriptor_json() {
const auto descriptor = describe<T>();
T defaults{};
Json fields = Json::array();
Json fields = json_array<Json>();
int default_order{};
std::apply([&](const auto&... item) {
([&] {
using Field = std::remove_cvref_t<decltype(item)>;
using Member = typename Field::member_type;
using Value = Adapted_Value_Type<Member, Json>;
const int order = item.order() < 0 ? default_order : item.order();
++default_order;
Json field_json{{"name", item.name()}, {"label", item.label()}, {"list_label", item.list_label()}, {"value_type", value_type_name<Member>()}, {"readable", item.is_readable()}, {"editable", item.is_editable()}, {"creatable", item.is_creatable()}, {"visible", item.is_visible()}, {"required", item.is_required()}, {"list_visible", item.is_list_visible()}, {"order", order}, {"sortable", item.is_sortable()}, {"default", encode_json_value(defaults.*Field::member)}};
if (!item.description().empty()) {
field_json["description"] = item.description();
Json field_json = json_object<Json>();
json_set(field_json, "name", item.name());
json_set(field_json, "label", item.label());
json_set(field_json, "list_label", item.list_label());
json_set(field_json, "value_type", value_type_name<Json, Member>());
json_set(field_json, "readable", item.is_readable());
json_set(field_json, "editable", item.is_editable());
json_set(field_json, "creatable", item.is_creatable());
json_set(field_json, "visible", item.is_visible());
json_set(field_json, "required", item.is_required());
json_set(field_json, "list_visible", item.is_list_visible());
json_set(field_json, "order", order);
json_set(field_json, "sortable", item.is_sortable());
json_set(field_json, "default", encode_json_value<Json>(item.get(defaults)));
if(!item.description().empty()) {
json_set(field_json, "description", item.description());
}
if(!item.widget().empty()) {
field_json["widget"] = item.widget();
json_set(field_json, "widget", item.widget());
}
if(!item.visible_on().empty()) {
field_json["visible_on"] = item.visible_on();
json_set(field_json, "visible_on", item.visible_on());
}
if constexpr(Described_Type<Member>) {
field_json["children"] = to_descriptor_json<Member>().at("fields");
if constexpr(Described_Type<Value>) {
json_set(field_json, "children", Json_Adapter<Json>::at(to_descriptor_json<Json, Value>(), "fields"));
}
append_constraints<Member>(field_json);
const auto options = enum_options<Member>(item.enum_labels());
if (!options.empty()) {
field_json["options"] = options;
append_constraints<Json, Member>(field_json);
auto options = enum_options<Json, Member>(item.enum_labels());
if(Json_Adapter<Json>::size(options) != 0) {
json_set(field_json, "options", std::move(options));
}
fields.push_back(std::move(field_json));
json_append(fields, std::move(field_json));
}(), ...);
}, descriptor.fields());
const auto& list_options = descriptor.list_options();
Json list{{"id_label", list_options.id_label}, {"create_label", list_options.create_label}, {"edit_label", list_options.edit_label}, {"delete_label", list_options.delete_label}, {"actions_label", list_options.actions_label}, {"confirm_label", list_options.confirm_label}, {"delete_confirm", list_options.delete_confirm}, {"view_status_label", list_options.view_status_label}, {"management_label", list_options.management_label.empty() ? descriptor.label() + " Management" : list_options.management_label}, {"default_order_by", list_options.default_order_by}, {"default_order_dir", list_options.default_order_dir}, {"user_reorderable", list_options.user_reorderable_}, {"column_reorderable", list_options.column_reorderable_}};
return Json{{"protocol", "adminive.resource"}, {"protocol_version", 1}, {"name", descriptor.name()}, {"label", descriptor.label()}, {"value_type", "object"}, {"list", std::move(list)}, {"fields", std::move(fields)}};
const auto& options = descriptor.list_options();
Json list = json_object<Json>();
json_set(list, "id_label", options.id_label);
json_set(list, "create_label", options.create_label);
json_set(list, "edit_label", options.edit_label);
json_set(list, "delete_label", options.delete_label);
json_set(list, "actions_label", options.actions_label);
json_set(list, "confirm_label", options.confirm_label);
json_set(list, "delete_confirm", options.delete_confirm);
json_set(list, "view_status_label", options.view_status_label);
json_set(list, "management_label", options.management_label.empty() ? descriptor.label() + " Management" : options.management_label);
json_set(list, "default_order_by", options.default_order_by);
json_set(list, "default_order_dir", options.default_order_dir);
json_set(list, "user_reorderable", options.user_reorderable_);
json_set(list, "column_reorderable", options.column_reorderable_);
Json result = json_object<Json>();
json_set(result, "protocol", "adminive.resource");
json_set(result, "protocol_version", 1);
json_set(result, "name", descriptor.name());
json_set(result, "label", descriptor.label());
json_set(result, "value_type", "object");
json_set(result, "list", std::move(list));
json_set(result, "fields", std::move(fields));
return result;
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
Json to_json(const T& value) {
const auto descriptor = describe<T>();
Json result = Json::object();
Json result = json_object<Json>();
std::apply([&](const auto&... item) {
([&] {
using Field = std::remove_cvref_t<decltype(item)>;
result[item.name()] = encode_json_value(value.*Field::member);
}(), ...);
(json_set(result, item.name(), encode_json_value<Json>(item.get(value))), ...);
}, descriptor.fields());
return result;
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
Json to_frontend_json(const T& value) {
const auto descriptor = describe<T>();
Json result = Json::object();
Json result = json_object<Json>();
std::apply([&](const auto&... item) {
([&] {
using Field = std::remove_cvref_t<decltype(item)>;
if (item.is_readable()) {
result[item.name()] = encode_json_value(value.*Field::member);
if(item.is_readable()) {
json_set(result, item.name(), encode_json_value<Json>(item.get(value)));
}
}(), ...);
}, descriptor.fields());
return result;
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
Json to_data_json(const T& value) {
return to_frontend_json(value);
return to_frontend_json<Json>(value);
}
template <class Field>
bool field_writable(const Field& field, Write_Mode mode) {
if (mode == Write_Mode::internal) {
if(mode == Write_Mode::internal) {
return true;
}
if (mode == Write_Mode::create) {
if(mode == Write_Mode::create) {
return field.is_creatable();
}
return field.is_editable();
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
requires std::copy_constructible<T> && std::assignable_from<T&, T>
Update_Result apply_object_json(T& target, const Json& value, Write_Mode mode, bool complete) {
Update_Result result;
if (!value.is_object()) {
if(!Json_Adapter<Json>::is_object(value)) {
result.message = "value must be a JSON object";
return result;
}
T candidate = target;
const auto descriptor = describe<T>();
for (auto iterator = value.begin(); iterator != value.end(); ++iterator) {
for(const auto& key : Json_Adapter<Json>::keys(value)) {
bool known{};
std::apply([&](const auto&... item) {
known = ((iterator.key() == item.name()) || ...);
known = ((key == item.name()) || ...);
}, descriptor.fields());
if (!known) {
result.field_errors[iterator.key()] = "unknown field";
if(!known) {
result.field_errors[key] = "unknown field";
}
}
std::apply([&](const auto&... item) {
([&] {
using Field = std::remove_cvref_t<decltype(item)>;
const auto iterator = value.find(item.name());
const bool present = Json_Adapter<Json>::contains(value, item.name());
const bool writable = field_writable(item, mode);
if (complete && item.is_required() && writable && iterator == value.end()) {
if(complete && item.is_required() && writable && !present) {
result.field_errors[item.name()] = "field is required";
return;
}
if (iterator == value.end()) {
if(!present) {
return;
}
if (!writable) {
if(!writable) {
result.field_errors[item.name()] = mode == Write_Mode::create ? "field is not creatable" : "field is not editable";
return;
}
try {
assign_json_value(candidate.*Field::member, *iterator);
} catch (const Json_Assignment_Error& error) {
assign_json_value<Json>(item.get(candidate), Json_Adapter<Json>::at(value, item.name()));
} catch(const Json_Assignment_Error& error) {
result.field_errors[item.name()] = error.what();
} catch (const std::exception& error) {
} catch(const std::exception& error) {
result.field_errors[item.name()] = error.what();
}
}(), ...);
}, descriptor.fields());
if (!result.field_errors.empty()) {
if(!result.field_errors.empty()) {
result.message = "one or more fields are invalid";
return result;
}
try {
descriptor.object_validator()(candidate);
} catch (const std::exception& error) {
} catch(const std::exception& error) {
result.message = error.what();
return result;
}
@@ -314,42 +370,42 @@ Update_Result apply_object_json(T& target, const Json& value, Write_Mode mode, b
result.message = complete ? "assigned" : "updated";
return result;
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
Update_Result assign_json(T& target, const Json& value) {
T candidate{};
auto result = apply_object_json(candidate, value, Write_Mode::internal, true);
if (result.success) {
auto result = apply_object_json<Json>(candidate, value, Write_Mode::internal, true);
if(result.success) {
target = std::move(candidate);
}
return result;
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
requires std::copy_constructible<T> && std::assignable_from<T&, T>
Update_Result apply_json_patch(T& target, const Json& patch) {
return apply_object_json(target, patch, Write_Mode::internal, false);
return apply_object_json<Json>(target, patch, Write_Mode::internal, false);
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
requires std::copy_constructible<T> && std::assignable_from<T&, T>
Update_Result apply_frontend_create(T& target, const Json& value) {
return apply_object_json(target, value, Write_Mode::create, true);
return apply_object_json<Json>(target, value, Write_Mode::create, true);
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
requires std::copy_constructible<T> && std::assignable_from<T&, T>
Update_Result apply_frontend_patch(T& target, const Json& patch) {
return apply_object_json(target, patch, Write_Mode::update, false);
return apply_object_json<Json>(target, patch, Write_Mode::update, false);
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
requires std::copy_constructible<T> && std::assignable_from<T&, T>
Update_Result apply_patch(T& target, const Json& patch) {
return apply_frontend_patch(target, patch);
return apply_frontend_patch<Json>(target, patch);
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
T from_json(const Json& value) {
T result{};
auto update = assign_json(result, value);
if (!update.success) {
auto update = assign_json<Json>(result, value);
if(!update.success) {
throw Json_Assignment_Error(std::move(update));
}
return result;
@@ -361,7 +417,7 @@ Update_Result validate(const T& value) {
describe<T>().object_validator()(value);
result.success = true;
result.message = "valid";
} catch (const std::exception& error) {
} catch(const std::exception& error) {
result.message = error.what();
}
return result;
+26 -38
View File
@@ -1,6 +1,5 @@
#pragma once
#include "adminive/json.hpp"
#include <functional>
#include <string>
#include <type_traits>
#include <utility>
@@ -31,64 +30,53 @@ struct Status_Inner_Value<Status_Value<T>> {
};
template <class T>
using Status_Inner_Value_Type = typename Status_Inner_Value<std::remove_cvref_t<T>>::type;
template <class T>
template <Json_Type Json, class T>
Json encode_status_field(const T& value) {
Json result = json_object<Json>();
if constexpr(Status_Value_Type<T>) {
return Json{{"value", encode_json_value(value.value)}, {"color", value.color}};
json_set(result, "value", encode_json_value<Json>(value.value));
json_set(result, "color", value.color);
} else {
return Json{{"value", encode_json_value(value)}};
json_set(result, "value", encode_json_value<Json>(value));
}
return result;
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
Json to_status_descriptor_json() {
const auto descriptor = describe<T>();
Json fields = Json::array();
Json fields = json_array<Json>();
std::apply([&](const auto&... item) {
([&] {
using Field = std::remove_cvref_t<decltype(item)>;
using Member = typename Field::member_type;
using Value = Status_Inner_Value_Type<Member>;
Json field_json{{"name", item.name()}, {"label", item.label()}, {"value_type", value_type_name<Value>()}, {"color", Status_Value_Type<Member>}};
Json field = json_object<Json>();
json_set(field, "name", item.name());
json_set(field, "label", item.label());
json_set(field, "value_type", value_type_name<Json, Value>());
json_set(field, "color", Status_Value_Type<Member>);
if(!item.description().empty()) {
field_json["description"] = item.description();
json_set(field, "description", item.description());
}
fields.push_back(std::move(field_json));
json_append(fields, std::move(field));
}(), ...);
}, descriptor.fields());
return Json{{"protocol", "adminive.status"}, {"protocol_version", 1}, {"name", descriptor.name()}, {"label", descriptor.label()}, {"value_type", "status_object"}, {"fields", std::move(fields)}};
Json result = json_object<Json>();
json_set(result, "protocol", "adminive.status");
json_set(result, "protocol_version", 1);
json_set(result, "name", descriptor.name());
json_set(result, "label", descriptor.label());
json_set(result, "value_type", "status_object");
json_set(result, "fields", std::move(fields));
return result;
}
template <Described_Type T>
template <Json_Type Json, Described_Type T>
Json to_status_json(const T& value) {
const auto descriptor = describe<T>();
Json result = Json::object();
Json result = json_object<Json>();
std::apply([&](const auto&... item) {
([&] {
using Field = std::remove_cvref_t<decltype(item)>;
result[item.name()] = encode_status_field(value.*Field::member);
}(), ...);
(json_set(result, item.name(), encode_status_field<Json>(item.get(value))), ...);
}, descriptor.fields());
return result;
}
template <auto Function>
struct Member_Function_Traits;
template <class Owner, class Result, Result (Owner::*Function)()>
struct Member_Function_Traits<Function> {
using owner_type = Owner;
using result_type = Result;
};
template <class Owner, class Result, Result (Owner::*Function)() const>
struct Member_Function_Traits<Function> {
using owner_type = Owner;
using result_type = Result;
};
template <class Owner, class Result, Result (Owner::*Function)() noexcept>
struct Member_Function_Traits<Function> {
using owner_type = Owner;
using result_type = Result;
};
template <class Owner, class Result, Result (Owner::*Function)() const noexcept>
struct Member_Function_Traits<Function> {
using owner_type = Owner;
using result_type = Result;
};
}
+36
View File
@@ -1,6 +1,8 @@
#pragma once
#include "adminive/adapter.hpp"
#include <concepts>
#include <stdexcept>
#include <string_view>
#include <type_traits>
#include <utility>
namespace adminive {
@@ -85,4 +87,38 @@ template <class T, class Validator>
struct Unwrapped_Value<Validated_Value<T, Validator>> : Unwrapped_Value<T> {};
template <class T>
using Unwrapped_Value_Type = typename Unwrapped_Value<std::remove_cvref_t<T>>::type;
template <class T, T Minimum, T Maximum, class Json>
struct Value_Adapter<Range_Value<T, Minimum, Maximum>, Json> {
using value_type = T;
static constexpr T minimum = Minimum;
static constexpr T maximum = Maximum;
static const T& read(const Range_Value<T, Minimum, Maximum>& value) noexcept {
return value.value();
}
static void write(Range_Value<T, Minimum, Maximum>& target, T value) {
target = value;
}
};
template <class Validator, class = void>
struct Validator_Multiple_Of_Metadata {};
template <class Validator>
struct Validator_Multiple_Of_Metadata<Validator, std::void_t<decltype(Validator::multiple_of)>> {
static constexpr auto multiple_of = Validator::multiple_of;
};
template <class Validator, class = void>
struct Validator_Message_Metadata {};
template <class Validator>
struct Validator_Message_Metadata<Validator, std::void_t<decltype(Validator::message)>> {
static constexpr std::string_view validation_message = Validator::message;
};
template <class T, class Validator, class Json>
struct Value_Adapter<Validated_Value<T, Validator>, Json> : Validator_Multiple_Of_Metadata<Validator>, Validator_Message_Metadata<Validator> {
using value_type = T;
static const T& read(const Validated_Value<T, Validator>& value) noexcept {
return value.value();
}
static void write(Validated_Value<T, Validator>& target, T value) {
target = value;
}
};
}
+146 -48
View File
@@ -1,11 +1,34 @@
#pragma once
#include "adminive/adminive.hpp"
#include "adminive/adapters/magic_enum.hpp"
#include "adminive/adapters/nlohmann_json.hpp"
#include "adminive/http.hpp"
#include <chrono>
#include <cstdint>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
namespace adminive::example {
std::string format_utc_time(std::chrono::system_clock::time_point value);
using Json = nlohmann::json;
template <class T>
using Http_Resource = adminive::Http_Resource<T, Json>;
template <class T>
using Http_Collection_Resource = adminive::Http_Collection_Resource<T, Json>;
inline std::string format_utc_time(std::chrono::system_clock::time_point value) {
const std::time_t raw_time = std::chrono::system_clock::to_time_t(value);
std::tm utc_time{};
#ifdef _WIN32
gmtime_s(&utc_time, &raw_time);
#else
gmtime_r(&raw_time, &utc_time);
#endif
std::ostringstream stream;
stream << std::put_time(&utc_time, "%Y-%m-%dT%H:%M:%SZ");
return stream.str();
}
enum class Radio_Mode {
receive,
transmit,
@@ -30,20 +53,6 @@ enum class Alert_Channel {
email,
webhook
};
enum class Device_Table_Type {
serial,
network
};
enum class Serial_Parity {
none,
odd,
even
};
enum class Network_Protocol {
tcp,
udp,
websocket
};
struct Server_Status {
std::string service_state{"running"};
std::string server_time;
@@ -86,54 +95,143 @@ struct Alert_Config {
std::string highlight_color{"#d97706"};
Endpoint_Config webhook_endpoint{true, "127.0.0.1", Range_Value<int, 1, 65535>{9100}};
};
struct Serial_Table_Config {
std::string group_name{"本地串口设备"};
Range_Value<int, 1200, 4000000> default_baud_rate{115200};
Serial_Parity default_parity{Serial_Parity::none};
std::string accent_color{"#2563eb"};
};
struct Network_Table_Config {
std::string group_name{"网络终端"};
Network_Protocol default_protocol{Network_Protocol::tcp};
Range_Value<int, 100, 60000> timeout_ms{3000};
bool tls_enabled{};
std::string accent_color{"#16a34a"};
};
struct Application_Config {
Device_Table_Type default_device_type{Device_Table_Type::serial};
Radio_Service_Config radio_service{};
Alert_Config alerts{};
Serial_Table_Config serial_table{};
Network_Table_Config network_table{};
};
struct Radio_State {
struct Even_Validator {
static constexpr int multiple_of = 2;
static constexpr std::string_view message = "buffer_count must be even";
void operator()(const int& value) const;
void operator()(const int& value) const {
if(value % 2 != 0) {
throw std::invalid_argument("buffer_count must be even");
}
}
};
struct Watermark_Validator {
void operator()(const Radio_State& value) const;
void operator()(const Radio_State& value) const {
if(value.low_watermark > value.high_watermark) {
throw std::invalid_argument("low_watermark must not exceed high_watermark");
}
}
};
Radio_Mode mode{Radio_Mode::receive};
Range_Value<int, 1, 65535> port{9999};
Validated_Value<int, Even_Validator> buffer_count{2};
int low_watermark{};
int high_watermark{};
Radio_Item_Status status();
Radio_Item_Status status() {
const auto now = std::chrono::system_clock::now();
const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
const auto state_index = (static_cast<std::uint64_t>(seconds / 4) + static_cast<std::uint64_t>(port.value())) % 4;
Radio_Operating_State state{};
std::string color;
switch(state_index) {
case 0:
state = Radio_Operating_State::starting;
color = "#2563eb";
break;
case 1:
state = Radio_Operating_State::running;
color = "#16a34a";
break;
case 2:
state = Radio_Operating_State::degraded;
color = "#d97706";
break;
default:
state = Radio_Operating_State::stopped;
color = "#dc2626";
break;
}
++status_sequence_;
return Radio_Item_Status{status_value(state, std::move(color)), status_value(status_sequence_, "#475569"), status_value(format_utc_time(now), "#475569")};
}
private:
std::uint64_t status_sequence_{};
};
struct Serial_Device_Row {
std::string port_name{"COM1"};
Range_Value<int, 1200, 4000000> baud_rate{115200};
Serial_Parity parity{Serial_Parity::none};
bool enabled{true};
}
namespace adminive {
template <>
struct Type_Descriptor<example::Server_Status> {
static auto get() {
using T = example::Server_Status;
return object<T>("server_status", ADMINIVE_FIELD(T, service_state), ADMINIVE_FIELD(T, server_time), ADMINIVE_FIELD(T, uptime_seconds), ADMINIVE_FIELD(T, poll_sequence), ADMINIVE_FIELD(T, radio_state_count), ADMINIVE_FIELD(T, listen_port)).label("Server Status");
}
};
struct Network_Device_Row {
std::string endpoint{"127.0.0.1:7001"};
Network_Protocol protocol{Network_Protocol::tcp};
Range_Value<int, 100, 60000> timeout_ms{3000};
bool tls_enabled{};
template <>
struct Type_Descriptor<example::Radio_Item_Status> {
static auto get() {
using T = example::Radio_Item_Status;
return object<T>("radio_item_status", ADMINIVE_FIELD_LABEL(T, operating_state, "State"), ADMINIVE_FIELD_LABEL(T, refresh_sequence, "Refresh Sequence"), ADMINIVE_FIELD_LABEL(T, refresh_time, "Refresh Time")).label("Runtime Status");
}
};
template <>
struct Type_Descriptor<example::Endpoint_Config> {
static auto get() {
using T = example::Endpoint_Config;
return object<T>(
"endpoint_config",
ADMINIVE_FIELD_LABEL(T, enabled, "Enable Endpoint").editable().creatable().description("Controls whether this endpoint participates in the current configuration"),
ADMINIVE_FIELD_LABEL(T, host, "Host Address").editable().creatable().required().description("String input example"),
ADMINIVE_FIELD_LABEL(T, port, "Port").editable().creatable().required().description("Integer input example")
).label("Endpoint");
}
};
template <>
struct Type_Descriptor<example::Appearance_Config> {
static auto get() {
using T = example::Appearance_Config;
return object<T>(
"appearance_config",
ADMINIVE_FIELD_LABEL(T, panel_title, "Panel Title").editable().creatable().required().description("String input example"),
ADMINIVE_FIELD_LABEL(T, effective_date, "Effective Date").editable().creatable().required().widget("input-date").description("Date selector example"),
ADMINIVE_FIELD_LABEL(T, accent_color, "Accent Color").editable().creatable().required().widget("input-color").description("Color selector example")
).label("Appearance");
}
};
template <>
struct Type_Descriptor<example::Radio_Service_Config> {
static auto get() {
using T = example::Radio_Service_Config;
return object<T>(
"radio_service_config",
ADMINIVE_FIELD_LABEL(T, profile_name, "Profile Name").editable().required().description("Editable string configuration"),
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode").editable().required().description("Enum options are generated by magic_enum"),
ADMINIVE_FIELD_LABEL(T, worker_count, "Worker Count").editable().required().description("Integer number input"),
ADMINIVE_FIELD_LABEL(T, receive_gain, "Receive Gain").editable().required().description("Floating-point number input"),
ADMINIVE_FIELD_LABEL(T, primary_endpoint, "Primary Endpoint").editable().description("First child configuration group"),
ADMINIVE_FIELD_LABEL(T, backup_endpoint, "Backup Endpoint").editable().visible_on("${mode == 'duplex'}").description("Displayed only when Operating Mode is duplex"),
ADMINIVE_FIELD_LABEL(T, appearance, "Appearance").editable().description("Nested date and color controls")
).label("Radio Service Configuration");
}
};
template <>
struct Type_Descriptor<example::Alert_Config> {
static auto get() {
using T = example::Alert_Config;
return object<T>(
"alert_config",
ADMINIVE_FIELD_LABEL(T, enabled, "Enable Alerts").editable(),
ADMINIVE_FIELD_LABEL(T, rule_name, "Rule Name").editable().required(),
ADMINIVE_FIELD_LABEL(T, channel, "Delivery Channel").editable().required().description("Enum options are generated by magic_enum"),
ADMINIVE_FIELD_LABEL(T, minimum_level, "Minimum Log Level").editable().required().description("Second enum example"),
ADMINIVE_FIELD_LABEL(T, repeat_minutes, "Repeat Interval Minutes").editable().required(),
ADMINIVE_FIELD_LABEL(T, effective_date, "Effective Date").editable().required().widget("input-date"),
ADMINIVE_FIELD_LABEL(T, highlight_color, "Highlight Color").editable().required().widget("input-color"),
ADMINIVE_FIELD_LABEL(T, webhook_endpoint, "Webhook Endpoint").editable().visible_on("${enabled && channel == 'webhook'}").description("Displayed only when alerts are enabled and channel is webhook")
).label("Alert Configuration");
}
};
template <>
struct Type_Descriptor<example::Radio_State> {
static auto get() {
using T = example::Radio_State;
return object<T>(
"radio_state",
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode").creatable().editable().required().list_label("Mode").order(5).sortable().description("Enum list column generated through magic_enum"),
ADMINIVE_FIELD(T, port).creatable().editable().required().description("Listening port").list_label("Listen Port").order(20).sortable(),
ADMINIVE_FIELD(T, buffer_count).creatable().editable().required().list_label("Buffers").order(10).sortable(),
ADMINIVE_FIELD(T, low_watermark).creatable().editable().order(30),
ADMINIVE_FIELD(T, high_watermark).creatable().editable().order(40)
).label("Radio State").validator(T::Watermark_Validator{});
}
};
}
+91 -36
View File
@@ -1,53 +1,108 @@
#include "server_app.hpp"
#include "example.hpp"
#include <atomic>
#include <charconv>
#include <chrono>
#include <filesystem>
#include <iostream>
#include <string_view>
#include <vector>
#ifndef ADMINIVE_FRONTEND_DIST_DIR
#define ADMINIVE_FRONTEND_DIST_DIR "frontend_dist"
#endif
#ifndef ADMINIVE_CONFIG_FILE
#define ADMINIVE_CONFIG_FILE "config/adminive.json"
#endif
namespace {
int read_port(int argc, char** argv) {
if(argc == 1) {
return 9999;
}
const std::string_view text(argv[1]);
int port{};
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), port);
if(error != std::errc{} || end != text.data() + text.size() || port < 1 || port > 65535) {
return 0;
}
return port;
using Alert_Config = adminive::example::Alert_Config;
using Radio_Item_Status = adminive::example::Radio_Item_Status;
using Radio_Mode = adminive::example::Radio_Mode;
using Radio_Service_Config = adminive::example::Radio_Service_Config;
using Radio_State = adminive::example::Radio_State;
using Json = adminive::example::Json;
using State_Resource = adminive::example::Http_Collection_Resource<Radio_State>;
using Service_Config_Resource = adminive::example::Http_Resource<Radio_Service_Config>;
using Alert_Config_Resource = adminive::example::Http_Resource<Alert_Config>;
Radio_State make_radio_state(Radio_Mode mode, int port, int buffer_count, int low_watermark, int high_watermark) {
Radio_State result;
result.mode = mode;
result.port = port;
result.buffer_count = buffer_count;
result.low_watermark = low_watermark;
result.high_watermark = high_watermark;
return result;
}
void describe_example() {
using namespace adminive::example;
Radio_State state;
const auto status_descriptor = adminive::to_status_descriptor_json<Radio_Item_Status>();
std::cout << adminive::to_descriptor_json<Radio_State>().dump(2) << '\n';
std::cout << adminive::to_json(state).dump(2) << '\n';
std::cout << status_descriptor.dump(2) << '\n';
std::cout << adminive::to_descriptor_json<Radio_Service_Config>().dump(2) << '\n';
std::cout << adminive::to_descriptor_json<Serial_Device_Row>().dump(2) << '\n';
std::cout << adminive::to_descriptor_json<Network_Device_Row>().dump(2) << '\n';
Json make_admin_schema(const State_Resource& states, const Service_Config_Resource& service_config, const Alert_Config_Resource& alert_config) {
const Json states_schema = states.amis_schema();
Json tabs = Json::array();
tabs.push_back(Json{{"title", "Radio State List"}, {"body", states_schema.at("body")}});
tabs.push_back(Json{{"title", "Radio Service Configuration"}, {"body", service_config.amis_schema()}});
tabs.push_back(Json{{"title", "Alert Configuration"}, {"body", alert_config.amis_schema()}});
return Json{{"type", "page"}, {"title", "Adminive Complete Example"}, {"subTitle", "CRUD, enum columns, lazy status, hierarchical configuration, linkage, date and color controls are defined by the backend"}, {"body", Json{{"type", "tabs"}, {"tabs", std::move(tabs)}}}};
}
}
int main(int argc, char** argv) {
if(argc == 2 && std::string_view(argv[1]) == "--describe") {
describe_example();
Radio_State state;
Radio_Service_Config service_config;
Alert_Config alert_config;
const auto status_descriptor = adminive::to_status_descriptor_json<Json, Radio_Item_Status>();
std::cout << adminive::to_descriptor_json<Json, Radio_State>().dump(2) << '\n';
std::cout << adminive::to_json<Json>(state).dump(2) << '\n';
std::cout << status_descriptor.dump(2) << '\n';
std::cout << adminive::to_descriptor_json<Json, Radio_Service_Config>().dump(2) << '\n';
std::cout << adminive::to_amis_form_schema<Json>(service_config, "/admin/config/radio/data", "Confirm Changes").dump(2) << '\n';
std::cout << adminive::to_descriptor_json<Json, Alert_Config>().dump(2) << '\n';
std::cout << adminive::to_amis_form_schema<Json>(alert_config, "/admin/config/alerts/data", "Confirm Changes").dump(2) << '\n';
return 0;
}
const int port = read_port(argc, argv);
if(port == 0) {
std::cerr << "invalid port\n";
return 2;
int port = 9999;
if(argc == 2) {
const std::string_view text(argv[1]);
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), port);
if(error != std::errc{} || end != text.data() + text.size() || port < 1 || port > 65535) {
std::cerr << "invalid port\n";
return 2;
}
}
try {
adminive::example::Server_App app(port, ADMINIVE_FRONTEND_DIST_DIR, ADMINIVE_CONFIG_FILE);
return app.run();
} catch(const std::exception& error) {
std::cerr << error.what() << '\n';
return 4;
const auto started_at = std::chrono::steady_clock::now();
std::vector<Radio_State> initial;
initial.push_back(make_radio_state(Radio_Mode::receive, 10001, 2, 16, 64));
initial.push_back(make_radio_state(Radio_Mode::transmit, 10002, 4, 32, 128));
initial.push_back(make_radio_state(Radio_Mode::duplex, 10003, 8, 64, 256));
Radio_Service_Config radio_service_config;
Alert_Config alert_config;
httplib::Server server;
State_Resource state_resource("/admin/radio_states", std::move(initial), "/admin/status");
Service_Config_Resource service_config_resource(radio_service_config, "/admin/config/radio");
Alert_Config_Resource alert_config_resource(alert_config, "/admin/config/alerts");
state_resource.register_status<&Radio_State::status>(2000);
state_resource.bind(server);
service_config_resource.bind(server);
alert_config_resource.bind(server);
server.Get("/admin/amis", [&](const httplib::Request&, httplib::Response& response) {
adminive::write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", make_admin_schema(state_resource, service_config_resource, alert_config_resource)}});
});
std::atomic<std::uint64_t> poll_sequence{};
server.Get("/admin/status", [&](const httplib::Request&, httplib::Response& response) {
const auto now = std::chrono::system_clock::now();
const auto uptime = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - started_at).count();
adminive::example::Server_Status status;
status.server_time = adminive::example::format_utc_time(now);
status.uptime_seconds = static_cast<std::uint64_t>(uptime);
status.poll_sequence = poll_sequence.fetch_add(1, std::memory_order_relaxed) + 1;
status.radio_state_count = state_resource.size();
status.listen_port = port;
adminive::write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", adminive::to_frontend_json<Json>(status)}});
});
const std::filesystem::path frontend_dist = std::filesystem::path(ADMINIVE_FRONTEND_DIST_DIR).lexically_normal();
if(!server.set_mount_point("/", frontend_dist.string())) {
std::cerr << "frontend build directory does not exist: " << frontend_dist.string() << '\n';
std::cerr << "run npm run build from the project root before starting the backend\n";
return 3;
}
std::cout << "Adminive server listening on http://127.0.0.1:" << port << '\n';
std::cout << "Serving frontend from " << frontend_dist.string() << '\n';
std::cout << std::flush;
if(!server.listen("0.0.0.0", port)) {
std::cerr << "failed to start server\n";
return 1;
}
return 0;
}
+254
View File
@@ -0,0 +1,254 @@
#include "adminive/adminive.hpp"
#include <cassert>
#include <cstdint>
#include <map>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
namespace adapter_test {
struct Mini_Json;
using Mini_Object = std::map<std::string, Mini_Json>;
using Mini_Array = std::vector<Mini_Json>;
struct Mini_Json {
using Storage = std::variant<std::nullptr_t, bool, long double, std::string, Mini_Object, Mini_Array>;
Storage value{};
};
struct External_Atomic {
int value{};
};
enum class Mode {
primary = 10,
secondary = 20
};
struct Runtime_Base {
External_Atomic worker_count{4};
};
struct Mode_Base {
Mode mode{Mode::primary};
};
struct Config : Runtime_Base, Mode_Base {
std::string name{"default"};
};
}
namespace adminive {
template <>
struct Json_Adapter<adapter_test::Mini_Json> {
using Json = adapter_test::Mini_Json;
static Json object() {
return Json{adapter_test::Mini_Object{}};
}
static Json array() {
return Json{adapter_test::Mini_Array{}};
}
static Json parse(std::string_view) {
throw std::runtime_error("parse is not implemented by the test adapter");
}
static std::string dump(const Json&, int) {
return {};
}
static bool is_object(const Json& value) noexcept {
return std::holds_alternative<adapter_test::Mini_Object>(value.value);
}
static bool is_array(const Json& value) noexcept {
return std::holds_alternative<adapter_test::Mini_Array>(value.value);
}
static bool is_string(const Json& value) noexcept {
return std::holds_alternative<std::string>(value.value);
}
static bool is_number(const Json& value) noexcept {
return std::holds_alternative<long double>(value.value);
}
static bool is_boolean(const Json& value) noexcept {
return std::holds_alternative<bool>(value.value);
}
static long double number(const Json& value) {
return std::get<long double>(value.value);
}
static bool contains(const Json& value, std::string_view name) {
const auto& object_value = std::get<adapter_test::Mini_Object>(value.value);
return object_value.contains(std::string(name));
}
static std::size_t size(const Json& value) {
if(is_object(value)) {
return std::get<adapter_test::Mini_Object>(value.value).size();
}
if(is_array(value)) {
return std::get<adapter_test::Mini_Array>(value.value).size();
}
return 0;
}
static std::vector<std::string> keys(const Json& value) {
std::vector<std::string> result;
for(const auto& [name, item] : std::get<adapter_test::Mini_Object>(value.value)) {
static_cast<void>(item);
result.push_back(name);
}
return result;
}
static Json& at(Json& value, std::string_view name) {
return std::get<adapter_test::Mini_Object>(value.value).at(std::string(name));
}
static const Json& at(const Json& value, std::string_view name) {
return std::get<adapter_test::Mini_Object>(value.value).at(std::string(name));
}
static Json& at(Json& value, std::size_t index) {
return std::get<adapter_test::Mini_Array>(value.value).at(index);
}
static const Json& at(const Json& value, std::size_t index) {
return std::get<adapter_test::Mini_Array>(value.value).at(index);
}
static void set(Json& value, std::string_view name, Json item) {
std::get<adapter_test::Mini_Object>(value.value).insert_or_assign(std::string(name), std::move(item));
}
static void append(Json& value, Json item) {
std::get<adapter_test::Mini_Array>(value.value).push_back(std::move(item));
}
static void erase(Json& value, std::string_view name) {
std::get<adapter_test::Mini_Object>(value.value).erase(std::string(name));
}
template <class T>
static Json make(T&& value) {
using Value = std::remove_cvref_t<T>;
if constexpr(std::same_as<Value, bool>) {
return Json{value};
} else if constexpr(std::integral<Value> || std::floating_point<Value>) {
return Json{static_cast<long double>(value)};
} else if constexpr(std::same_as<Value, std::string>) {
return Json{std::forward<T>(value)};
} else if constexpr(std::same_as<Value, std::string_view>) {
return Json{std::string(value)};
} else if constexpr(std::is_convertible_v<T, const char*>) {
return Json{std::string(value)};
} else {
static_assert(!sizeof(T), "unsupported Mini_Json scalar");
}
}
template <class T>
static T get(const Json& value) {
if constexpr(std::same_as<T, bool>) {
return std::get<bool>(value.value);
} else if constexpr(std::integral<T> || std::floating_point<T>) {
return static_cast<T>(std::get<long double>(value.value));
} else if constexpr(std::same_as<T, std::string>) {
return std::get<std::string>(value.value);
} else {
static_assert(!sizeof(T), "unsupported Mini_Json conversion");
}
}
};
template <class Json>
struct Value_Adapter<adapter_test::External_Atomic, Json> {
using value_type = int;
static int read(const adapter_test::External_Atomic& value) noexcept {
return value.value;
}
static void write(adapter_test::External_Atomic& target, int value) noexcept {
target.value = value;
}
};
template <>
struct Enum_Adapter<adapter_test::Mode> {
static std::vector<adapter_test::Mode> values() {
return {adapter_test::Mode::primary, adapter_test::Mode::secondary};
}
static std::string_view name(adapter_test::Mode value) {
return value == adapter_test::Mode::primary ? "primary" : "secondary";
}
static std::optional<adapter_test::Mode> cast(std::string_view value) {
if(value == "primary") {
return adapter_test::Mode::primary;
}
if(value == "secondary") {
return adapter_test::Mode::secondary;
}
return std::nullopt;
}
static std::optional<adapter_test::Mode> cast(std::underlying_type_t<adapter_test::Mode> value) {
if(value == 10) {
return adapter_test::Mode::primary;
}
if(value == 20) {
return adapter_test::Mode::secondary;
}
return std::nullopt;
}
};
template <>
struct Reflection_Adapter<adapter_test::Config> {
static constexpr std::size_t field_count = 3;
template <std::size_t Index>
static decltype(auto) get(adapter_test::Config& value) noexcept {
if constexpr(Index == 0) {
return (value.worker_count);
} else if constexpr(Index == 1) {
return (value.mode);
} else {
return (value.name);
}
}
template <std::size_t Index>
static decltype(auto) get(const adapter_test::Config& value) noexcept {
if constexpr(Index == 0) {
return (value.worker_count);
} else if constexpr(Index == 1) {
return (value.mode);
} else {
return (value.name);
}
}
template <std::size_t Index>
static constexpr std::string_view name() noexcept {
if constexpr(Index == 0) {
return "worker_count";
} else if constexpr(Index == 1) {
return "mode";
} else {
return "name";
}
}
};
template <>
struct Type_Descriptor<adapter_test::Config> {
static auto get() {
using T = adapter_test::Config;
return reflected_object_with<T>("config", "配置", []<std::size_t Index>(auto field) {
if constexpr(Index == 0) {
return field.label("线程数").editable().required();
} else if constexpr(Index == 1) {
return field.label("模式").editable().template enum_label<adapter_test::Mode::primary>("主用").template enum_label<adapter_test::Mode::secondary>("备用");
} else {
return field.label("名称").editable();
}
});
}
};
}
int main() {
using Json = adapter_test::Mini_Json;
using Config = adapter_test::Config;
const Json descriptor = adminive::to_descriptor_json<Json, Config>();
const auto& fields = adminive::Json_Adapter<Json>::at(descriptor, "fields");
assert((adminive::json_get<Json, std::string>(adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(fields, 0), "name")) == "worker_count"));
assert((adminive::json_get<Json, int>(adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(fields, 0), "default")) == 4));
const auto& options = adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(fields, 1), "options");
assert((adminive::json_get<Json, std::string>(adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(options, 0), "label")) == "主用"));
Config config;
Json patch = adminive::json_object<Json>();
adminive::json_set(patch, "worker_count", 12);
adminive::json_set(patch, "mode", "secondary");
adminive::json_set(patch, "name", "updated");
const auto result = adminive::apply_frontend_patch<Json>(config, patch);
assert(result.success);
assert(config.worker_count.value == 12);
assert(config.mode == adapter_test::Mode::secondary);
assert(config.name == "updated");
const Json encoded = adminive::to_json<Json>(config);
assert((adminive::json_get<Json, int>(adminive::Json_Adapter<Json>::at(encoded, "worker_count")) == 12));
assert((adminive::json_get<Json, std::string>(adminive::Json_Adapter<Json>::at(encoded, "mode")) == "secondary"));
return 0;
}
+154 -63
View File
@@ -1,70 +1,161 @@
#include "config_store.hpp"
#include "device_tables.hpp"
#include "example_descriptors.hpp"
#include "example.hpp"
#include <cassert>
#include <filesystem>
#include <string>
#include <vector>
int main() {
using namespace adminive::example;
const auto radio_descriptor = adminive::to_descriptor_json<Radio_State>();
assert(radio_descriptor.at("fields").at(0).at("name") == "mode");
assert(radio_descriptor.at("fields").at(0).at("list_label") == "模式");
assert(radio_descriptor.at("fields").at(0).at("options").at(0).at("label") == "接收");
assert(radio_descriptor.at("fields").at(0).at("options").at(2).at("label") == "双工");
assert(radio_descriptor.at("list").at("id_label") == "编号");
assert(radio_descriptor.at("list").at("default_order_by") == "buffer_count");
assert(radio_descriptor.at("list").at("default_order_dir") == "asc");
const auto radio_crud = adminive::to_amis_crud_schema<Radio_State>("/admin/radio_states");
const auto& radio_table = radio_crud.at("body");
assert(radio_table.at("columns").at(0).at("label") == "编号");
assert(radio_table.at("columns").at(1).at("map").at("receive") == "接收");
assert(radio_table.at("columns").at(2).at("name") == "buffer_count");
assert(radio_table.at("headerToolbar").at(0).at("type") == "columns-toggler");
assert(radio_table.at("headerToolbar").at(0).at("draggable") == true);
assert(radio_table.at("defaultParams").at("orderBy") == "buffer_count");
assert(!radio_table.contains("draggable"));
const auto serial_descriptor = adminive::to_descriptor_json<Serial_Device_Row>();
assert(serial_descriptor.at("list").at("user_reorderable") == true);
assert(serial_descriptor.at("fields").at(2).at("options").at(0).at("label") == "无校验");
const auto serial_crud = adminive::to_amis_crud_schema<Serial_Device_Row>("/admin/device_tables/serial/items");
assert(serial_crud.at("body").at("draggable") == true);
assert(serial_crud.at("body").at("saveOrderApi").at("url") == "/admin/device_tables/serial/items/order");
const auto network_crud = adminive::to_amis_crud_schema<Network_Device_Row>("/admin/device_tables/network/items");
assert(network_crud.at("body").at("columns").at(1).at("label") == "地址");
assert(network_crud.at("body").at("columns").at(2).at("map").at("websocket") == "WebSocket");
Radio_Service_Config radio_config;
const auto radio_form = adminive::to_amis_form_schema(radio_config, "/admin/config/radio/data", "确认修改");
assert(radio_form.at("body").at(0).at("type") == "input-text");
assert(radio_form.at("body").at(1).at("type") == "select");
assert(radio_form.at("body").at(1).at("options").at(2).at("label") == "双工");
assert(radio_form.at("body").at(5).at("visibleOn") == "${mode == 'duplex'}");
assert(radio_form.at("body").at(6).at("body").at(1).at("type") == "input-date");
assert(radio_form.at("body").at(6).at("body").at(2).at("type") == "input-color");
using adminive::example::Alert_Config;
using adminive::example::Radio_Item_Status;
using adminive::example::Radio_Mode;
using adminive::example::Radio_Service_Config;
using adminive::example::Radio_State;
using Json = adminive::example::Json;
Radio_State state;
const auto first_status = adminive::to_status_json(state.status());
const auto second_status = adminive::to_status_json(state.status());
const auto descriptor = adminive::to_descriptor_json<Json, Radio_State>();
assert(descriptor.at("name") == "radio_state");
assert(descriptor.at("fields").size() == 5);
assert(descriptor.at("fields").at(0).at("name") == "mode");
assert(descriptor.at("fields").at(0).at("label") == "Operating Mode");
assert(descriptor.at("fields").at(0).at("list_label") == "Mode");
assert(descriptor.at("fields").at(0).at("value_type") == "enum");
assert(descriptor.at("fields").at(0).at("options").size() == 4);
assert(descriptor.at("fields").at(0).at("options").at(0).at("value") == "receive");
assert(descriptor.at("fields").at(0).at("options").at(2).at("value") == "duplex");
assert(descriptor.at("fields").at(0).at("order") == 5);
assert(descriptor.at("fields").at(0).at("sortable") == true);
assert(descriptor.at("fields").at(1).at("name") == "port");
assert(descriptor.at("fields").at(1).at("list_label") == "Listen Port");
assert(descriptor.at("fields").at(1).at("minimum") == 1);
assert(descriptor.at("fields").at(1).at("maximum") == 65535);
assert(descriptor.at("fields").at(2).at("name") == "buffer_count");
assert(descriptor.at("fields").at(2).at("multiple_of") == 2);
const auto initial = adminive::to_json<Json>(state);
assert(initial.at("mode") == "receive");
assert(initial.at("port") == 9999);
Radio_State assigned;
auto result = adminive::assign_json<Json>(assigned, Json{{"mode", "transmit"}, {"port", 7000}, {"buffer_count", 6}, {"low_watermark", 10}, {"high_watermark", 20}});
assert(result.success);
assert(assigned.mode == Radio_Mode::transmit);
assert(assigned.port.value() == 7000);
result = adminive::apply_json_patch<Json>(assigned, Json{{"mode", "duplex"}, {"port", 7200}});
assert(result.success);
assert(assigned.mode == Radio_Mode::duplex);
assert(assigned.port.value() == 7200);
result = adminive::apply_patch<Json>(state, Json{{"buffer_count", 3}});
assert(!result.success);
assert(state.buffer_count.value() == 2);
result = adminive::apply_patch<Json>(state, Json{{"low_watermark", 64}, {"high_watermark", 32}});
assert(!result.success);
assert(state.low_watermark == 0);
result = adminive::apply_patch<Json>(state, Json{{"missing", 1}});
assert(!result.success);
assert(result.field_errors.contains("missing"));
Radio_State created;
result = adminive::apply_frontend_create<Json>(created, Json{{"mode", "maintenance"}, {"port", 7300}, {"buffer_count", 10}, {"low_watermark", 12}, {"high_watermark", 24}});
assert(result.success);
assert(created.mode == Radio_Mode::maintenance);
result = adminive::apply_frontend_create<Json>(created, Json{{"port", 7300}});
assert(!result.success);
assert(result.field_errors.contains("mode"));
assert(result.field_errors.contains("buffer_count"));
const auto form = adminive::to_amis_form_schema<Json>(state, "/admin/radio_state", "Confirm Changes");
assert(form.at("type") == "form");
assert(form.at("body").size() == 5);
assert(form.at("body").at(0).at("type") == "select");
assert(form.at("body").at(0).at("options").size() == 4);
assert(form.at("body").at(1).at("type") == "input-number");
assert(form.at("body").at(2).at("step") == 2);
assert(form.at("actions").size() == 2);
assert(form.at("actions").at(0).at("type") == "reset");
assert(form.at("actions").at(1).at("type") == "submit");
assert(form.at("actions").at(1).at("label") == "Confirm Changes");
const auto crud = adminive::to_amis_crud_schema<Json, Radio_State>("/admin/radio_states");
assert(crud.at("type") == "page");
assert(crud.at("body").at("type") == "crud");
assert(crud.at("body").at("columns").size() == 7);
assert(crud.at("body").at("columns").at(1).at("name") == "mode");
assert(crud.at("body").at("columns").at(1).at("type") == "mapping");
assert(crud.at("body").at("columns").at(1).at("map").at("duplex") == "Duplex");
assert(crud.at("body").at("columns").at(2).at("name") == "buffer_count");
assert(crud.at("body").at("columns").at(3).at("name") == "port");
assert(!crud.at("body").contains("interval"));
const auto& create_form = crud.at("body").at("headerToolbar").at(2).at("dialog").at("body");
assert(create_form.at("actions").at(1).at("type") == "submit");
assert(create_form.at("actions").at(1).at("label") == "Create");
const auto status_descriptor = adminive::to_status_descriptor_json<Json, Radio_Item_Status>();
assert(status_descriptor.at("protocol") == "adminive.status");
assert(status_descriptor.at("fields").size() == 3);
assert(status_descriptor.at("fields").at(0).at("name") == "operating_state");
assert(status_descriptor.at("fields").at(0).at("value_type") == "enum");
assert(status_descriptor.at("fields").at(0).at("color") == true);
const auto first_status = adminive::to_status_json<Json>(state.status());
const auto second_status = adminive::to_status_json<Json>(state.status());
assert(first_status.at("operating_state").contains("color"));
assert(!first_status.at("operating_state").at("color").get<std::string>().empty());
assert(first_status.at("refresh_sequence").at("value") == 1);
assert(second_status.at("refresh_sequence").at("value") == 2);
const auto config_root = std::filesystem::temp_directory_path() / "adminive_config_store_test";
const auto config_file = config_root / "adminive.json";
std::filesystem::remove_all(config_root);
{
Config_Store store(config_file);
assert(std::filesystem::exists(config_file));
Radio_Service_Config changed = store.data().radio_service;
changed.profile_name = "持久化配置";
std::scoped_lock lock(store.mutex());
store.persist_radio_service(changed);
}
{
Config_Store store(config_file);
assert(store.data().radio_service.profile_name == "持久化配置");
Device_Table_Manager manager(store);
const auto schema = manager.amis_schema();
assert(schema.at("data").at("device_type") == "serial");
assert(schema.at("body").at("body").at(0).at("label") == "设备类型");
assert(schema.at("body").at("body").at(0).at("options").size() == 2);
assert(schema.at("body").at("body").at(2).at("schemaApi").at("url") == "/admin/device_tables/schema?type=${device_type}");
}
std::filesystem::remove_all(config_root);
const auto status_crud = adminive::to_amis_crud_status_schema<Json, Radio_State>("/admin/radio_states", "/admin/status", &status_descriptor);
assert(status_crud.at("body").is_array());
assert(status_crud.at("body").at(0).at("type") == "service");
assert(status_crud.at("body").at(1).at("columns").size() == 8);
const auto& status_column = status_crud.at("body").at(1).at("columns").at(6);
assert(status_column.at("label") == "Runtime Status");
assert(status_column.at("popOver").at("trigger") == "click");
assert(status_column.at("popOver").at("body").at("api").at("url") == "/admin/radio_states/${id}/status");
adminive::example::Http_Collection_Resource<Radio_State> collection_resource("/admin/radio_states", std::vector<Radio_State>{state}, "/admin/status");
collection_resource.register_status<&Radio_State::status>(3000);
const auto registered_schema = collection_resource.amis_schema();
const auto& registered_status_column = registered_schema.at("body").at(1).at("columns").at(6);
assert(registered_status_column.at("popOver").at("body").at("interval") == 3000);
Radio_Service_Config service_config;
const auto service_descriptor = adminive::to_descriptor_json<Json, Radio_Service_Config>();
assert(service_descriptor.at("fields").size() == 7);
assert(service_descriptor.at("fields").at(0).at("value_type") == "string");
assert(service_descriptor.at("fields").at(1).at("value_type") == "enum");
assert(service_descriptor.at("fields").at(2).at("value_type") == "integer");
assert(service_descriptor.at("fields").at(3).at("value_type") == "number");
assert(service_descriptor.at("fields").at(4).at("value_type") == "object");
assert(service_descriptor.at("fields").at(4).at("children").size() == 3);
assert(service_descriptor.at("fields").at(5).at("visible_on") == "${mode == 'duplex'}");
assert(service_descriptor.at("fields").at(6).at("children").at(1).at("widget") == "input-date");
assert(service_descriptor.at("fields").at(6).at("children").at(2).at("widget") == "input-color");
const auto service_form = adminive::to_amis_form_schema<Json>(service_config, "/admin/config/radio/data", "Confirm Changes");
assert(service_form.at("body").at(0).at("type") == "input-text");
assert(service_form.at("body").at(1).at("type") == "select");
assert(service_form.at("body").at(2).at("type") == "input-number");
assert(service_form.at("body").at(3).at("type") == "input-number");
assert(service_form.at("body").at(4).at("type") == "fieldset");
assert(service_form.at("body").at(4).at("body").at(1).at("name") == "primary_endpoint.host");
assert(service_form.at("body").at(5).at("visibleOn") == "${mode == 'duplex'}");
assert(service_form.at("body").at(6).at("body").at(1).at("type") == "input-date");
assert(service_form.at("body").at(6).at("body").at(1).at("valueFormat") == "YYYY-MM-DD");
assert(service_form.at("body").at(6).at("body").at(2).at("type") == "input-color");
assert(service_form.at("actions").at(1).at("type") == "submit");
result = adminive::apply_frontend_patch<Json>(service_config, Json{{"profile_name", "Duplex Profile"}, {"mode", "duplex"}, {"worker_count", 8}, {"receive_gain", 2.5}, {"primary_endpoint", Json{{"enabled", true}, {"host", "10.0.0.1"}, {"port", 9200}}}, {"backup_endpoint", Json{{"enabled", true}, {"host", "10.0.0.2"}, {"port", 9201}}}, {"appearance", Json{{"panel_title", "Duplex Radio"}, {"effective_date", "2026-08-07"}, {"accent_color", "#16a34a"}}}});
assert(result.success);
assert(service_config.mode == Radio_Mode::duplex);
assert(service_config.primary_endpoint.host == "10.0.0.1");
assert(service_config.appearance.accent_color == "#16a34a");
Alert_Config alert_config;
const auto alert_form = adminive::to_amis_form_schema<Json>(alert_config, "/admin/config/alerts/data", "Confirm Changes");
assert(alert_form.at("body").size() == 8);
assert(alert_form.at("body").at(1).at("type") == "input-text");
assert(alert_form.at("body").at(2).at("type") == "select");
assert(alert_form.at("body").at(3).at("type") == "select");
assert(alert_form.at("body").at(4).at("type") == "input-number");
assert(alert_form.at("body").at(5).at("type") == "input-date");
assert(alert_form.at("body").at(6).at("type") == "input-color");
assert(alert_form.at("body").at(7).at("type") == "fieldset");
assert(alert_form.at("body").at(7).at("visibleOn") == "${enabled && channel == 'webhook'}");
adminive::example::Http_Resource<Radio_Service_Config> service_resource(service_config, "/admin/config/radio");
const auto resource_form = service_resource.amis_schema();
assert(resource_form.at("api").at("method") == "post");
assert(resource_form.at("api").at("url") == "/admin/config/radio/data");
assert(resource_form.at("actions").at(1).at("label") == "Confirm Changes");
adminive::example::Server_Status server_status;
server_status.server_time = "2026-08-06T00:00:00Z";
server_status.poll_sequence = 7;
const auto status_json = adminive::to_frontend_json<Json>(server_status);
assert(status_json.at("service_state") == "running");
assert(status_json.at("poll_sequence") == 7);
return 0;
}