分离 json magic_enum 依赖
This commit is contained in:
+27
-36
@@ -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 ()
|
||||
|
||||
@@ -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,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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user