From bba2895e2bd5074579d5f40454e53bc5d3e1176b Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Fri, 7 Aug 2026 09:25:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E6=A0=B8=E5=BF=83=E5=BA=93?= =?UTF-8?q?=E5=92=8C=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 94 ++-- backend/CMakeLists.txt | 100 +--- backend/include/adminive/adapters/httplib.hpp | 386 --------------- backend/include/adminive/http.hpp | 197 -------- backend/include/adminive/synchronized.hpp | 122 ----- backend/library/CMakeLists.txt | 24 + .../include/adminive/adapter.hpp | 0 .../include/adminive/adminive.hpp | 2 + .../{ => library}/include/adminive/amis.hpp | 10 +- .../library/include/adminive/collection.hpp | 329 +++++++++++++ .../include/adminive/concepts.hpp | 0 .../include/adminive/descriptor.hpp | 14 + backend/library/include/adminive/http.hpp | 264 ++++++++++ .../{ => library}/include/adminive/json.hpp | 96 ++-- .../{ => library}/include/adminive/status.hpp | 0 .../library/include/adminive/synchronized.hpp | 454 ++++++++++++++++++ .../{ => library}/include/adminive/value.hpp | 0 .../{ => library}/tests/core_adapter_test.cpp | 5 + backend/main.cmake | 0 backend/service/CMakeLists.txt | 87 ++++ .../include/adminive/adapters/boost_pfr.hpp | 0 .../include/adminive/adapters/drogon.hpp | 119 ++++- .../include/adminive/adapters/httplib.hpp | 165 +++++++ .../include/adminive/adapters/magic_enum.hpp | 0 .../adminive/adapters/nlohmann_json.hpp | 0 backend/{ => service}/src/config_store.cpp | 16 +- backend/{ => service}/src/config_store.hpp | 9 +- backend/{ => service}/src/device_tables.cpp | 4 +- backend/{ => service}/src/device_tables.hpp | 0 backend/{ => service}/src/example.cpp | 0 backend/{ => service}/src/example.hpp | 0 .../{ => service}/src/example_descriptors.hpp | 2 +- backend/{ => service}/src/main.cpp | 0 backend/{ => service}/src/server_app.cpp | 2 +- backend/{ => service}/src/server_app.hpp | 0 .../tests/advanced_adapter_test.cpp | 17 +- .../{ => service}/tests/config_store_test.cpp | 0 .../tests/drogon_adapter_test.cpp | 46 +- .../tests/fake_drogon/drogon/drogon.h | 60 ++- backend/{ => service}/tests/safety_test.cpp | 0 backend/service/tests/synchronized_test.cpp | 175 +++++++ backend/{ => service}/tests/test.cpp | 0 .../third_party/include/httplib.h | 0 .../include/magic_enum/magic_enum.hpp | 0 .../include/magic_enum/magic_enum_all.hpp | 0 .../magic_enum/magic_enum_containers.hpp | 0 .../include/magic_enum/magic_enum_flags.hpp | 0 .../include/magic_enum/magic_enum_format.hpp | 0 .../include/magic_enum/magic_enum_fuse.hpp | 0 .../magic_enum/magic_enum_iostream.hpp | 0 .../include/magic_enum/magic_enum_switch.hpp | 0 .../include/magic_enum/magic_enum_utility.hpp | 0 .../include/nlohmann/adl_serializer.hpp | 0 .../nlohmann/byte_container_with_subtype.hpp | 0 .../include/nlohmann/detail/abi_macros.hpp | 0 .../nlohmann/detail/conversions/from_json.hpp | 0 .../nlohmann/detail/conversions/to_chars.hpp | 0 .../nlohmann/detail/conversions/to_json.hpp | 0 .../include/nlohmann/detail/exceptions.hpp | 0 .../include/nlohmann/detail/hash.hpp | 0 .../nlohmann/detail/input/binary_reader.hpp | 0 .../nlohmann/detail/input/input_adapters.hpp | 0 .../nlohmann/detail/input/json_sax.hpp | 0 .../include/nlohmann/detail/input/lexer.hpp | 0 .../include/nlohmann/detail/input/parser.hpp | 0 .../nlohmann/detail/input/position_t.hpp | 0 .../detail/iterators/internal_iterator.hpp | 0 .../nlohmann/detail/iterators/iter_impl.hpp | 0 .../detail/iterators/iteration_proxy.hpp | 0 .../detail/iterators/iterator_traits.hpp | 0 .../iterators/json_reverse_iterator.hpp | 0 .../detail/iterators/primitive_iterator.hpp | 0 .../detail/json_custom_base_class.hpp | 0 .../include/nlohmann/detail/json_pointer.hpp | 0 .../include/nlohmann/detail/json_ref.hpp | 0 .../include/nlohmann/detail/macro_scope.hpp | 0 .../include/nlohmann/detail/macro_unscope.hpp | 0 .../nlohmann/detail/meta/call_std/begin.hpp | 0 .../nlohmann/detail/meta/call_std/end.hpp | 0 .../nlohmann/detail/meta/cpp_future.hpp | 0 .../include/nlohmann/detail/meta/detected.hpp | 0 .../nlohmann/detail/meta/identity_tag.hpp | 0 .../include/nlohmann/detail/meta/is_sax.hpp | 0 .../include/nlohmann/detail/meta/std_fs.hpp | 0 .../nlohmann/detail/meta/type_traits.hpp | 0 .../include/nlohmann/detail/meta/void_t.hpp | 0 .../nlohmann/detail/output/binary_writer.hpp | 0 .../detail/output/output_adapters.hpp | 0 .../nlohmann/detail/output/serializer.hpp | 0 .../include/nlohmann/detail/string_concat.hpp | 0 .../include/nlohmann/detail/string_escape.hpp | 0 .../include/nlohmann/detail/string_utils.hpp | 0 .../include/nlohmann/detail/value_t.hpp | 0 .../third_party/include/nlohmann/json.hpp | 0 .../third_party/include/nlohmann/json_fwd.hpp | 0 .../include/nlohmann/ordered_map.hpp | 0 .../nlohmann/thirdparty/hedley/hedley.hpp | 0 .../thirdparty/hedley/hedley_undef.hpp | 0 backend/{ => service}/third_party/init.bash | 0 backend/tests/synchronized_test.cpp | 75 --- cmake/AdminiveConfig.cmake.in | 26 +- 101 files changed, 1915 insertions(+), 985 deletions(-) delete mode 100644 backend/include/adminive/adapters/httplib.hpp delete mode 100644 backend/include/adminive/http.hpp delete mode 100644 backend/include/adminive/synchronized.hpp create mode 100644 backend/library/CMakeLists.txt rename backend/{ => library}/include/adminive/adapter.hpp (100%) rename backend/{ => library}/include/adminive/adminive.hpp (80%) rename backend/{ => library}/include/adminive/amis.hpp (98%) create mode 100644 backend/library/include/adminive/collection.hpp rename backend/{ => library}/include/adminive/concepts.hpp (100%) rename backend/{ => library}/include/adminive/descriptor.hpp (97%) create mode 100644 backend/library/include/adminive/http.hpp rename backend/{ => library}/include/adminive/json.hpp (89%) rename backend/{ => library}/include/adminive/status.hpp (100%) create mode 100644 backend/library/include/adminive/synchronized.hpp rename backend/{ => library}/include/adminive/value.hpp (100%) rename backend/{ => library}/tests/core_adapter_test.cpp (96%) delete mode 100644 backend/main.cmake create mode 100644 backend/service/CMakeLists.txt rename backend/{ => service}/include/adminive/adapters/boost_pfr.hpp (100%) rename backend/{ => service}/include/adminive/adapters/drogon.hpp (52%) create mode 100644 backend/service/include/adminive/adapters/httplib.hpp rename backend/{ => service}/include/adminive/adapters/magic_enum.hpp (100%) rename backend/{ => service}/include/adminive/adapters/nlohmann_json.hpp (100%) rename backend/{ => service}/src/config_store.cpp (91%) rename backend/{ => service}/src/config_store.hpp (77%) rename backend/{ => service}/src/device_tables.cpp (96%) rename backend/{ => service}/src/device_tables.hpp (100%) rename backend/{ => service}/src/example.cpp (100%) rename backend/{ => service}/src/example.hpp (100%) rename backend/{ => service}/src/example_descriptors.hpp (99%) rename backend/{ => service}/src/main.cpp (100%) rename backend/{ => service}/src/server_app.cpp (93%) rename backend/{ => service}/src/server_app.hpp (100%) rename backend/{ => service}/tests/advanced_adapter_test.cpp (94%) rename backend/{ => service}/tests/config_store_test.cpp (100%) rename backend/{ => service}/tests/drogon_adapter_test.cpp (65%) rename backend/{ => service}/tests/fake_drogon/drogon/drogon.h (55%) rename backend/{ => service}/tests/safety_test.cpp (100%) create mode 100644 backend/service/tests/synchronized_test.cpp rename backend/{ => service}/tests/test.cpp (100%) rename backend/{ => service}/third_party/include/httplib.h (100%) rename backend/{ => service}/third_party/include/magic_enum/magic_enum.hpp (100%) rename backend/{ => service}/third_party/include/magic_enum/magic_enum_all.hpp (100%) rename backend/{ => service}/third_party/include/magic_enum/magic_enum_containers.hpp (100%) rename backend/{ => service}/third_party/include/magic_enum/magic_enum_flags.hpp (100%) rename backend/{ => service}/third_party/include/magic_enum/magic_enum_format.hpp (100%) rename backend/{ => service}/third_party/include/magic_enum/magic_enum_fuse.hpp (100%) rename backend/{ => service}/third_party/include/magic_enum/magic_enum_iostream.hpp (100%) rename backend/{ => service}/third_party/include/magic_enum/magic_enum_switch.hpp (100%) rename backend/{ => service}/third_party/include/magic_enum/magic_enum_utility.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/adl_serializer.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/byte_container_with_subtype.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/abi_macros.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/conversions/from_json.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/conversions/to_chars.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/conversions/to_json.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/exceptions.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/hash.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/input/binary_reader.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/input/input_adapters.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/input/json_sax.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/input/lexer.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/input/parser.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/input/position_t.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/iterators/internal_iterator.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/iterators/iter_impl.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/iterators/iteration_proxy.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/iterators/iterator_traits.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/iterators/json_reverse_iterator.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/iterators/primitive_iterator.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/json_custom_base_class.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/json_pointer.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/json_ref.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/macro_scope.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/macro_unscope.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/meta/call_std/begin.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/meta/call_std/end.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/meta/cpp_future.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/meta/detected.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/meta/identity_tag.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/meta/is_sax.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/meta/std_fs.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/meta/type_traits.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/meta/void_t.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/output/binary_writer.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/output/output_adapters.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/output/serializer.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/string_concat.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/string_escape.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/string_utils.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/detail/value_t.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/json.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/json_fwd.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/ordered_map.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/thirdparty/hedley/hedley.hpp (100%) rename backend/{ => service}/third_party/include/nlohmann/thirdparty/hedley/hedley_undef.hpp (100%) rename backend/{ => service}/third_party/init.bash (100%) delete mode 100644 backend/tests/synchronized_test.cpp diff --git a/README.md b/README.md index ed20e1b..718c03e 100644 --- a/README.md +++ b/README.md @@ -2,25 +2,26 @@ Adminive 使用 C++20 模板、Concepts 和可替换适配器描述业务类型。同一份后端描述驱动 JSON 双向转换、事务式赋值、字段权限、校验、amis CRUD、枚举列表列、树状配置、联动条件、日期控件、颜色控件和一次性提交。核心层不依赖具体 JSON 库、枚举反射库或结构体反射库。前端只读取 `/admin/amis` 并渲染后端返回的页面 JSON,不包含业务字段或业务判断。 -## 适配层 +## 后端分层 -核心目标只提供描述器、转换流程和 amis Schema 生成: +`backend/library` 是库级协议层,只有头文件,不依赖也不链接任何具体 JSON、HTTP Server、枚举反射或结构体反射实现。它包含 descriptor、JSON 适配协议、AMIS schema、状态协议、同步协议和抽象 `Resource_Service`: ```cmake target_link_libraries(your_target PRIVATE Adminive::Core) ``` -HTTP 资源服务与具体服务器分层: +`Adminive::Http` 与 `Adminive::Core` 指向同一套纯头文件协议层,只用于表达消费侧语义,不额外引入依赖。 + +`backend/service` 是服务桥接层,放置具体适配器、第三方头文件和示例服务。需要什么桥接就显式选择什么目标: ```cmake -target_link_libraries(your_target PRIVATE Adminive::Http) +target_link_libraries(json_target PRIVATE Adminive::Nlohmann) +target_link_libraries(enum_target PRIVATE Adminive::MagicEnum) target_link_libraries(httplib_target PRIVATE Adminive::Httplib) target_link_libraries(drogon_target PRIVATE Adminive::Drogon) ``` -`Adminive::Http` 只提供 `Resource_Service`、HTTP 响应模型和错误结构,不包含服务器头文件。`adminive/adapters/httplib.hpp` 与 `adminive/adapters/drogon.hpp` 分别负责路由注册和响应写回。 - -项目自带的 `nlohmann::json` 和 `magic_enum` 桥接不会被核心头自动包含。需要默认实现时显式包含: +`adminive/adapters/nlohmann_json.hpp`、`magic_enum.hpp`、`boost_pfr.hpp`、`httplib.hpp` 和 `drogon.hpp` 都属于服务桥接层。核心协议不会自动包含它们。项目默认示例使用: ```cpp #include "adminive/adapters/nlohmann_json.hpp" @@ -129,31 +130,65 @@ transaction.rollback = [](const Runtime_Config_Model& original, const adminive:: adminive::Resource_Service resource(runtime, "/config", std::move(transaction)); ``` -`Resource_Service` 对一次更新持有同一把对象锁,锁覆盖快照、补丁应用、`prepare`、运行时 `commit`、外部 `commit`、失败恢复和响应快照。这样外部业务线程只要通过同一个同步对象访问配置,就不会读到跨字段撕裂状态,也不会在持久化提交期间覆盖候选配置。事务回调应保持短小;使用非递归锁时,回调不能再次进入同一个同步对象,而应调用已经处于锁内的持久化实现。 +`Resource_Service` 对一次更新持有同一个一致性作用域,锁覆盖快照、补丁应用、`prepare`、运行时 `commit`、外部 `commit`、失败恢复和响应快照。绑定根 `Synchronized_Value` 时独占根 barrier;绑定 `Synchronized_Field` 子对象时默认只独占对应子对象节点并持有祖先共享 barrier,不会为了修改一个子配置锁死整个根配置。如果事务回调需要读取、复制或序列化整个根配置,则必须把资源作用域设为 `Resource_Lock_Scope::root`,让整个事务持有根独占 barrier。事务回调应保持短小;使用非递归锁时,回调不能再次进入同一个同步作用域,而应调用已经处于锁内的持久化实现。 + +```cpp +auto radio = config.member<&Application_Config::radio_service>(); +adminive::Resource_Service resource( + radio, + "/config/radio", + transaction, + adminive::Resource_Lock_Scope::root +); +``` + +`local` 是子资源默认值,适用于事务只操作该子对象或对应数据库行的场景;`root` 只用于事务确实需要根对象一致性快照的场景,例如把整个 `Application_Config` 写入一个 JSON 配置文件。 ### 配置同步协议 -配置是否写入文件或数据库,与进程内是否需要加锁是两件事。数据库负责持久化和跨进程事务;同一进程里的 HTTP 线程、工作线程和业务线程仍会同时访问运行时配置,因此默认应对整个配置结构体加锁。字段级锁无法保证多个字段之间的约束和序列化快照一致性,Adminive 不提供字段级隐式锁。 +持久化和进程内同步是两个独立问题。配置写入文件或数据库后,HTTP 线程、业务线程和后台任务仍可能同时访问运行时对象,因此动态字段仍需要进程内同步。Adminive 的默认策略是:`.editable()` 字段自动拥有字段锁;普通只读字段不拥有字段锁;`.locked()` 可以强制给不可编辑但会被程序动态修改的字段启用字段锁;`.locked(false)` 可以显式关闭某个可编辑字段的字段锁。 -`Synchronized_Value` 持有配置和值锁,`read()`、`write()`、`snapshot()` 和 `member()` 是统一访问协议。成员视图与根对象共享同一把锁,因此编辑子配置时仍会锁住整个根配置: +```cpp +return adminive::object( + "config", + ADMINIVE_FIELD(Config, editable_value).editable(), + ADMINIVE_FIELD(Config, startup_only_value), + ADMINIVE_FIELD(Config, backend_mutable_value).locked(), + ADMINIVE_FIELD(Config, no_dedicated_field_lock).editable().locked(false) +); +``` + +`Synchronized_Value` 内部同时维护根对象 barrier 和层级字段锁。所有字段读取至少持有根共享 barrier,保证它不会和整对象提交、替换或序列化事务并发;普通只读字段不再额外获取字段锁。可编辑或显式 `.locked()` 的字段再获取对应的层级字段锁,因此不同动态字段仍可以并发。`.locked(false)` 只关闭当前字段自己的专用字段锁;如果该字段是结构体且后代存在动态字段,后代所需的结构 barrier 仍然保留。对未启用字段锁的字段执行写操作时会直接升级为根独占操作,避免出现无保护写入。序列化、反序列化、对象级校验和整对象事务属于一致性操作,会独占被操作对象的 barrier,避免跨字段撕裂快照。嵌套对象序列化只独占该子对象节点,同时持有祖先共享锁,不会无条件锁死整个根配置。 ```cpp adminive::Synchronized_Value config; auto radio = config.member<&Application_Config::radio_service>(); -adminive::Resource_Service resource(radio, "/config/radio", transaction); -auto snapshot = config.snapshot(); -config.write([](Application_Config& value) { - value.default_device_type = Device_Table_Type::network; +auto worker_count = radio.member<&Radio_Service_Config::worker_count>(); +worker_count.write([](auto& value) { + value = 8; +}); +auto snapshot = radio.snapshot(); +auto root_snapshot = config.snapshot(); + +adminive::Synchronized_Value reflected_config; +reflected_config.field<0>().write([](auto& value) { + value = 8; }); ``` -直接调用 `to_json()`、`to_frontend_json()`、`assign_json()` 或 `apply_frontend_patch()` 处理同步对象和成员视图时也会进入同一把锁。确实由外部生命周期保证单线程访问时,将锁模板参数替换为 `Empty_Lock`,接口和执行路径不变: +描述协议 `adminive.resource` 版本为 3。每个字段会输出 `lock_mode` 和最终计算后的 `synchronized`,因此服务层能够明确知道该字段的并发语义,而不是由具体 JSON 或 HTTP 后端隐式决定。 + +数据库事务不能替代这套运行时锁。`Resource_Service` 在一次 HTTP 更新中持有对应资源的一致性锁作用域,覆盖候选快照、补丁应用、对象校验、运行时提交、持久化回调、失败回滚和响应快照。数据库负责持久化原子性;对象/字段 barrier 负责当前进程中的运行时一致性。 + +如果对象生命周期已经由外部严格保证为单线程,可以统一换成 `Empty_Lock`,不需要维护另一套无锁实现: ```cpp adminive::Synchronized_Value config; adminive::Resource_Service resource(config, "/config"); ``` +同步对象不公开原始值和 mutex。普通业务代码只能通过 `read()`、`write()`、`snapshot()`、`replace()`、显式成员的 `member<&T::x>()` 和反射字段的 `field()` 进入同步协议,避免重新出现 `data() + mutex()` 由调用方手工配对的问题。同步回调会静态拒绝引用、指针、`reference_wrapper` 和 ranges view 等常见引用逃逸形式;自定义返回类型如果内部保存受保护对象的地址或引用,仍由调用方保证不会把它带出锁作用域。 + ### 外部控件适配 复杂业务类型通过 `Control_Adapter` 明确决定表单控件和列表列。框架不会把 `vector` 或 `map` 隐式决定为表格、列表、分页或标签页;未配置控件的结构类型会在 Schema 生成时给出明确错误。 @@ -231,7 +266,7 @@ options.filters = {"LoginFilter", "AdminPermissionFilter"}; resource.bind(drogon::app(), std::move(options)); ``` -整体状态使用 `Drogon_Status_Resource` 注册 `/descriptor`、`/amis` 和 `/data`。所有 Drogon 路由都会把异常转换成结构化 HTTP 500。 +整体状态使用 `Drogon_Status_Resource` 注册 `/descriptor`、`/amis` 和 `/data`。列表 CRUD 使用 `Drogon_Collection_Resource`,和 httplib 的 `Http_Collection_Resource` 共用纯头文件 `Collection_Service`;Drogon/httplib 只负责路由参数、请求体和响应桥接,不再各自实现 CRUD 协议。当前 `Collection_Service` 是线程安全的内存集合服务,尚未定义数据库 repository/transaction 协议;需要把列表直接落数据库时,应在这一层补持久化抽象,而不是把数据库逻辑塞回 Drogon 或 httplib binder。所有 Drogon 路由都会把异常转换成结构化 HTTP 500。 ### 描述器驱动状态 @@ -242,20 +277,17 @@ resource.bind(drogon::app(), std::move(options)); ```text Adminive/ ├── backend/ -│ ├── include/adminive/ -│ ├── src/ -│ │ ├── example.hpp -│ │ ├── example.cpp -│ │ ├── example_descriptors.hpp -│ │ ├── config_store.hpp -│ │ ├── config_store.cpp -│ │ ├── device_tables.hpp -│ │ ├── device_tables.cpp -│ │ ├── server_app.hpp -│ │ ├── server_app.cpp -│ │ └── main.cpp -│ ├── tests/ -│ └── third_party/ +│ ├── library/ +│ │ ├── include/adminive/ +│ │ ├── tests/ +│ │ └── CMakeLists.txt +│ ├── service/ +│ │ ├── include/adminive/adapters/ +│ │ ├── src/ +│ │ ├── tests/ +│ │ ├── third_party/ +│ │ └── CMakeLists.txt +│ └── CMakeLists.txt ├── cmake/ ├── frontend/ ├── scripts/ @@ -263,7 +295,7 @@ Adminive/ └── README.md ``` -`example.hpp` 位于 `backend/src`,只作为完整示例实现,不属于公共库头文件。 +`example.hpp` 位于 `backend/service/src`,只作为完整示例实现,不属于公共库头文件。 ## 列字段名称、显示名称和排序 diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index 47b2bfe..3dc4ad9 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -3,103 +3,13 @@ include(CMakePackageConfigHelpers) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -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") -file(TO_CMAKE_PATH "${adminive_frontend_dist_dir}" adminive_frontend_dist_dir) option(ADMINIVE_PROPAGATE_MSVC_STRICT_MODE "Propagate /permissive- to Adminive consumers" OFF) -add_library(Adminive INTERFACE) -set_target_properties(Adminive PROPERTIES EXPORT_NAME Core) -add_library(Adminive::Adminive ALIAS Adminive) -add_library(Adminive::Core ALIAS Adminive) -target_include_directories(Adminive INTERFACE "$" "$") -target_compile_features(Adminive INTERFACE cxx_std_20) -if(MSVC) - target_compile_options(Adminive INTERFACE /utf-8 /Zc:__cplusplus) - if(ADMINIVE_PROPAGATE_MSVC_STRICT_MODE) - target_compile_options(Adminive INTERFACE /permissive-) - endif() -endif() -add_library(Adminive_Http INTERFACE) -set_target_properties(Adminive_Http PROPERTIES EXPORT_NAME Http) -add_library(Adminive::Http ALIAS Adminive_Http) -target_link_libraries(Adminive_Http INTERFACE Adminive::Core) -add_library(Adminive_Httplib INTERFACE) -set_target_properties(Adminive_Httplib PROPERTIES EXPORT_NAME Httplib) -add_library(Adminive::Httplib ALIAS Adminive_Httplib) -target_link_libraries(Adminive_Httplib INTERFACE Adminive::Http) -target_include_directories(Adminive_Httplib INTERFACE "$" "$") -if(WIN32) - target_compile_definitions(Adminive_Httplib INTERFACE WINVER=0x0A00 _WIN32_WINNT=0x0A00) - target_link_libraries(Adminive_Httplib INTERFACE ws2_32) -endif() -add_library(Adminive_Drogon INTERFACE) -set_target_properties(Adminive_Drogon PROPERTIES EXPORT_NAME Drogon) -add_library(Adminive::Drogon ALIAS Adminive_Drogon) -target_link_libraries(Adminive_Drogon INTERFACE Adminive::Http "$") -if(TARGET Drogon::Drogon) - target_link_libraries(Adminive_Drogon INTERFACE "$") -elseif(TARGET drogon) - target_link_libraries(Adminive_Drogon INTERFACE "$") -endif() -add_library(Adminive_Default INTERFACE) -set_target_properties(Adminive_Default PROPERTIES EXPORT_NAME Default) -add_library(Adminive::Default ALIAS Adminive_Default) -target_link_libraries(Adminive_Default INTERFACE Adminive::Httplib) -target_include_directories(Adminive_Default INTERFACE "$" "$") -add_library(Adminive_Example STATIC "${adminive_backend_root}/src/example.cpp" "${adminive_backend_root}/src/config_store.cpp" "${adminive_backend_root}/src/device_tables.cpp" "${adminive_backend_root}/src/server_app.cpp") -target_include_directories(Adminive_Example PUBLIC "${adminive_backend_root}/src") -target_link_libraries(Adminive_Example PUBLIC Adminive::Default) -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_Example) -if(MSVC) - target_compile_options(Adminive_Example PRIVATE /permissive-) - target_compile_options(Adminive_Server PRIVATE /permissive-) -endif() -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_Example) - add_test(NAME Adminive_Test COMMAND Adminive_Test) - 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) - add_executable(Adminive_Advanced_Adapter_Test "${adminive_backend_root}/tests/advanced_adapter_test.cpp") - target_link_libraries(Adminive_Advanced_Adapter_Test PRIVATE Adminive::Default) - add_test(NAME Adminive_Advanced_Adapter_Test COMMAND Adminive_Advanced_Adapter_Test) - add_executable(Adminive_Safety_Test "${adminive_backend_root}/tests/safety_test.cpp") - target_link_libraries(Adminive_Safety_Test PRIVATE Adminive::Default) - add_test(NAME Adminive_Safety_Test COMMAND Adminive_Safety_Test) - add_executable(Adminive_Config_Store_Test "${adminive_backend_root}/tests/config_store_test.cpp") - target_include_directories(Adminive_Config_Store_Test PRIVATE "${adminive_backend_root}/src") - target_link_libraries(Adminive_Config_Store_Test PRIVATE Adminive_Example) - add_test(NAME Adminive_Config_Store_Test COMMAND Adminive_Config_Store_Test) - add_executable(Adminive_Synchronized_Test "${adminive_backend_root}/tests/synchronized_test.cpp") - target_link_libraries(Adminive_Synchronized_Test PRIVATE Adminive::Default) - add_test(NAME Adminive_Synchronized_Test COMMAND Adminive_Synchronized_Test) - add_executable(Adminive_Drogon_Adapter_Test "${adminive_backend_root}/tests/drogon_adapter_test.cpp") - target_include_directories(Adminive_Drogon_Adapter_Test BEFORE PRIVATE "${adminive_backend_root}/tests/fake_drogon") - target_link_libraries(Adminive_Drogon_Adapter_Test PRIVATE Adminive::Default) - add_test(NAME Adminive_Drogon_Adapter_Test COMMAND Adminive_Drogon_Adapter_Test) - if(MSVC) - target_compile_options(Adminive_Test PRIVATE /permissive-) - target_compile_options(Adminive_Core_Adapter_Test PRIVATE /permissive-) - target_compile_options(Adminive_Advanced_Adapter_Test PRIVATE /permissive-) - target_compile_options(Adminive_Drogon_Adapter_Test PRIVATE /permissive-) - target_compile_options(Adminive_Safety_Test PRIVATE /permissive-) - target_compile_options(Adminive_Config_Store_Test PRIVATE /permissive-) - target_compile_options(Adminive_Synchronized_Test PRIVATE /permissive-) - endif() -endif() +add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/library") +add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/service") set(adminive_install_cmake_dir "${CMAKE_INSTALL_LIBDIR}/cmake/Adminive") -configure_package_config_file("${adminive_backend_root}/../cmake/AdminiveConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/AdminiveConfig.cmake" INSTALL_DESTINATION "${adminive_install_cmake_dir}") +configure_package_config_file("${CMAKE_CURRENT_LIST_DIR}/../cmake/AdminiveConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/AdminiveConfig.cmake" INSTALL_DESTINATION "${adminive_install_cmake_dir}") write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/AdminiveConfigVersion.cmake" VERSION "${PROJECT_VERSION}" COMPATIBILITY SameMajorVersion) -install(TARGETS Adminive Adminive_Http Adminive_Httplib Adminive_Default EXPORT AdminiveTargets) -install(TARGETS Adminive_Drogon EXPORT AdminiveDrogonTargets) -install(DIRECTORY "${adminive_include_dir}/" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") -install(DIRECTORY "${adminive_third_party_include_dir}/" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") -install(EXPORT AdminiveTargets FILE AdminiveTargets.cmake NAMESPACE Adminive:: DESTINATION "${adminive_install_cmake_dir}") +install(EXPORT AdminiveCoreTargets FILE AdminiveCoreTargets.cmake NAMESPACE Adminive:: DESTINATION "${adminive_install_cmake_dir}") +install(EXPORT AdminiveServiceTargets FILE AdminiveServiceTargets.cmake NAMESPACE Adminive:: DESTINATION "${adminive_install_cmake_dir}") install(EXPORT AdminiveDrogonTargets FILE AdminiveDrogonTargets.cmake NAMESPACE Adminive:: DESTINATION "${adminive_install_cmake_dir}") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/AdminiveConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/AdminiveConfigVersion.cmake" DESTINATION "${adminive_install_cmake_dir}") diff --git a/backend/include/adminive/adapters/httplib.hpp b/backend/include/adminive/adapters/httplib.hpp deleted file mode 100644 index c343632..0000000 --- a/backend/include/adminive/adapters/httplib.hpp +++ /dev/null @@ -1,386 +0,0 @@ -#pragma once -#include "adminive/http.hpp" -#include "httplib.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -namespace adminive { -template -void write_http_json(httplib::Response& response, const Json& value) { - response.set_content(dump_json(value, 2), "application/json; charset=utf-8"); -} -template -void write_http_response(httplib::Response& response, Http_Response value) { - response.status = value.status; - write_http_json(response, value.body); -} -template -void write_http_error(httplib::Response& response, int status, const Update_Result& update) { - write_http_response(response, make_http_error(status, update)); -} -template -requires Writable_Adapted_Object && std::copy_constructible> -class Http_Resource { -public: - using Service = Resource_Service; - using Transaction = typename Service::Transaction; - Http_Resource(T& value, std::string path, Transaction transaction = {}, Lock* shared_lock = nullptr) : service_(std::make_shared(value, std::move(path), std::move(transaction), shared_lock)) {} - explicit Http_Resource(Synchronized_Value, Lock>& value, std::string path, Transaction transaction = {}) : service_(std::make_shared(value, std::move(path), std::move(transaction))) {} - template - requires (!std::is_const_v) && std::same_as::value_type, std::remove_cvref_t> - explicit Http_Resource(Synchronized_Member value, std::string path, Transaction transaction = {}) : service_(std::make_shared(value, std::move(path), std::move(transaction))) {} - Json amis_schema() const { - return service_->amis_schema(); - } - void bind(httplib::Server& server) const { - const auto service = service_; - server.Get(service->path() + "/descriptor", [service](const httplib::Request&, httplib::Response& response) { - write_http_response(response, service->descriptor_response()); - }); - server.Get(service->path() + "/data", [service](const httplib::Request&, httplib::Response& response) { - write_http_response(response, service->data_response()); - }); - server.Get(service->path() + "/amis", [service](const httplib::Request&, httplib::Response& response) { - write_http_response(response, service->amis_response()); - }); - server.Post(service->path() + "/data", [service](const httplib::Request& request, httplib::Response& response) { - write_http_response(response, service->update_response(request.body)); - }); - } -private: - std::shared_ptr service_; -}; -template -requires std::default_initializable && std::copy_constructible && std::assignable_from -class Http_Collection_Resource { -public: - explicit Http_Collection_Resource(std::string path, std::vector initial = {}) : path_(std::move(path)) { - for(auto& value : initial) { - entries_.push_back(Entry{next_id_++, std::move(value)}); - } - } - template - void register_overview_status(std::string api, std::uint64_t interval = 2000) { - overview_status_api_ = std::move(api); - overview_status_descriptor_ = to_status_descriptor_json(); - overview_status_interval_ = interval; - } - template - void register_status(std::uint64_t interval = 2000) { - using Function_Type = decltype(Function); - static_assert(std::is_member_function_pointer_v); - static_assert(std::is_invocable_v); - using Status = std::remove_cvref_t>; - static_assert(Described_Type); - status_descriptor_ = to_status_descriptor_json(); - status_interval_ = interval; - status_reader_ = [](T& value) { - return to_status_json(std::invoke(Function, value)); - }; - } - std::size_t size() const { - std::scoped_lock lock(mutex_); - return entries_.size(); - } - Json items_json() const { - std::scoped_lock lock(mutex_); - Json items = json_array(); - for(const auto& entry : entries_) { - 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(path_, descriptor, status_interval_); - } - return to_amis_crud_status_schema(path_, overview_status_api_, overview_status_descriptor_, overview_status_interval_, descriptor, status_interval_); - } - void bind(httplib::Server& server) { - server.Get(path_ + "/descriptor", [](const httplib::Request&, httplib::Response& response) { - write_http_json(response, make_http_result(0, "", to_descriptor_json())); - }); - server.Get(path_ + "/amis", [this](const httplib::Request&, httplib::Response& response) { - write_http_json(response, make_http_result(0, "", amis_schema())); - }); - if(status_reader_) { - server.Get(path_ + "/status/descriptor", [this](const httplib::Request&, httplib::Response& response) { - write_http_json(response, make_http_result(0, "", status_descriptor_)); - }); - server.Get(status_pattern(), [this](const httplib::Request& request, httplib::Response& response) { - const auto id = read_route_id(request); - std::scoped_lock lock(mutex_); - const auto iterator = find_entry(id); - if(iterator == entries_.end()) { - write_not_found(response); - return; - } - write_http_json(response, make_http_result(0, "", status_reader_(iterator->value))); - }); - } - server.Get(path_, [this](const httplib::Request& request, httplib::Response& response) { - const std::size_t page = read_size_parameter(request, "page", 1); - const std::size_t per_page = read_size_parameter(request, "perPage", 20); - std::scoped_lock lock(mutex_); - std::vector ordered_entries; - ordered_entries.reserve(entries_.size()); - for(const auto& entry : entries_) { - ordered_entries.push_back(&entry); - } - 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(); - for(std::size_t index = offset; index < end; ++index) { - json_append(items, encode_entry(*ordered_entries[index])); - } - Json data = json_object(); - json_set(data, "items", std::move(items)); - json_set(data, "total", ordered_entries.size()); - write_http_json(response, make_http_result(0, "", std::move(data))); - }); - server.Get(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) { - const auto id = read_route_id(request); - std::scoped_lock lock(mutex_); - const auto iterator = find_entry(id); - if(iterator == entries_.end()) { - write_not_found(response); - return; - } - write_http_json(response, make_http_result(0, "", encode_entry(*iterator))); - }); - server.Post(path_, [this](const httplib::Request& request, httplib::Response& response) { - try { - Json input = parse_json(request.body); - Json_Adapter::erase(input, "id"); - T value{}; - const auto result = apply_frontend_create(value, input); - if(!result.success) { - write_http_error(response, 422, result); - return; - } - std::scoped_lock lock(mutex_); - entries_.push_back(Entry{next_id_++, std::move(value)}); - write_http_json(response, make_http_result(0, "created", encode_entry(entries_.back()))); - } catch(const std::exception& error) { - response.status = 400; - write_http_json(response, make_http_result(400, error.what(), json_object())); - } - }); - if(describe().list_options().user_reorderable_) { - server.Post(path_ + "/order", [this](const httplib::Request& request, httplib::Response& response) { - try { - const Json input = parse_json(request.body); - const auto ids = read_order_ids(Json_Adapter::at(input, "ids")); - std::scoped_lock lock(mutex_); - reorder(ids); - write_http_json(response, make_http_result(0, "reordered", json_object())); - } catch(const std::exception& error) { - response.status = 400; - write_http_json(response, make_http_result(400, error.what(), json_object())); - } - }); - } - server.Put(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) { - update(request, response); - }); - server.Patch(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) { - update(request, response); - }); - server.Delete(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) { - const auto id = read_route_id(request); - std::scoped_lock lock(mutex_); - const auto iterator = find_entry(id); - if(iterator == entries_.end()) { - write_not_found(response); - return; - } - entries_.erase(iterator); - write_http_json(response, make_http_result(0, "deleted", json_object())); - }); - } -private: - struct Entry { - std::uint64_t id{}; - T value; - }; - using Iterator = typename std::vector::iterator; - static std::size_t read_size_parameter(const httplib::Request& request, const char* name, std::size_t fallback) { - if(!request.has_param(name)) { - return fallback; - } - const auto text = request.get_param_value(name); - std::size_t value{}; - const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); - if(error != std::errc{} || end != text.data() + text.size() || value == 0) { - return fallback; - } - return value; - } - static int compare_json_values(const Json& left, const Json& right) { - if(Json_Adapter::is_number(left) && Json_Adapter::is_number(right)) { - const long double left_value = Json_Adapter::number(left); - const long double right_value = Json_Adapter::number(right); - return left_value < right_value ? -1 : left_value > right_value ? 1 : 0; - } - if(Json_Adapter::is_string(left) && Json_Adapter::is_string(right)) { - const auto left_value = json_get(left); - const auto right_value = json_get(right); - return left_value < right_value ? -1 : left_value > right_value ? 1 : 0; - } - if(Json_Adapter::is_boolean(left) && Json_Adapter::is_boolean(right)) { - const bool left_value = json_get(left); - const bool right_value = json_get(right); - return left_value == right_value ? 0 : left_value ? 1 : -1; - } - 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) { - if(name == "id") { - return true; - } - bool sortable{}; - std::apply([&](const auto&... field) { - ((field.name() == name ? sortable = field.is_sortable() : false), ...); - }, describe().fields()); - return sortable; - } - static std::uint64_t read_route_id(const httplib::Request& request) { - const std::string text = request.matches[1].str(); - std::uint64_t id{}; - const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), id); - if(error != std::errc{} || end != text.data() + text.size()) { - return 0; - } - return id; - } - static std::vector read_order_ids(const Json& value) { - std::vector result; - if(Json_Adapter::is_array(value)) { - for(std::size_t index = 0; index < Json_Adapter::size(value); ++index) { - const auto& item = Json_Adapter::at(value, index); - result.push_back(Json_Adapter::is_string(item) ? std::stoull(json_get(item)) : json_get(item)); - } - return result; - } - const std::string text = json_get(value); - std::size_t begin{}; - while(begin < text.size()) { - const auto end = text.find(',', begin); - result.push_back(std::stoull(text.substr(begin, end == std::string::npos ? text.size() - begin : end - begin))); - if(end == std::string::npos) { - break; - } - begin = end + 1; - } - return result; - } - void apply_request_sort(const httplib::Request& request, std::vector& entries) const { - const auto descriptor = describe(); - const auto& options = descriptor.list_options(); - std::string order_by = options.default_order_by; - std::string order_dir = options.default_order_dir; - if(request.has_param("orderBy")) { - order_by = request.get_param_value("orderBy"); - order_dir = request.has_param("orderDir") ? request.get_param_value("orderDir") : "asc"; - } - if(order_by.empty() || !is_sortable_field(order_by)) { - return; - } - const bool descending = order_dir == "desc"; - std::stable_sort(entries.begin(), entries.end(), [&](const Entry* left, const Entry* right) { - int comparison{}; - if(order_by == "id") { - comparison = left->id < right->id ? -1 : left->id > right->id ? 1 : 0; - } else { - const auto left_json = to_frontend_json(left->value); - const auto right_json = to_frontend_json(right->value); - comparison = compare_json_values(Json_Adapter::at(left_json, order_by), Json_Adapter::at(right_json, order_by)); - } - return descending ? comparison > 0 : comparison < 0; - }); - } - std::string item_pattern() const { - return path_ + R"(/(\d+))"; - } - std::string status_pattern() const { - return path_ + R"(/(\d+)/status)"; - } - Iterator find_entry(std::uint64_t id) { - return std::find_if(entries_.begin(), entries_.end(), [id](const Entry& entry) { - return entry.id == id; - }); - } - static Json encode_entry(const Entry& entry) { - Json result = to_frontend_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, make_http_result(404, "record not found", json_object())); - } - void reorder(const std::vector& ids) { - std::vector remaining = std::move(entries_); - std::vector reordered; - reordered.reserve(remaining.size()); - for(const auto id : ids) { - const auto iterator = std::find_if(remaining.begin(), remaining.end(), [id](const Entry& entry) { - return entry.id == id; - }); - if(iterator != remaining.end()) { - reordered.push_back(std::move(*iterator)); - remaining.erase(iterator); - } - } - for(auto& entry : remaining) { - reordered.push_back(std::move(entry)); - } - entries_ = std::move(reordered); - } - void update(const httplib::Request& request, httplib::Response& response) { - try { - Json patch = parse_json(request.body); - Json_Adapter::erase(patch, "id"); - const auto id = read_route_id(request); - std::scoped_lock lock(mutex_); - const auto iterator = find_entry(id); - if(iterator == entries_.end()) { - write_not_found(response); - return; - } - const auto result = apply_frontend_patch(iterator->value, patch); - if(!result.success) { - write_http_error(response, 422, result); - return; - } - write_http_json(response, make_http_result(0, result.message, encode_entry(*iterator))); - } catch(const std::exception& error) { - response.status = 400; - write_http_json(response, make_http_result(400, error.what(), json_object())); - } - } - std::string path_; - std::string overview_status_api_; - Json overview_status_descriptor_{}; - std::uint64_t overview_status_interval_{2000}; - Json status_descriptor_{}; - std::function status_reader_; - std::uint64_t status_interval_{2000}; - std::vector entries_; - std::uint64_t next_id_{1}; - mutable Lock mutex_; -}; -} diff --git a/backend/include/adminive/http.hpp b/backend/include/adminive/http.hpp deleted file mode 100644 index d860a95..0000000 --- a/backend/include/adminive/http.hpp +++ /dev/null @@ -1,197 +0,0 @@ -#pragma once -#include "adminive/amis.hpp" -#include "adminive/synchronized.hpp" -#include -#include -#include -#include -#include -#include -namespace adminive { -struct Request_Context { - std::string user; - std::string request_id; - std::string remote_address; - std::map attributes; -}; -template -struct Http_Response { - int status{200}; - Json body{}; -}; -template -Json make_http_result(int status, std::string message, Data&& data) { - Json result = json_object(); - json_set(result, "status", status); - json_set(result, "msg", std::move(message)); - json_set(result, "data", std::forward(data)); - return result; -} -template -Http_Response make_http_success(Json data, std::string message = {}) { - return Http_Response{200, make_http_result(0, std::move(message), std::move(data))}; -} -template -Http_Response make_http_error(int status, const Update_Result& update) { - Json result = make_http_result(status, update.message, json_object()); - Json errors = json_object(); - Json field_errors = json_object(); - for(const auto& [name, message] : update.field_errors) { - json_set(errors, name, message); - json_set(field_errors, name, message); - } - json_set(result, "errors", std::move(errors)); - json_set(result, "field_errors", std::move(field_errors)); - return Http_Response{status, std::move(result)}; -} -template -Http_Response make_http_error(int status, std::string message) { - return Http_Response{status, make_http_result(status, std::move(message), json_object())}; -} -template -struct Resource_Transaction { - using Prepare_Function = std::function; - using Commit_Function = std::function; - using Rollback_Function = std::function; - Prepare_Function prepare; - Commit_Function commit; - Rollback_Function rollback; -}; -template -Resource_Transaction make_resource_transaction(Function&& function) { - Resource_Transaction result; - result.commit = std::forward(function); - return result; -} -template -requires Writable_Adapted_Object && std::copy_constructible> -class Resource_Service { -public: - using Object = std::remove_cvref_t; - using Model = Object_Model_Type; - using Transaction = Resource_Transaction; - Resource_Service(T& value, std::string path, Transaction transaction = {}, Lock* shared_lock = nullptr) : value_(value), path_(std::move(path)), transaction_(std::move(transaction)), shared_lock_(shared_lock) {} - explicit Resource_Service(Synchronized_Value& value, std::string path, Transaction transaction = {}) : Resource_Service(value.unsafe_value(), std::move(path), std::move(transaction), &value.mutex()) {} - template - requires (!std::is_const_v) && std::same_as::value_type, Object> - explicit Resource_Service(Synchronized_Member value, std::string path, Transaction transaction = {}) : Resource_Service(value.unsafe_value(), std::move(path), std::move(transaction), &value.mutex()) {} - const std::string& path() const noexcept { - return path_; - } - Json amis_schema() const { - std::scoped_lock lock(resource_mutex()); - return to_amis_form_schema(value_, path_ + "/data", describe().list_options().confirm_label); - } - Http_Response descriptor_response() const noexcept { - return safe_response([&] { - return make_http_success(to_descriptor_json()); - }); - } - Http_Response data_response() const noexcept { - return safe_response([&] { - std::scoped_lock lock(resource_mutex()); - return make_http_success(to_frontend_json(value_)); - }); - } - Http_Response amis_response() const noexcept { - return safe_response([&] { - return make_http_success(amis_schema()); - }); - } - Http_Response update_response(std::string_view body, const Request_Context& context = {}) noexcept { - return safe_response([&] { - Json patch; - try { - patch = parse_json(body); - } catch(const std::exception& error) { - return make_http_error(400, error.what()); - } - std::scoped_lock lock(resource_mutex()); - Model original = Object_Adapter::snapshot(value_); - Model candidate = original; - auto result = apply_model_json(candidate, patch, Write_Mode::update, false); - if(!result.success) { - return make_http_error(422, result); - } - bool transaction_started{}; - bool runtime_commit_started{}; - try { - if(transaction_.prepare) { - transaction_started = true; - transaction_.prepare(candidate, context); - } - runtime_commit_started = true; - Object_Adapter::commit(value_, candidate); - if(transaction_.commit) { - transaction_started = true; - transaction_.commit(candidate, context); - } - } catch(const Json_Assignment_Error& error) { - rollback_after_failure_locked(original, context, runtime_commit_started, transaction_started); - return make_http_error(422, error.result()); - } catch(const Field_Validation_Error& error) { - rollback_after_failure_locked(original, context, runtime_commit_started, transaction_started); - return make_http_error(422, object_commit_error(error)); - } catch(const std::exception& error) { - const std::string rollback_error = rollback_after_failure_locked(original, context, runtime_commit_started, transaction_started); - return make_http_error(500, rollback_error.empty() ? error.what() : std::string(error.what()) + "; rollback failed: " + rollback_error); - } catch(...) { - const std::string rollback_error = rollback_after_failure_locked(original, context, runtime_commit_started, transaction_started); - return make_http_error(500, rollback_error.empty() ? "unknown server error" : "unknown server error; rollback failed: " + rollback_error); - } - return make_http_success(to_frontend_json(value_), std::move(result.message)); - }); - } -private: - template - static Http_Response safe_response(Function&& function) noexcept { - try { - return std::forward(function)(); - } catch(const Json_Assignment_Error& error) { - return make_http_error(422, error.result()); - } catch(const Field_Validation_Error& error) { - return make_http_error(422, object_commit_error(error)); - } catch(const std::exception& error) { - return make_http_error(500, error.what()); - } catch(...) { - return make_http_error(500, "unknown server error"); - } - } - std::string rollback_after_failure_locked(const Model& original, const Request_Context& context, bool restore_runtime, bool transaction_started) noexcept { - std::string message; - if(restore_runtime) { - try { - Object_Adapter::commit(value_, original); - } catch(const std::exception& error) { - message = error.what(); - } catch(...) { - message = "runtime object rollback failed"; - } - } - if(transaction_started && transaction_.rollback) { - try { - transaction_.rollback(original, context); - } catch(const std::exception& error) { - if(!message.empty()) { - message += "; "; - } - message += error.what(); - } catch(...) { - if(!message.empty()) { - message += "; "; - } - message += "external rollback failed"; - } - } - return message; - } - Lock& resource_mutex() const noexcept { - return shared_lock_ ? *shared_lock_ : lock_; - } - T& value_; - std::string path_; - Transaction transaction_; - Lock* shared_lock_{}; - mutable Lock lock_{}; -}; -} diff --git a/backend/include/adminive/synchronized.hpp b/backend/include/adminive/synchronized.hpp deleted file mode 100644 index c261e97..0000000 --- a/backend/include/adminive/synchronized.hpp +++ /dev/null @@ -1,122 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -namespace adminive { -struct Empty_Lock { - void lock() noexcept {} - bool try_lock() noexcept { - return true; - } - void unlock() noexcept {} -}; -template -concept Basic_Lock = std::default_initializable && requires(T& value) { - value.lock(); - value.unlock(); -}; -template -concept Synchronized_Result = std::is_void_v || (!std::is_reference_v && !std::is_pointer_v); -template -class Synchronized_Member; -template -class Synchronized_Value { -public: - using value_type = T; - using lock_type = Lock; - Synchronized_Value() requires std::default_initializable = default; - explicit Synchronized_Value(T value) : value_(std::move(value)) {} - Synchronized_Value(const Synchronized_Value&) = delete; - Synchronized_Value& operator=(const Synchronized_Value&) = delete; - template - requires Synchronized_Result> - decltype(auto) read(Function&& function) const { - std::scoped_lock guard(lock_); - return std::invoke(std::forward(function), std::as_const(value_)); - } - template - requires Synchronized_Result> - decltype(auto) write(Function&& function) { - std::scoped_lock guard(lock_); - return std::invoke(std::forward(function), value_); - } - T snapshot() const requires std::copy_constructible { - return read([](const T& value) { - return value; - }); - } - void replace(T value) requires std::assignable_from { - write([&](T& target) { - target = std::move(value); - }); - } - template - auto member() noexcept { - return Synchronized_Member(*this); - } - template - auto member() const noexcept { - return Synchronized_Member(*this); - } - T& unsafe_value() noexcept { - return value_; - } - const T& unsafe_value() const noexcept { - return value_; - } - Lock& mutex() const noexcept { - return lock_; - } -private: - T value_{}; - mutable Lock lock_{}; -}; -template -class Synchronized_Member { -public: - using root_type = std::remove_const_t; - using value_type = std::remove_cvref_t().*Member)>; - using lock_type = Lock; - using owner_type = std::conditional_t, const Synchronized_Value, Synchronized_Value>; - explicit Synchronized_Member(Synchronized_Value& owner) requires (!std::is_const_v) : owner_(&owner) {} - explicit Synchronized_Member(const Synchronized_Value& owner) requires std::is_const_v : owner_(&owner) {} - template - requires Synchronized_Result> - decltype(auto) read(Function&& function) const { - return owner_->read([&](const root_type& root) -> decltype(auto) { - return std::invoke(std::forward(function), root.*Member); - }); - } - template - requires (!std::is_const_v) && Synchronized_Result> - decltype(auto) write(Function&& function) { - return owner_->write([&](root_type& root) -> decltype(auto) { - return std::invoke(std::forward(function), root.*Member); - }); - } - value_type snapshot() const requires std::copy_constructible { - return read([](const value_type& value) { - return value; - }); - } - void replace(value_type value) requires (!std::is_const_v) && std::assignable_from { - write([&](value_type& target) { - target = std::move(value); - }); - } - decltype(auto) unsafe_value() const noexcept { - if constexpr(std::is_const_v) { - return std::as_const(owner_->unsafe_value()).*Member; - } else { - return owner_->unsafe_value().*Member; - } - } - Lock& mutex() const noexcept { - return owner_->mutex(); - } -private: - owner_type* owner_; -}; -} diff --git a/backend/library/CMakeLists.txt b/backend/library/CMakeLists.txt new file mode 100644 index 0000000..9c5a36c --- /dev/null +++ b/backend/library/CMakeLists.txt @@ -0,0 +1,24 @@ +set(adminive_library_include_dir "${CMAKE_CURRENT_LIST_DIR}/include") +add_library(Adminive INTERFACE) +set_target_properties(Adminive PROPERTIES EXPORT_NAME Core) +add_library(Adminive::Adminive ALIAS Adminive) +add_library(Adminive::Core ALIAS Adminive) +add_library(Adminive::Http ALIAS Adminive) +target_include_directories(Adminive INTERFACE "$" "$") +target_compile_features(Adminive INTERFACE cxx_std_20) +if(MSVC) + target_compile_options(Adminive INTERFACE /utf-8 /Zc:__cplusplus) + if(ADMINIVE_PROPAGATE_MSVC_STRICT_MODE) + target_compile_options(Adminive INTERFACE /permissive-) + endif() +endif() +if(BUILD_TESTING) + add_executable(Adminive_Core_Adapter_Test "${CMAKE_CURRENT_LIST_DIR}/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) + if(MSVC) + target_compile_options(Adminive_Core_Adapter_Test PRIVATE /permissive-) + endif() +endif() +install(TARGETS Adminive EXPORT AdminiveCoreTargets) +install(DIRECTORY "${adminive_library_include_dir}/" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") diff --git a/backend/include/adminive/adapter.hpp b/backend/library/include/adminive/adapter.hpp similarity index 100% rename from backend/include/adminive/adapter.hpp rename to backend/library/include/adminive/adapter.hpp diff --git a/backend/include/adminive/adminive.hpp b/backend/library/include/adminive/adminive.hpp similarity index 80% rename from backend/include/adminive/adminive.hpp rename to backend/library/include/adminive/adminive.hpp index e4f80f9..b51ad8a 100644 --- a/backend/include/adminive/adminive.hpp +++ b/backend/library/include/adminive/adminive.hpp @@ -1,8 +1,10 @@ #pragma once #include "adminive/adapter.hpp" #include "adminive/amis.hpp" +#include "adminive/collection.hpp" #include "adminive/concepts.hpp" #include "adminive/descriptor.hpp" +#include "adminive/http.hpp" #include "adminive/json.hpp" #include "adminive/status.hpp" #include "adminive/synchronized.hpp" diff --git a/backend/include/adminive/amis.hpp b/backend/library/include/adminive/amis.hpp similarity index 98% rename from backend/include/adminive/amis.hpp rename to backend/library/include/adminive/amis.hpp index b558158..e29e9be 100644 --- a/backend/include/adminive/amis.hpp +++ b/backend/library/include/adminive/amis.hpp @@ -459,15 +459,15 @@ Json to_amis_form_schema(const T& value, std::string submit_api = {}, std::strin } return result; } -template -Json to_amis_form_schema(const Synchronized_Value& value, std::string submit_api = {}, std::string submit_label = "Apply") { +template +Json to_amis_form_schema(const Synchronized_Value& value, std::string submit_api = {}, std::string submit_label = "Apply") { return value.read([&](const T& item) { return adminive::to_amis_form_schema(item, std::move(submit_api), std::move(submit_label)); }); } -template -Json to_amis_form_schema(const Synchronized_Member& value, std::string submit_api = {}, std::string submit_label = "Apply") { - return value.read([&](const auto& item) { +template +Json to_amis_form_schema(const Synchronized_Field& value, std::string submit_api = {}, std::string submit_label = "Apply") { + return value.object_read([&](const auto& item) { return adminive::to_amis_form_schema(item, std::move(submit_api), std::move(submit_label)); }); } diff --git a/backend/library/include/adminive/collection.hpp b/backend/library/include/adminive/collection.hpp new file mode 100644 index 0000000..4688da9 --- /dev/null +++ b/backend/library/include/adminive/collection.hpp @@ -0,0 +1,329 @@ +#pragma once +#include "adminive/http.hpp" +#include "adminive/status.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace adminive { +struct Collection_Query { + std::size_t page{1}; + std::size_t per_page{20}; + std::string order_by; + std::string order_dir; +}; +template +requires std::default_initializable && std::copy_constructible && std::assignable_from +class Collection_Service { +public: + explicit Collection_Service(std::string path, std::vector initial = {}) : path_(std::move(path)) { + for(auto& value : initial) { + entries_.push_back(Entry{next_id_++, std::move(value)}); + } + } + template + void register_overview_status(std::string api, std::uint64_t interval = 2000) { + overview_status_api_ = std::move(api); + overview_status_descriptor_ = to_status_descriptor_json(); + overview_status_interval_ = interval; + } + template + void register_status(std::uint64_t interval = 2000) { + using Function_Type = decltype(Function); + static_assert(std::is_member_function_pointer_v); + static_assert(std::is_invocable_v); + using Status = std::remove_cvref_t>; + static_assert(Described_Type); + status_descriptor_ = to_status_descriptor_json(); + status_interval_ = interval; + status_reader_ = [](T& value) { + return to_status_json(std::invoke(Function, value)); + }; + } + const std::string& path() const noexcept { + return path_; + } + bool has_status() const noexcept { + return static_cast(status_reader_); + } + bool user_reorderable() const { + return describe().list_options().user_reorderable_; + } + std::size_t size() const { + std::scoped_lock lock(mutex_); + return entries_.size(); + } + Json items_json() const { + std::scoped_lock lock(mutex_); + Json items = json_array(); + for(const auto& entry : entries_) { + 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(path_, descriptor, status_interval_); + } + return to_amis_crud_status_schema(path_, overview_status_api_, overview_status_descriptor_, overview_status_interval_, descriptor, status_interval_); + } + Http_Response descriptor_response() const noexcept { + return safe_response([&] { + return make_http_success(to_descriptor_json()); + }); + } + Http_Response amis_response() const noexcept { + return safe_response([&] { + return make_http_success(amis_schema()); + }); + } + Http_Response status_descriptor_response() const noexcept { + return safe_response([&] { + if(!status_reader_) { + return make_http_error(404, "item status is not configured"); + } + return make_http_success(status_descriptor_); + }); + } + Http_Response status_response(std::uint64_t id) noexcept { + return safe_response([&] { + if(!status_reader_) { + return make_http_error(404, "item status is not configured"); + } + std::scoped_lock lock(mutex_); + const auto iterator = find_entry(id); + if(iterator == entries_.end()) { + return make_http_error(404, "record not found"); + } + return make_http_success(status_reader_(iterator->value)); + }); + } + Http_Response list_response(Collection_Query query = {}) const noexcept { + return safe_response([&] { + std::scoped_lock lock(mutex_); + std::vector ordered_entries; + ordered_entries.reserve(entries_.size()); + for(const auto& entry : entries_) { + ordered_entries.push_back(&entry); + } + apply_sort(query, ordered_entries); + const std::size_t page = query.page == 0 ? 1 : query.page; + const std::size_t per_page = query.per_page == 0 ? 20 : query.per_page; + const std::size_t page_index = page - 1; + const std::size_t offset = page_index > ordered_entries.size() / per_page ? ordered_entries.size() : page_index * per_page; + const std::size_t end = offset + std::min(per_page, ordered_entries.size() - offset); + Json items = json_array(); + for(std::size_t index = offset; index < end; ++index) { + json_append(items, encode_entry(*ordered_entries[index])); + } + Json data = json_object(); + json_set(data, "items", std::move(items)); + json_set(data, "total", ordered_entries.size()); + return make_http_success(std::move(data)); + }); + } + Http_Response item_response(std::uint64_t id) const noexcept { + return safe_response([&] { + std::scoped_lock lock(mutex_); + const auto iterator = find_entry(id); + if(iterator == entries_.end()) { + return make_http_error(404, "record not found"); + } + return make_http_success(encode_entry(*iterator)); + }); + } + Http_Response create_response(std::string_view body) noexcept { + return safe_response([&] { + Json input = parse_json(body); + Json_Adapter::erase(input, "id"); + T value{}; + const auto result = apply_frontend_create(value, input); + if(!result.success) { + return make_http_error(422, result); + } + std::scoped_lock lock(mutex_); + entries_.push_back(Entry{next_id_++, std::move(value)}); + return make_http_success(encode_entry(entries_.back()), "created"); + }); + } + Http_Response update_response(std::uint64_t id, std::string_view body) noexcept { + return safe_response([&] { + Json patch = parse_json(body); + Json_Adapter::erase(patch, "id"); + std::scoped_lock lock(mutex_); + const auto iterator = find_entry(id); + if(iterator == entries_.end()) { + return make_http_error(404, "record not found"); + } + const auto result = apply_frontend_patch(iterator->value, patch); + if(!result.success) { + return make_http_error(422, result); + } + return make_http_success(encode_entry(*iterator), result.message); + }); + } + Http_Response delete_response(std::uint64_t id) noexcept { + return safe_response([&] { + std::scoped_lock lock(mutex_); + const auto iterator = find_entry(id); + if(iterator == entries_.end()) { + return make_http_error(404, "record not found"); + } + entries_.erase(iterator); + return make_http_success(json_object(), "deleted"); + }); + } + Http_Response reorder_response(std::string_view body) noexcept { + return safe_response([&] { + const Json input = parse_json(body); + const auto ids = read_order_ids(Json_Adapter::at(input, "ids")); + std::scoped_lock lock(mutex_); + reorder(ids); + return make_http_success(json_object(), "reordered"); + }); + } +private: + struct Entry { + std::uint64_t id{}; + T value; + }; + using Iterator = typename std::vector::iterator; + using Const_Iterator = typename std::vector::const_iterator; + template + static Http_Response safe_response(Function&& function) noexcept { + try { + return std::forward(function)(); + } catch(const Json_Assignment_Error& error) { + return make_http_error(422, error.result()); + } catch(const Field_Validation_Error& error) { + return make_http_error(422, object_commit_error(error)); + } catch(const std::exception& error) { + return make_http_error(400, error.what()); + } catch(...) { + return make_http_error(500, "unknown server error"); + } + } + static int compare_json_values(const Json& left, const Json& right) { + if(Json_Adapter::is_number(left) && Json_Adapter::is_number(right)) { + const long double left_value = Json_Adapter::number(left); + const long double right_value = Json_Adapter::number(right); + return left_value < right_value ? -1 : left_value > right_value ? 1 : 0; + } + if(Json_Adapter::is_string(left) && Json_Adapter::is_string(right)) { + const auto left_value = json_get(left); + const auto right_value = json_get(right); + return left_value < right_value ? -1 : left_value > right_value ? 1 : 0; + } + if(Json_Adapter::is_boolean(left) && Json_Adapter::is_boolean(right)) { + const bool left_value = json_get(left); + const bool right_value = json_get(right); + return left_value == right_value ? 0 : left_value ? 1 : -1; + } + 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) { + if(name == "id") { + return true; + } + bool sortable{}; + std::apply([&](const auto&... field) { + ((field.name() == name ? sortable = field.is_sortable() : false), ...); + }, describe().fields()); + return sortable; + } + static std::vector read_order_ids(const Json& value) { + std::vector result; + if(Json_Adapter::is_array(value)) { + for(std::size_t index = 0; index < Json_Adapter::size(value); ++index) { + const auto& item = Json_Adapter::at(value, index); + result.push_back(Json_Adapter::is_string(item) ? std::stoull(json_get(item)) : json_get(item)); + } + return result; + } + const std::string text = json_get(value); + std::size_t begin{}; + while(begin < text.size()) { + const auto end = text.find(',', begin); + result.push_back(std::stoull(text.substr(begin, end == std::string::npos ? text.size() - begin : end - begin))); + if(end == std::string::npos) { + break; + } + begin = end + 1; + } + return result; + } + void apply_sort(const Collection_Query& query, std::vector& entries) const { + const auto descriptor = describe(); + const auto& options = descriptor.list_options(); + const std::string order_by = query.order_by.empty() ? options.default_order_by : query.order_by; + const std::string order_dir = query.order_by.empty() ? options.default_order_dir : query.order_dir.empty() ? "asc" : query.order_dir; + if(order_by.empty() || !is_sortable_field(order_by)) { + return; + } + const bool descending = order_dir == "desc"; + std::stable_sort(entries.begin(), entries.end(), [&](const Entry* left, const Entry* right) { + int comparison{}; + if(order_by == "id") { + comparison = left->id < right->id ? -1 : left->id > right->id ? 1 : 0; + } else { + const auto left_json = to_frontend_json(left->value); + const auto right_json = to_frontend_json(right->value); + comparison = compare_json_values(Json_Adapter::at(left_json, order_by), Json_Adapter::at(right_json, order_by)); + } + return descending ? comparison > 0 : comparison < 0; + }); + } + Iterator find_entry(std::uint64_t id) { + return std::find_if(entries_.begin(), entries_.end(), [id](const Entry& entry) { + return entry.id == id; + }); + } + Const_Iterator find_entry(std::uint64_t id) const { + return std::find_if(entries_.begin(), entries_.end(), [id](const Entry& entry) { + return entry.id == id; + }); + } + static Json encode_entry(const Entry& entry) { + Json result = to_frontend_json(entry.value); + json_set(result, "id", entry.id); + return result; + } + void reorder(const std::vector& ids) { + std::vector remaining = std::move(entries_); + std::vector reordered; + reordered.reserve(remaining.size()); + for(const auto id : ids) { + const auto iterator = std::find_if(remaining.begin(), remaining.end(), [id](const Entry& entry) { + return entry.id == id; + }); + if(iterator != remaining.end()) { + reordered.push_back(std::move(*iterator)); + remaining.erase(iterator); + } + } + for(auto& entry : remaining) { + reordered.push_back(std::move(entry)); + } + entries_ = std::move(reordered); + } + std::string path_; + std::string overview_status_api_; + Json overview_status_descriptor_{}; + std::uint64_t overview_status_interval_{2000}; + Json status_descriptor_{}; + std::function status_reader_; + std::uint64_t status_interval_{2000}; + std::vector entries_; + std::uint64_t next_id_{1}; + mutable Lock mutex_; +}; +} diff --git a/backend/include/adminive/concepts.hpp b/backend/library/include/adminive/concepts.hpp similarity index 100% rename from backend/include/adminive/concepts.hpp rename to backend/library/include/adminive/concepts.hpp diff --git a/backend/include/adminive/descriptor.hpp b/backend/library/include/adminive/descriptor.hpp similarity index 97% rename from backend/include/adminive/descriptor.hpp rename to backend/library/include/adminive/descriptor.hpp index 08feef0..1c53be4 100644 --- a/backend/include/adminive/descriptor.hpp +++ b/backend/library/include/adminive/descriptor.hpp @@ -12,6 +12,11 @@ #include #include namespace adminive { +enum class Field_Lock_Mode { + automatic, + enabled, + disabled +}; template struct Type_Descriptor; template @@ -101,6 +106,11 @@ public: result.editable_ = value; return result; } + Derived locked(bool value = true) const { + auto result = derived(); + result.lock_mode_ = value ? Field_Lock_Mode::enabled : Field_Lock_Mode::disabled; + return result; + } Derived creatable(bool value = true) const { auto result = derived(); result.creatable_ = value; @@ -225,6 +235,9 @@ public: bool is_editable() const noexcept { return editable_; } + Field_Lock_Mode lock_mode() const noexcept { + return lock_mode_; + } bool is_creatable() const noexcept { return creatable_; } @@ -265,6 +278,7 @@ private: std::string visible_on_; std::map enum_labels_; bool editable_{}; + Field_Lock_Mode lock_mode_{Field_Lock_Mode::automatic}; bool creatable_{}; bool readable_{true}; bool sensitive_{}; diff --git a/backend/library/include/adminive/http.hpp b/backend/library/include/adminive/http.hpp new file mode 100644 index 0000000..7344fea --- /dev/null +++ b/backend/library/include/adminive/http.hpp @@ -0,0 +1,264 @@ +#pragma once +#include "adminive/amis.hpp" +#include "adminive/synchronized.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +namespace adminive { +struct Request_Context { + std::string user; + std::string request_id; + std::string remote_address; + std::map attributes; +}; +template +struct Http_Response { + int status{200}; + Json body{}; +}; +template +Json make_http_result(int status, std::string message, Data&& data) { + Json result = json_object(); + json_set(result, "status", status); + json_set(result, "msg", std::move(message)); + json_set(result, "data", std::forward(data)); + return result; +} +template +Http_Response make_http_success(Json data, std::string message = {}) { + return Http_Response{200, make_http_result(0, std::move(message), std::move(data))}; +} +template +Http_Response make_http_error(int status, const Update_Result& update) { + Json result = make_http_result(status, update.message, json_object()); + Json errors = json_object(); + Json field_errors = json_object(); + for(const auto& [name, message] : update.field_errors) { + json_set(errors, name, message); + json_set(field_errors, name, message); + } + json_set(result, "errors", std::move(errors)); + json_set(result, "field_errors", std::move(field_errors)); + return Http_Response{status, std::move(result)}; +} +template +Http_Response make_http_error(int status, std::string message) { + return Http_Response{status, make_http_result(status, std::move(message), json_object())}; +} +enum class Resource_Lock_Scope { + local, + root +}; +template +struct Resource_Transaction { + using Prepare_Function = std::function; + using Commit_Function = std::function; + using Rollback_Function = std::function; + Prepare_Function prepare; + Commit_Function commit; + Rollback_Function rollback; +}; +template +Resource_Transaction make_resource_transaction(Function&& function) { + Resource_Transaction result; + result.commit = std::forward(function); + return result; +} +template +requires Writable_Adapted_Object && std::copy_constructible> +class Resource_Service { +public: + using Object = std::remove_cvref_t; + using Model = Object_Model_Type; + using Transaction = Resource_Transaction; + Resource_Service(const Resource_Service&) = delete; + Resource_Service& operator=(const Resource_Service&) = delete; + Resource_Service(Resource_Service&&) = delete; + Resource_Service& operator=(Resource_Service&&) = delete; + Resource_Service(T& value, std::string path, Transaction transaction = {}, Lock* shared_lock = nullptr) : path_(std::move(path)), transaction_(std::move(transaction)) { + if(shared_lock) { + configure_raw_access(value, *shared_lock); + } else { + owned_lock_ = std::make_shared(); + configure_raw_access(value, *owned_lock_); + } + } + template + explicit Resource_Service(Synchronized_Value& value, std::string path, Transaction transaction = {}) : path_(std::move(path)), transaction_(std::move(transaction)) { + read_access_ = [&value](const Read_Function& function) { + value.read([&](const Object& object) { + function(object); + }); + }; + write_access_ = [&value](const Write_Function& function) { + value.write([&](Object& object) { + function(object); + }); + }; + } + template + requires (!std::is_const_v) && std::same_as::value_type, Object> + explicit Resource_Service(Synchronized_Field value, std::string path, Transaction transaction = {}, Resource_Lock_Scope lock_scope = Resource_Lock_Scope::local) : path_(std::move(path)), transaction_(std::move(transaction)) { + if(lock_scope == Resource_Lock_Scope::root) { + configure_raw_access(detail::Synchronized_Access::value(value), detail::Synchronized_Access::mutex(value)); + return; + } + read_access_ = [value](const Read_Function& function) { + value.object_read([&](const Object& object) { + function(object); + }); + }; + write_access_ = [value](const Write_Function& function) mutable { + value.object_write([&](Object& object) { + function(object); + }); + }; + } + const std::string& path() const noexcept { + return path_; + } + Json amis_schema() const { + Json result; + read_access_([&](const Object& value) { + result = to_amis_form_schema(value, path_ + "/data", describe().list_options().confirm_label); + }); + return result; + } + Http_Response descriptor_response() const noexcept { + return safe_response([&] { + return make_http_success(to_descriptor_json()); + }); + } + Http_Response data_response() const noexcept { + return safe_response([&] { + Http_Response response; + read_access_([&](const Object& value) { + response = make_http_success(to_frontend_json(value)); + }); + return response; + }); + } + Http_Response amis_response() const noexcept { + return safe_response([&] { + return make_http_success(amis_schema()); + }); + } + Http_Response update_response(std::string_view body, const Request_Context& context = {}) noexcept { + return safe_response([&] { + Json patch; + try { + patch = parse_json(body); + } catch(const std::exception& error) { + return make_http_error(400, error.what()); + } + Http_Response response; + write_access_([&](Object& value) { + response = update_locked(value, patch, context); + }); + return response; + }); + } +private: + using Read_Function = std::function; + using Write_Function = std::function; + using Read_Access = std::function; + using Write_Access = std::function; + void configure_raw_access(Object& value, Lock& lock) { + read_access_ = [&value, &lock](const Read_Function& function) { + std::unique_lock guard(lock); + function(value); + }; + write_access_ = [&value, &lock](const Write_Function& function) { + std::unique_lock guard(lock); + function(value); + }; + } + Http_Response update_locked(Object& value, const Json& patch, const Request_Context& context) { + Model original = Object_Adapter::snapshot(value); + Model candidate = original; + auto result = apply_model_json(candidate, patch, Write_Mode::update, false); + if(!result.success) { + return make_http_error(422, result); + } + bool transaction_started{}; + bool runtime_commit_started{}; + try { + if(transaction_.prepare) { + transaction_started = true; + transaction_.prepare(candidate, context); + } + runtime_commit_started = true; + Object_Adapter::commit(value, candidate); + if(transaction_.commit) { + transaction_started = true; + transaction_.commit(candidate, context); + } + } catch(const Json_Assignment_Error& error) { + rollback_after_failure_locked(value, original, context, runtime_commit_started, transaction_started); + return make_http_error(422, error.result()); + } catch(const Field_Validation_Error& error) { + rollback_after_failure_locked(value, original, context, runtime_commit_started, transaction_started); + return make_http_error(422, object_commit_error(error)); + } catch(const std::exception& error) { + const std::string rollback_error = rollback_after_failure_locked(value, original, context, runtime_commit_started, transaction_started); + return make_http_error(500, rollback_error.empty() ? error.what() : std::string(error.what()) + "; rollback failed: " + rollback_error); + } catch(...) { + const std::string rollback_error = rollback_after_failure_locked(value, original, context, runtime_commit_started, transaction_started); + return make_http_error(500, rollback_error.empty() ? "unknown server error" : "unknown server error; rollback failed: " + rollback_error); + } + return make_http_success(to_frontend_json(value), std::move(result.message)); + } + template + static Http_Response safe_response(Function&& function) noexcept { + try { + return std::forward(function)(); + } catch(const Json_Assignment_Error& error) { + return make_http_error(422, error.result()); + } catch(const Field_Validation_Error& error) { + return make_http_error(422, object_commit_error(error)); + } catch(const std::exception& error) { + return make_http_error(500, error.what()); + } catch(...) { + return make_http_error(500, "unknown server error"); + } + } + std::string rollback_after_failure_locked(Object& value, const Model& original, const Request_Context& context, bool restore_runtime, bool transaction_started) noexcept { + std::string message; + if(restore_runtime) { + try { + Object_Adapter::commit(value, original); + } catch(const std::exception& error) { + message = error.what(); + } catch(...) { + message = "runtime object rollback failed"; + } + } + if(transaction_started && transaction_.rollback) { + try { + transaction_.rollback(original, context); + } catch(const std::exception& error) { + if(!message.empty()) { + message += "; "; + } + message += error.what(); + } catch(...) { + if(!message.empty()) { + message += "; "; + } + message += "external rollback failed"; + } + } + return message; + } + std::string path_; + Transaction transaction_; + std::shared_ptr owned_lock_; + Read_Access read_access_; + Write_Access write_access_; +}; +} diff --git a/backend/include/adminive/json.hpp b/backend/library/include/adminive/json.hpp similarity index 89% rename from backend/include/adminive/json.hpp rename to backend/library/include/adminive/json.hpp index 6d330a6..9a8a73c 100644 --- a/backend/include/adminive/json.hpp +++ b/backend/library/include/adminive/json.hpp @@ -413,6 +413,14 @@ Json model_descriptor_json(const Model* defaults = nullptr) { json_set(field_json, "readable", item.is_readable()); json_set(field_json, "sensitive", item.is_sensitive()); json_set(field_json, "editable", item.is_editable()); + const char* lock_mode = "automatic"; + if(item.lock_mode() == Field_Lock_Mode::enabled) { + lock_mode = "enabled"; + } else if(item.lock_mode() == Field_Lock_Mode::disabled) { + lock_mode = "disabled"; + } + json_set(field_json, "lock_mode", lock_mode); + json_set(field_json, "synchronized", descriptor_field_requires_lock(item)); json_set(field_json, "creatable", item.is_creatable()); json_set(field_json, "visible", item.is_visible()); json_set(field_json, "required", item.is_required()); @@ -459,7 +467,7 @@ Json model_descriptor_json(const Model* defaults = nullptr) { json_set(list, "column_reorderable", options.column_reorderable_); Json result = json_object(); json_set(result, "protocol", "adminive.resource"); - json_set(result, "protocol_version", 2); + json_set(result, "protocol_version", 3); json_set(result, "name", descriptor.name()); json_set(result, "label", descriptor.label()); json_set(result, "value_type", "object"); @@ -522,36 +530,36 @@ requires Described_Type> || Object_Adapter_With_Snapshot< Json to_data_json(const T& value) { return to_frontend_json(value); } -template -Json to_json(const Synchronized_Value& value) { +template +Json to_json(const Synchronized_Value& value) { return value.read([](const T& item) { return adminive::to_json(item); }); } -template -Json to_frontend_json(const Synchronized_Value& value) { +template +Json to_frontend_json(const Synchronized_Value& value) { return value.read([](const T& item) { return adminive::to_frontend_json(item); }); } -template -Json to_data_json(const Synchronized_Value& value) { +template +Json to_data_json(const Synchronized_Value& value) { return to_frontend_json(value); } -template -Json to_json(const Synchronized_Member& value) { - return value.read([](const auto& item) { +template +Json to_json(const Synchronized_Field& value) { + return value.object_read([](const auto& item) { return adminive::to_json(item); }); } -template -Json to_frontend_json(const Synchronized_Member& value) { - return value.read([](const auto& item) { +template +Json to_frontend_json(const Synchronized_Field& value) { + return value.object_read([](const auto& item) { return adminive::to_frontend_json(item); }); } -template -Json to_data_json(const Synchronized_Member& value) { +template +Json to_data_json(const Synchronized_Field& value) { return to_frontend_json(value); } template @@ -725,70 +733,70 @@ template Update_Result apply_patch(T& target, const Json& patch) { return apply_frontend_patch(target, patch); } -template +template requires Writable_Adapted_Object -Update_Result assign_json(Synchronized_Value& target, const Json& value) { +Update_Result assign_json(Synchronized_Value& target, const Json& value) { return target.write([&](T& item) { return adminive::assign_json(item, value); }); } -template +template requires Writable_Adapted_Object -Update_Result apply_json_patch(Synchronized_Value& target, const Json& patch) { +Update_Result apply_json_patch(Synchronized_Value& target, const Json& patch) { return target.write([&](T& item) { return adminive::apply_json_patch(item, patch); }); } -template +template requires Creatable_Writable_Object -Update_Result apply_frontend_create(Synchronized_Value& target, const Json& value) { +Update_Result apply_frontend_create(Synchronized_Value& target, const Json& value) { return target.write([&](T& item) { return adminive::apply_frontend_create(item, value); }); } -template +template requires Writable_Adapted_Object -Update_Result apply_frontend_patch(Synchronized_Value& target, const Json& patch) { +Update_Result apply_frontend_patch(Synchronized_Value& target, const Json& patch) { return target.write([&](T& item) { return adminive::apply_frontend_patch(item, patch); }); } -template +template requires Writable_Adapted_Object -Update_Result apply_patch(Synchronized_Value& target, const Json& patch) { +Update_Result apply_patch(Synchronized_Value& target, const Json& patch) { return apply_frontend_patch(target, patch); } -template -requires (!std::is_const_v) && Writable_Adapted_Object::value_type> -Update_Result assign_json(Synchronized_Member target, const Json& value) { - return target.write([&](auto& item) { +template +requires (!std::is_const_v) && Writable_Adapted_Object::value_type> +Update_Result assign_json(Synchronized_Field target, const Json& value) { + return target.object_write([&](auto& item) { return adminive::assign_json(item, value); }); } -template -requires (!std::is_const_v) && Writable_Adapted_Object::value_type> -Update_Result apply_json_patch(Synchronized_Member target, const Json& patch) { - return target.write([&](auto& item) { +template +requires (!std::is_const_v) && Writable_Adapted_Object::value_type> +Update_Result apply_json_patch(Synchronized_Field target, const Json& patch) { + return target.object_write([&](auto& item) { return adminive::apply_json_patch(item, patch); }); } -template -requires (!std::is_const_v) && Creatable_Writable_Object::value_type> -Update_Result apply_frontend_create(Synchronized_Member target, const Json& value) { - return target.write([&](auto& item) { +template +requires (!std::is_const_v) && Creatable_Writable_Object::value_type> +Update_Result apply_frontend_create(Synchronized_Field target, const Json& value) { + return target.object_write([&](auto& item) { return adminive::apply_frontend_create(item, value); }); } -template -requires (!std::is_const_v) && Writable_Adapted_Object::value_type> -Update_Result apply_frontend_patch(Synchronized_Member target, const Json& patch) { - return target.write([&](auto& item) { +template +requires (!std::is_const_v) && Writable_Adapted_Object::value_type> +Update_Result apply_frontend_patch(Synchronized_Field target, const Json& patch) { + return target.object_write([&](auto& item) { return adminive::apply_frontend_patch(item, patch); }); } -template -requires (!std::is_const_v) && Writable_Adapted_Object::value_type> -Update_Result apply_patch(Synchronized_Member target, const Json& patch) { +template +requires (!std::is_const_v) && Writable_Adapted_Object::value_type> +Update_Result apply_patch(Synchronized_Field target, const Json& patch) { return apply_frontend_patch(target, patch); } template diff --git a/backend/include/adminive/status.hpp b/backend/library/include/adminive/status.hpp similarity index 100% rename from backend/include/adminive/status.hpp rename to backend/library/include/adminive/status.hpp diff --git a/backend/library/include/adminive/synchronized.hpp b/backend/library/include/adminive/synchronized.hpp new file mode 100644 index 0000000..c651c95 --- /dev/null +++ b/backend/library/include/adminive/synchronized.hpp @@ -0,0 +1,454 @@ +#pragma once +#include "adminive/descriptor.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +namespace adminive { +struct Empty_Lock { + void lock() noexcept {} + bool try_lock() noexcept { + return true; + } + void unlock() noexcept {} + void lock_shared() noexcept {} + bool try_lock_shared() noexcept { + return true; + } + void unlock_shared() noexcept {} +}; +template +concept Basic_Lock = std::default_initializable && requires(T& value) { + value.lock(); + value.unlock(); +}; +template +concept Shared_Basic_Lock = Basic_Lock && requires(T& value) { + value.lock_shared(); + value.unlock_shared(); +}; +template +struct Is_Reference_Wrapper : std::false_type {}; +template +struct Is_Reference_Wrapper> : std::true_type {}; +template +concept Synchronized_Result = std::is_void_v || (!std::is_reference_v && !std::is_pointer_v && !Is_Reference_Wrapper>::value && !std::ranges::view>); +template +using Read_Lock_Guard = std::conditional_t, std::shared_lock, std::unique_lock>; +template +using Descriptor_Type = decltype(Type_Descriptor>::get()); +template +using Descriptor_Fields = std::remove_cvref_t>().fields())>; +namespace detail { +template +struct Reflected_Field_Path_Key { + static constexpr std::size_t index = Index; +}; +template +struct Is_Reflected_Field_Path_Key : std::false_type {}; +template +struct Is_Reflected_Field_Path_Key> : std::true_type {}; +template +inline constexpr bool is_reflected_field_path_key_v = Is_Reflected_Field_Path_Key>::value; +} +template +consteval std::size_t described_field_index() { + using Object = std::remove_cvref_t; + using Fields = Descriptor_Fields; + if constexpr(Index == std::tuple_size_v) { + return Index; + } else { + using Field = std::tuple_element_t; + if constexpr(std::is_member_object_pointer_v) { + if constexpr(requires { Field::member; } && std::same_as, decltype(Key)>) { + if constexpr(Field::member == Key) { + return Index; + } + } + } else if constexpr(detail::is_reflected_field_path_key_v) { + if constexpr(requires { Field::index; } && std::same_as) { + if constexpr(Field::index == std::remove_cvref_t::index) { + return Index; + } + } + } + return described_field_index(); + } +} +template +consteval std::size_t described_member_index() { + return described_field_index(); +} +template +inline constexpr std::size_t described_field_count_v = std::tuple_size_v>>; +template +concept Described_Field_Key = Described_Type> && (described_field_index() < described_field_count_v); +template +concept Described_Member = std::is_member_object_pointer_v && Described_Field_Key; +template +concept Described_Reflected_Field = Described_Field_Key{}>; +template +bool described_type_requires_lock(); +template +bool descriptor_field_requires_lock(const Field& field) { + if(field.lock_mode() == Field_Lock_Mode::enabled) { + return true; + } + using Member = typename std::remove_cvref_t::member_type; + if(field.lock_mode() == Field_Lock_Mode::disabled) { + if constexpr(Described_Type) { + return described_type_requires_lock(); + } + return false; + } + if(field.is_editable()) { + return true; + } + if constexpr(Described_Type) { + return described_type_requires_lock(); + } + return false; +} +template +bool described_type_requires_lock() { + if constexpr(!Described_Type>) { + return false; + } else { + static const bool result = [] { + const auto descriptor = describe>(); + bool required{}; + std::apply([&](const auto&... field) { + required = (descriptor_field_requires_lock(field) || ...); + }, descriptor.fields()); + return required; + }(); + return result; + } +} +template +requires Described_Field_Key +bool described_field_requires_lock() { + static const bool result = [] { + const auto descriptor = describe>(); + constexpr std::size_t index = described_field_index(); + return descriptor_field_requires_lock(std::get(descriptor.fields())); + }(); + return result; +} +template +requires Described_Member +bool described_member_requires_lock() { + return described_field_requires_lock(); +} +template +decltype(auto) synchronized_field_value(Object& object) noexcept { + if constexpr(std::is_member_object_pointer_v) { + return object.*Key; + } else { + static_assert(detail::is_reflected_field_path_key_v); + using Owner = std::remove_cvref_t; + return Reflection_Adapter::template get::index>(object); + } +} +template +bool described_path_requires_lock() { + if constexpr(sizeof...(Rest) == 0) { + return described_field_requires_lock(); + } else { + using Next = std::remove_cvref_t(std::declval()))>; + return described_path_requires_lock(); + } +} +template +struct Field_Lock_Node; +template >> +class Field_Lock_Tree {}; +template +class Field_Lock_Tree { +private: + using Fields = Descriptor_Fields>; + template + static auto make_nodes(std::index_sequence) -> std::tuple, Lock>...>; +public: + using Nodes = decltype(make_nodes(std::make_index_sequence>{})); + template + auto& node() noexcept { + return std::get(nodes_); + } + template + const auto& node() const noexcept { + return std::get(nodes_); + } +private: + Nodes nodes_{}; +}; +template +struct Field_Lock_Node { + using value_type = typename std::remove_cvref_t::member_type; + mutable Lock lock{}; + Field_Lock_Tree children{}; +}; +namespace detail { +struct Synchronized_Access; +} +template +class Synchronized_Field; +template +class Synchronized_Value { +public: + using value_type = T; + using lock_type = Lock; + using field_lock_type = Field_Lock; + Synchronized_Value() requires std::default_initializable = default; + explicit Synchronized_Value(T value) : value_(std::move(value)) {} + Synchronized_Value(const Synchronized_Value&) = delete; + Synchronized_Value& operator=(const Synchronized_Value&) = delete; + template + requires Synchronized_Result> + decltype(auto) read(Function&& function) const { + std::unique_lock guard(lock_); + return std::invoke(std::forward(function), std::as_const(value_)); + } + template + requires Synchronized_Result> + decltype(auto) write(Function&& function) { + std::unique_lock guard(lock_); + return std::invoke(std::forward(function), value_); + } + T snapshot() const requires std::copy_constructible { + return read([](const T& value) { + return value; + }); + } + void replace(T value) requires std::assignable_from { + write([&](T& target) { + target = std::move(value); + }); + } + template + requires Described_Member + auto member() noexcept { + return Synchronized_Field(*this); + } + template + requires Described_Member + auto member() const noexcept { + return Synchronized_Field(*this); + } + template + requires Described_Reflected_Field + auto field() noexcept { + return Synchronized_Field{}>(*this); + } + template + requires Described_Reflected_Field + auto field() const noexcept { + return Synchronized_Field{}>(*this); + } +private: + friend struct detail::Synchronized_Access; + T& unsafe_value() noexcept { + return value_; + } + const T& unsafe_value() const noexcept { + return value_; + } + Lock& mutex() const noexcept { + return lock_; + } + template + friend class Synchronized_Field; + T value_{}; + mutable Lock lock_{}; + mutable Field_Lock_Tree field_locks_{}; +}; +template +decltype(auto) synchronized_path_value(Object& object) noexcept { + return synchronized_field_value(object); +} +template +decltype(auto) synchronized_path_value(Object& object) noexcept { + return synchronized_path_value(synchronized_field_value(object)); +} +template +struct Synchronized_Path_Owner { + using Next = std::remove_cvref_t(std::declval()))>; + using type = typename Synchronized_Path_Owner::type; +}; +template +struct Synchronized_Path_Owner { + using type = Current; +}; +template +class Synchronized_Field { + static_assert(sizeof...(Members) > 0); +public: + using root_type = std::remove_const_t; + using value_type = std::remove_cvref_t(std::declval()))>; + using lock_type = Object_Lock; + using field_lock_type = Field_Lock; + using owner_type = std::conditional_t, const Synchronized_Value, Synchronized_Value>; + explicit Synchronized_Field(Synchronized_Value& owner) requires (!std::is_const_v) : owner_(&owner) {} + explicit Synchronized_Field(const Synchronized_Value& owner) requires std::is_const_v : owner_(&owner) {} + template + requires Synchronized_Result> + decltype(auto) read(Function&& function) const { + return with_read_lock([&] { + return std::invoke(std::forward(function), std::as_const(unsafe_value())); + }); + } + template + requires (!std::is_const_v) && Synchronized_Result> + decltype(auto) write(Function&& function) { + return with_write_lock([&] { + return std::invoke(std::forward(function), unsafe_value()); + }); + } + template + requires Synchronized_Result> + decltype(auto) object_read(Function&& function) const { + return read(std::forward(function)); + } + template + requires (!std::is_const_v) && Synchronized_Result> + decltype(auto) object_write(Function&& function) { + return write(std::forward(function)); + } + value_type snapshot() const requires std::copy_constructible { + return object_read([](const value_type& value) { + return value; + }); + } + void replace(value_type value) requires (!std::is_const_v) && std::assignable_from { + object_write([&](value_type& target) { + target = std::move(value); + }); + } + template + requires Described_Member + auto member() noexcept requires (!std::is_const_v) { + return Synchronized_Field(*owner_); + } + template + requires Described_Member + auto member() const noexcept { + return Synchronized_Field(*owner_); + } + template + requires Described_Reflected_Field + auto field() noexcept requires (!std::is_const_v) { + return Synchronized_Field{}>(*owner_); + } + template + requires Described_Reflected_Field + auto field() const noexcept { + return Synchronized_Field{}>(*owner_); + } +private: + friend struct detail::Synchronized_Access; + decltype(auto) unsafe_value() const noexcept { + if constexpr(std::is_const_v) { + return std::as_const(synchronized_path_value(owner_->unsafe_value())); + } else { + return synchronized_path_value(owner_->unsafe_value()); + } + } + Object_Lock& mutex() const noexcept { + return owner_->mutex(); + } + template + decltype(auto) with_read_lock(Function&& function) const { + Read_Lock_Guard root_guard(owner_->mutex()); + if(!described_path_requires_lock()) { + return std::forward(function)(); + } + return with_path_read_lock<0, Members...>(owner_->field_locks_, std::forward(function)); + } + template + decltype(auto) with_write_lock(Function&& function) const { + if(!described_path_requires_lock()) { + std::unique_lock root_guard(owner_->mutex()); + return std::forward(function)(); + } + Read_Lock_Guard root_guard(owner_->mutex()); + return with_path_write_lock<0, Members...>(owner_->field_locks_, std::forward(function)); + } + template + static decltype(auto) with_path_read_lock(Tree& tree, Function&& function) { + using Object = typename Synchronized_Path_Owner::type; + constexpr std::size_t index = described_field_index(); + auto& node = tree.template node(); + const bool lock_required = described_field_requires_lock(); + if constexpr(sizeof...(Rest) == 0) { + if(!lock_required) { + return std::forward(function)(); + } + if constexpr(Described_Type::value_type>) { + if(described_type_requires_lock::value_type>()) { + std::unique_lock guard(node.lock); + return std::forward(function)(); + } + } + Read_Lock_Guard guard(node.lock); + return std::forward(function)(); + } else { + if(lock_required) { + Read_Lock_Guard guard(node.lock); + return with_path_read_lock(node.children, std::forward(function)); + } + return with_path_read_lock(node.children, std::forward(function)); + } + } + template + static decltype(auto) with_path_write_lock(Tree& tree, Function&& function) { + using Object = typename Synchronized_Path_Owner::type; + constexpr std::size_t index = described_field_index(); + auto& node = tree.template node(); + const bool lock_required = described_field_requires_lock(); + if constexpr(sizeof...(Rest) == 0) { + if(lock_required) { + std::unique_lock guard(node.lock); + return std::forward(function)(); + } + return std::forward(function)(); + } else { + if(lock_required) { + Read_Lock_Guard guard(node.lock); + return with_path_write_lock(node.children, std::forward(function)); + } + return with_path_write_lock(node.children, std::forward(function)); + } + } + owner_type* owner_; +}; +namespace detail { +struct Synchronized_Access { + template + static T& value(Synchronized_Value& synchronized) noexcept { + return synchronized.unsafe_value(); + } + template + static const T& value(const Synchronized_Value& synchronized) noexcept { + return synchronized.unsafe_value(); + } + template + static Lock& mutex(const Synchronized_Value& synchronized) noexcept { + return synchronized.mutex(); + } + template + static decltype(auto) value(const Synchronized_Field& synchronized) noexcept { + return synchronized.unsafe_value(); + } + template + static Lock& mutex(const Synchronized_Field& synchronized) noexcept { + return synchronized.mutex(); + } +}; +} +template +using Synchronized_Member = Synchronized_Field; +} diff --git a/backend/include/adminive/value.hpp b/backend/library/include/adminive/value.hpp similarity index 100% rename from backend/include/adminive/value.hpp rename to backend/library/include/adminive/value.hpp diff --git a/backend/tests/core_adapter_test.cpp b/backend/library/tests/core_adapter_test.cpp similarity index 96% rename from backend/tests/core_adapter_test.cpp rename to backend/library/tests/core_adapter_test.cpp index 6f65e8f..92a1b84 100644 --- a/backend/tests/core_adapter_test.cpp +++ b/backend/library/tests/core_adapter_test.cpp @@ -285,5 +285,10 @@ int main() { const Json encoded = adminive::to_json(config); assert((adminive::json_get(adminive::Json_Adapter::at(encoded, "worker_count")) == 12)); assert((adminive::json_get(adminive::Json_Adapter::at(encoded, "mode")) == "secondary")); + adminive::Collection_Service collection("/configs", {Config{}}); + const auto list = collection.list_response(); + assert(list.status == 200); + const auto& data = adminive::Json_Adapter::at(list.body, "data"); + assert((adminive::json_get(adminive::Json_Adapter::at(data, "total")) == 1)); return 0; } diff --git a/backend/main.cmake b/backend/main.cmake deleted file mode 100644 index e69de29..0000000 diff --git a/backend/service/CMakeLists.txt b/backend/service/CMakeLists.txt new file mode 100644 index 0000000..f193942 --- /dev/null +++ b/backend/service/CMakeLists.txt @@ -0,0 +1,87 @@ +set(adminive_service_include_dir "${CMAKE_CURRENT_LIST_DIR}/include") +set(adminive_third_party_include_dir "${CMAKE_CURRENT_LIST_DIR}/third_party/include") +set(adminive_frontend_dist_dir "${CMAKE_CURRENT_LIST_DIR}/../../frontend_dist") +file(TO_CMAKE_PATH "${adminive_frontend_dist_dir}" adminive_frontend_dist_dir) +add_library(Adminive_Service INTERFACE) +set_target_properties(Adminive_Service PROPERTIES EXPORT_NAME Service) +add_library(Adminive::Service ALIAS Adminive_Service) +target_include_directories(Adminive_Service INTERFACE "$" "$" "$") +target_link_libraries(Adminive_Service INTERFACE Adminive::Core) +add_library(Adminive_Nlohmann INTERFACE) +set_target_properties(Adminive_Nlohmann PROPERTIES EXPORT_NAME Nlohmann) +add_library(Adminive::Nlohmann ALIAS Adminive_Nlohmann) +target_link_libraries(Adminive_Nlohmann INTERFACE Adminive::Service) +add_library(Adminive_MagicEnum INTERFACE) +set_target_properties(Adminive_MagicEnum PROPERTIES EXPORT_NAME MagicEnum) +add_library(Adminive::MagicEnum ALIAS Adminive_MagicEnum) +target_link_libraries(Adminive_MagicEnum INTERFACE Adminive::Service) +add_library(Adminive_BoostPfr INTERFACE) +set_target_properties(Adminive_BoostPfr PROPERTIES EXPORT_NAME BoostPfr) +add_library(Adminive::BoostPfr ALIAS Adminive_BoostPfr) +target_link_libraries(Adminive_BoostPfr INTERFACE Adminive::Service "$") +add_library(Adminive_Httplib INTERFACE) +set_target_properties(Adminive_Httplib PROPERTIES EXPORT_NAME Httplib) +add_library(Adminive::Httplib ALIAS Adminive_Httplib) +target_link_libraries(Adminive_Httplib INTERFACE Adminive::Service) +if(WIN32) + target_compile_definitions(Adminive_Httplib INTERFACE WINVER=0x0A00 _WIN32_WINNT=0x0A00) + target_link_libraries(Adminive_Httplib INTERFACE ws2_32) +endif() +add_library(Adminive_Drogon INTERFACE) +set_target_properties(Adminive_Drogon PROPERTIES EXPORT_NAME Drogon) +add_library(Adminive::Drogon ALIAS Adminive_Drogon) +target_link_libraries(Adminive_Drogon INTERFACE Adminive::Service "$") +if(TARGET Drogon::Drogon) + target_link_libraries(Adminive_Drogon INTERFACE "$") +elseif(TARGET drogon) + target_link_libraries(Adminive_Drogon INTERFACE "$") +endif() +add_library(Adminive_Default INTERFACE) +set_target_properties(Adminive_Default PROPERTIES EXPORT_NAME Default) +add_library(Adminive::Default ALIAS Adminive_Default) +target_link_libraries(Adminive_Default INTERFACE Adminive::Httplib Adminive::Nlohmann Adminive::MagicEnum) +add_library(Adminive_Example STATIC "${CMAKE_CURRENT_LIST_DIR}/src/example.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/config_store.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/device_tables.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/server_app.cpp") +target_include_directories(Adminive_Example PUBLIC "${CMAKE_CURRENT_LIST_DIR}/src") +target_link_libraries(Adminive_Example PUBLIC Adminive::Default) +add_executable(Adminive_Server "${CMAKE_CURRENT_LIST_DIR}/src/main.cpp") +target_compile_definitions(Adminive_Server PRIVATE ADMINIVE_FRONTEND_DIST_DIR="${adminive_frontend_dist_dir}") +target_link_libraries(Adminive_Server PRIVATE Adminive_Example) +if(MSVC) + target_compile_options(Adminive_Example PRIVATE /permissive-) + target_compile_options(Adminive_Server PRIVATE /permissive-) +endif() +if(BUILD_TESTING) + add_executable(Adminive_Test "${CMAKE_CURRENT_LIST_DIR}/tests/test.cpp") + target_include_directories(Adminive_Test PRIVATE "${CMAKE_CURRENT_LIST_DIR}/src") + target_link_libraries(Adminive_Test PRIVATE Adminive_Example) + add_test(NAME Adminive_Test COMMAND Adminive_Test) + add_executable(Adminive_Advanced_Adapter_Test "${CMAKE_CURRENT_LIST_DIR}/tests/advanced_adapter_test.cpp") + target_link_libraries(Adminive_Advanced_Adapter_Test PRIVATE Adminive::Default Adminive::BoostPfr) + add_test(NAME Adminive_Advanced_Adapter_Test COMMAND Adminive_Advanced_Adapter_Test) + add_executable(Adminive_Safety_Test "${CMAKE_CURRENT_LIST_DIR}/tests/safety_test.cpp") + target_link_libraries(Adminive_Safety_Test PRIVATE Adminive::Default) + add_test(NAME Adminive_Safety_Test COMMAND Adminive_Safety_Test) + add_executable(Adminive_Config_Store_Test "${CMAKE_CURRENT_LIST_DIR}/tests/config_store_test.cpp") + target_include_directories(Adminive_Config_Store_Test PRIVATE "${CMAKE_CURRENT_LIST_DIR}/src") + target_link_libraries(Adminive_Config_Store_Test PRIVATE Adminive_Example) + add_test(NAME Adminive_Config_Store_Test COMMAND Adminive_Config_Store_Test) + add_executable(Adminive_Synchronized_Test "${CMAKE_CURRENT_LIST_DIR}/tests/synchronized_test.cpp") + target_link_libraries(Adminive_Synchronized_Test PRIVATE Adminive::Default) + add_test(NAME Adminive_Synchronized_Test COMMAND Adminive_Synchronized_Test) + add_executable(Adminive_Drogon_Adapter_Test "${CMAKE_CURRENT_LIST_DIR}/tests/drogon_adapter_test.cpp") + target_include_directories(Adminive_Drogon_Adapter_Test BEFORE PRIVATE "${CMAKE_CURRENT_LIST_DIR}/tests/fake_drogon") + target_link_libraries(Adminive_Drogon_Adapter_Test PRIVATE Adminive::Default) + add_test(NAME Adminive_Drogon_Adapter_Test COMMAND Adminive_Drogon_Adapter_Test) + if(MSVC) + target_compile_options(Adminive_Test PRIVATE /permissive-) + target_compile_options(Adminive_Advanced_Adapter_Test PRIVATE /permissive-) + target_compile_options(Adminive_Drogon_Adapter_Test PRIVATE /permissive-) + target_compile_options(Adminive_Safety_Test PRIVATE /permissive-) + target_compile_options(Adminive_Config_Store_Test PRIVATE /permissive-) + target_compile_options(Adminive_Synchronized_Test PRIVATE /permissive-) + endif() +endif() +install(TARGETS Adminive_Service Adminive_Nlohmann Adminive_MagicEnum Adminive_BoostPfr Adminive_Httplib Adminive_Default EXPORT AdminiveServiceTargets) +install(TARGETS Adminive_Drogon EXPORT AdminiveDrogonTargets) +install(DIRECTORY "${adminive_service_include_dir}/" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") +install(DIRECTORY "${adminive_third_party_include_dir}/" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") diff --git a/backend/include/adminive/adapters/boost_pfr.hpp b/backend/service/include/adminive/adapters/boost_pfr.hpp similarity index 100% rename from backend/include/adminive/adapters/boost_pfr.hpp rename to backend/service/include/adminive/adapters/boost_pfr.hpp diff --git a/backend/include/adminive/adapters/drogon.hpp b/backend/service/include/adminive/adapters/drogon.hpp similarity index 52% rename from backend/include/adminive/adapters/drogon.hpp rename to backend/service/include/adminive/adapters/drogon.hpp index 7c5aa45..7361fb6 100644 --- a/backend/include/adminive/adapters/drogon.hpp +++ b/backend/service/include/adminive/adapters/drogon.hpp @@ -1,9 +1,12 @@ #pragma once +#include "adminive/collection.hpp" #include "adminive/http.hpp" #include +#include #include #include #include +#include #include #include #include @@ -35,6 +38,15 @@ inline std::vector make_drogon_constraints(dro } return result; } +inline std::size_t read_drogon_size_parameter(const drogon::HttpRequestPtr& request, const char* name, std::size_t fallback) { + const std::string& text = request->getParameter(name); + if(text.empty()) { + return fallback; + } + std::size_t value{}; + const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); + return error == std::errc{} && end == text.data() + text.size() && value != 0 ? value : fallback; +} template void complete_drogon_request(std::function& callback, Function&& function) noexcept { try { @@ -63,17 +75,18 @@ void schedule_drogon_request(const Drogon_Executor& executor, std::function(500, "failed to schedule request"))); } } -template +template requires Writable_Adapted_Object && std::copy_constructible> class Drogon_Resource { public: using Service = Resource_Service; using Transaction = typename Service::Transaction; Drogon_Resource(T& value, std::string path, Transaction transaction = {}, Lock* shared_lock = nullptr) : service_(std::make_shared(value, std::move(path), std::move(transaction), shared_lock)) {} - explicit Drogon_Resource(Synchronized_Value, Lock>& value, std::string path, Transaction transaction = {}) : service_(std::make_shared(value, std::move(path), std::move(transaction))) {} - template - requires (!std::is_const_v) && std::same_as::value_type, std::remove_cvref_t> - explicit Drogon_Resource(Synchronized_Member value, std::string path, Transaction transaction = {}) : service_(std::make_shared(value, std::move(path), std::move(transaction))) {} + template + explicit Drogon_Resource(Synchronized_Value, Lock, Field_Lock>& value, std::string path, Transaction transaction = {}) : service_(std::make_shared(value, std::move(path), std::move(transaction))) {} + template + requires (!std::is_const_v) && std::same_as::value_type, std::remove_cvref_t> + explicit Drogon_Resource(Synchronized_Field value, std::string path, Transaction transaction = {}, Resource_Lock_Scope lock_scope = Resource_Lock_Scope::local) : service_(std::make_shared(value, std::move(path), std::move(transaction), lock_scope)) {} Json amis_schema() const { return service_->amis_schema(); } @@ -112,6 +125,102 @@ public: private: std::shared_ptr service_; }; +template +requires std::default_initializable && std::copy_constructible && std::assignable_from +class Drogon_Collection_Resource { +public: + using Service = Collection_Service; + explicit Drogon_Collection_Resource(std::string path, std::vector initial = {}) : service_(std::make_shared(std::move(path), std::move(initial))) {} + template + void register_overview_status(std::string api, std::uint64_t interval = 2000) { + service_->template register_overview_status(std::move(api), interval); + } + template + void register_status(std::uint64_t interval = 2000) { + service_->template register_status(interval); + } + std::size_t size() const { + return service_->size(); + } + Json items_json() const { + return service_->items_json(); + } + Json amis_schema() const { + return service_->amis_schema(); + } + void bind(drogon::HttpAppFramework& app, Drogon_Bind_Options options = {}) const { + const auto service = service_; + const auto get_constraints = make_drogon_constraints(drogon::Get, options.filters); + const auto post_constraints = make_drogon_constraints(drogon::Post, options.filters); + const auto put_constraints = make_drogon_constraints(drogon::Put, options.filters); + const auto patch_constraints = make_drogon_constraints(drogon::Patch, options.filters); + const auto delete_constraints = make_drogon_constraints(drogon::Delete, options.filters); + app.registerHandler(service->path() + "/descriptor", [service](const drogon::HttpRequestPtr&, std::function&& callback) { + complete_drogon_request(callback, [service] { + return service->descriptor_response(); + }); + }, get_constraints); + app.registerHandler(service->path() + "/amis", [service](const drogon::HttpRequestPtr&, std::function&& callback) { + complete_drogon_request(callback, [service] { + return service->amis_response(); + }); + }, get_constraints); + if(service->has_status()) { + app.registerHandler(service->path() + "/status/descriptor", [service](const drogon::HttpRequestPtr&, std::function&& callback) { + complete_drogon_request(callback, [service] { + return service->status_descriptor_response(); + }); + }, get_constraints); + app.registerHandler(service->path() + "/{1:id}/status", [service, options](const drogon::HttpRequestPtr&, std::function&& callback, std::uint64_t id) { + schedule_drogon_request(options.executor, std::move(callback), [service, id] { + return service->status_response(id); + }); + }, get_constraints); + } + app.registerHandler(service->path(), [service](const drogon::HttpRequestPtr& request, std::function&& callback) { + Collection_Query query; + query.page = read_drogon_size_parameter(request, "page", 1); + query.per_page = read_drogon_size_parameter(request, "perPage", 20); + query.order_by = request->getParameter("orderBy"); + query.order_dir = request->getParameter("orderDir"); + complete_drogon_request(callback, [service, query = std::move(query)]() mutable { + return service->list_response(std::move(query)); + }); + }, get_constraints); + app.registerHandler(service->path() + "/{1:id}", [service](const drogon::HttpRequestPtr&, std::function&& callback, std::uint64_t id) { + complete_drogon_request(callback, [service, id] { + return service->item_response(id); + }); + }, get_constraints); + app.registerHandler(service->path(), [service, options](const drogon::HttpRequestPtr& request, std::function&& callback) { + schedule_drogon_request(options.executor, std::move(callback), [service, body = std::string(request->getBody())] { + return service->create_response(body); + }); + }, post_constraints); + if(service->user_reorderable()) { + app.registerHandler(service->path() + "/order", [service, options](const drogon::HttpRequestPtr& request, std::function&& callback) { + schedule_drogon_request(options.executor, std::move(callback), [service, body = std::string(request->getBody())] { + return service->reorder_response(body); + }); + }, post_constraints); + } + const auto bind_update = [service, options](const drogon::HttpRequestPtr& request, std::function&& callback, std::uint64_t id) { + schedule_drogon_request(options.executor, std::move(callback), [service, id, body = std::string(request->getBody())] { + return service->update_response(id, body); + }); + }; + app.registerHandler(service->path() + "/{1:id}", bind_update, put_constraints); + app.registerHandler(service->path() + "/{1:id}", bind_update, patch_constraints); + app.registerHandler(service->path() + "/{1:id}", [service, options](const drogon::HttpRequestPtr&, std::function&& callback, std::uint64_t id) { + schedule_drogon_request(options.executor, std::move(callback), [service, id] { + return service->delete_response(id); + }); + }, delete_constraints); + } +private: + std::shared_ptr service_; +}; + template class Drogon_Status_Resource { public: diff --git a/backend/service/include/adminive/adapters/httplib.hpp b/backend/service/include/adminive/adapters/httplib.hpp new file mode 100644 index 0000000..6014491 --- /dev/null +++ b/backend/service/include/adminive/adapters/httplib.hpp @@ -0,0 +1,165 @@ +#pragma once +#include "adminive/collection.hpp" +#include "adminive/http.hpp" +#include "httplib.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace adminive { +template +void write_http_json(httplib::Response& response, const Json& value) { + response.set_content(dump_json(value, 2), "application/json; charset=utf-8"); +} +template +void write_http_response(httplib::Response& response, Http_Response value) { + response.status = value.status; + write_http_json(response, value.body); +} +template +void write_http_error(httplib::Response& response, int status, const Update_Result& update) { + write_http_response(response, make_http_error(status, update)); +} +template +requires Writable_Adapted_Object && std::copy_constructible> +class Http_Resource { +public: + using Service = Resource_Service; + using Transaction = typename Service::Transaction; + Http_Resource(T& value, std::string path, Transaction transaction = {}, Lock* shared_lock = nullptr) : service_(std::make_shared(value, std::move(path), std::move(transaction), shared_lock)) {} + template + explicit Http_Resource(Synchronized_Value, Lock, Field_Lock>& value, std::string path, Transaction transaction = {}) : service_(std::make_shared(value, std::move(path), std::move(transaction))) {} + template + requires (!std::is_const_v) && std::same_as::value_type, std::remove_cvref_t> + explicit Http_Resource(Synchronized_Field value, std::string path, Transaction transaction = {}, Resource_Lock_Scope lock_scope = Resource_Lock_Scope::local) : service_(std::make_shared(value, std::move(path), std::move(transaction), lock_scope)) {} + Json amis_schema() const { + return service_->amis_schema(); + } + void bind(httplib::Server& server) const { + const auto service = service_; + server.Get(service->path() + "/descriptor", [service](const httplib::Request&, httplib::Response& response) { + write_http_response(response, service->descriptor_response()); + }); + server.Get(service->path() + "/data", [service](const httplib::Request&, httplib::Response& response) { + write_http_response(response, service->data_response()); + }); + server.Get(service->path() + "/amis", [service](const httplib::Request&, httplib::Response& response) { + write_http_response(response, service->amis_response()); + }); + server.Post(service->path() + "/data", [service](const httplib::Request& request, httplib::Response& response) { + write_http_response(response, service->update_response(request.body)); + }); + } +private: + std::shared_ptr service_; +}; +template +requires std::default_initializable && std::copy_constructible && std::assignable_from +class Http_Collection_Resource { +public: + using Service = Collection_Service; + explicit Http_Collection_Resource(std::string path, std::vector initial = {}) : service_(std::make_shared(std::move(path), std::move(initial))) {} + template + void register_overview_status(std::string api, std::uint64_t interval = 2000) { + service_->template register_overview_status(std::move(api), interval); + } + template + void register_status(std::uint64_t interval = 2000) { + service_->template register_status(interval); + } + std::size_t size() const { + return service_->size(); + } + Json items_json() const { + return service_->items_json(); + } + Json amis_schema() const { + return service_->amis_schema(); + } + void bind(httplib::Server& server) const { + const auto service = service_; + server.Get(service->path() + "/descriptor", [service](const httplib::Request&, httplib::Response& response) { + write_http_response(response, service->descriptor_response()); + }); + server.Get(service->path() + "/amis", [service](const httplib::Request&, httplib::Response& response) { + write_http_response(response, service->amis_response()); + }); + if(service->has_status()) { + server.Get(service->path() + "/status/descriptor", [service](const httplib::Request&, httplib::Response& response) { + write_http_response(response, service->status_descriptor_response()); + }); + server.Get(status_pattern(service->path()), [service](const httplib::Request& request, httplib::Response& response) { + write_http_response(response, service->status_response(read_route_id(request))); + }); + } + server.Get(service->path(), [service](const httplib::Request& request, httplib::Response& response) { + Collection_Query query; + query.page = read_size_parameter(request, "page", 1); + query.per_page = read_size_parameter(request, "perPage", 20); + if(request.has_param("orderBy")) { + query.order_by = request.get_param_value("orderBy"); + query.order_dir = request.has_param("orderDir") ? request.get_param_value("orderDir") : "asc"; + } + write_http_response(response, service->list_response(std::move(query))); + }); + server.Get(item_pattern(service->path()), [service](const httplib::Request& request, httplib::Response& response) { + write_http_response(response, service->item_response(read_route_id(request))); + }); + server.Post(service->path(), [service](const httplib::Request& request, httplib::Response& response) { + write_http_response(response, service->create_response(request.body)); + }); + if(service->user_reorderable()) { + server.Post(service->path() + "/order", [service](const httplib::Request& request, httplib::Response& response) { + write_http_response(response, service->reorder_response(request.body)); + }); + } + server.Put(item_pattern(service->path()), [service](const httplib::Request& request, httplib::Response& response) { + write_http_response(response, service->update_response(read_route_id(request), request.body)); + }); + server.Patch(item_pattern(service->path()), [service](const httplib::Request& request, httplib::Response& response) { + write_http_response(response, service->update_response(read_route_id(request), request.body)); + }); + server.Delete(item_pattern(service->path()), [service](const httplib::Request& request, httplib::Response& response) { + write_http_response(response, service->delete_response(read_route_id(request))); + }); + } +private: + static std::size_t read_size_parameter(const httplib::Request& request, const char* name, std::size_t fallback) { + if(!request.has_param(name)) { + return fallback; + } + const auto text = request.get_param_value(name); + std::size_t value{}; + const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); + if(error != std::errc{} || end != text.data() + text.size() || value == 0) { + return fallback; + } + return value; + } + static std::uint64_t read_route_id(const httplib::Request& request) { + const std::string text = request.matches[1].str(); + std::uint64_t id{}; + const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), id); + if(error != std::errc{} || end != text.data() + text.size()) { + return 0; + } + return id; + } + static std::string item_pattern(const std::string& path) { + return path + R"(/(\d+))"; + } + static std::string status_pattern(const std::string& path) { + return path + R"(/(\d+)/status)"; + } + std::shared_ptr service_; +}; +} diff --git a/backend/include/adminive/adapters/magic_enum.hpp b/backend/service/include/adminive/adapters/magic_enum.hpp similarity index 100% rename from backend/include/adminive/adapters/magic_enum.hpp rename to backend/service/include/adminive/adapters/magic_enum.hpp diff --git a/backend/include/adminive/adapters/nlohmann_json.hpp b/backend/service/include/adminive/adapters/nlohmann_json.hpp similarity index 100% rename from backend/include/adminive/adapters/nlohmann_json.hpp rename to backend/service/include/adminive/adapters/nlohmann_json.hpp diff --git a/backend/src/config_store.cpp b/backend/service/src/config_store.cpp similarity index 91% rename from backend/src/config_store.cpp rename to backend/service/src/config_store.cpp index cbf4341..a1ab19d 100644 --- a/backend/src/config_store.cpp +++ b/backend/service/src/config_store.cpp @@ -51,8 +51,8 @@ Application_Config Config_Store::snapshot() const { return data_.snapshot(); } Device_Table_Type Config_Store::default_device_type() const { - return data_.read([](const Application_Config& value) { - return value.default_device_type; + return data_.member<&Application_Config::default_device_type>().read([](const Device_Table_Type& value) { + return value; }); } const std::filesystem::path& Config_Store::path() const noexcept { @@ -120,7 +120,7 @@ void Config_Store::persist_default_device_type(Device_Table_Type value) { } void Config_Store::load_or_create() { if(!std::filesystem::exists(path_)) { - persist_candidate(data_.unsafe_value()); + persist_candidate(detail::Synchronized_Access::value(data_)); return; } std::ifstream input(path_); @@ -133,7 +133,7 @@ void Config_Store::load_or_create() { if(!result.success) { throw std::runtime_error("invalid configuration file: " + result.message); } - data_.unsafe_value() = std::move(candidate); + detail::Synchronized_Access::value(data_) = std::move(candidate); } void Config_Store::persist_candidate(const Application_Config& value) { const auto parent = path_.parent_path(); @@ -158,22 +158,22 @@ void Config_Store::persist_candidate(const Application_Config& value) { replace_file(temporary, path_); } void Config_Store::persist_radio_service_locked(const Radio_Service_Config& value) { - Application_Config candidate = data_.unsafe_value(); + Application_Config candidate = detail::Synchronized_Access::value(data_); candidate.radio_service = value; persist_candidate(candidate); } void Config_Store::persist_alerts_locked(const Alert_Config& value) { - Application_Config candidate = data_.unsafe_value(); + Application_Config candidate = detail::Synchronized_Access::value(data_); candidate.alerts = value; persist_candidate(candidate); } void Config_Store::persist_serial_table_locked(const Serial_Table_Config& value) { - Application_Config candidate = data_.unsafe_value(); + Application_Config candidate = detail::Synchronized_Access::value(data_); candidate.serial_table = value; persist_candidate(candidate); } void Config_Store::persist_network_table_locked(const Network_Table_Config& value) { - Application_Config candidate = data_.unsafe_value(); + Application_Config candidate = detail::Synchronized_Access::value(data_); candidate.network_table = value; persist_candidate(candidate); } diff --git a/backend/src/config_store.hpp b/backend/service/src/config_store.hpp similarity index 77% rename from backend/src/config_store.hpp rename to backend/service/src/config_store.hpp index f40500b..5c08ac8 100644 --- a/backend/src/config_store.hpp +++ b/backend/service/src/config_store.hpp @@ -2,14 +2,15 @@ #include "adminive/http.hpp" #include "example_descriptors.hpp" #include +#include namespace adminive::example { class Config_Store { public: using Storage = Synchronized_Value; - using Radio_Service_Access = Synchronized_Member; - using Alert_Access = Synchronized_Member; - using Serial_Table_Access = Synchronized_Member; - using Network_Table_Access = Synchronized_Member; + using Radio_Service_Access = decltype(std::declval().member<&Application_Config::radio_service>()); + using Alert_Access = decltype(std::declval().member<&Application_Config::alerts>()); + using Serial_Table_Access = decltype(std::declval().member<&Application_Config::serial_table>()); + using Network_Table_Access = decltype(std::declval().member<&Application_Config::network_table>()); explicit Config_Store(std::filesystem::path path); Radio_Service_Access radio_service() noexcept; Alert_Access alerts() noexcept; diff --git a/backend/src/device_tables.cpp b/backend/service/src/device_tables.cpp similarity index 96% rename from backend/src/device_tables.cpp rename to backend/service/src/device_tables.cpp index 03196f6..4952754 100644 --- a/backend/src/device_tables.cpp +++ b/backend/service/src/device_tables.cpp @@ -22,7 +22,7 @@ Json make_table_page(std::string_view title, std::string_view color, Json config return Json{{"type", "container"}, {"body", Json::array({std::move(heading), std::move(config_form), std::move(table_page.at("body"))})}}; } } -Serial_Device_Table::Serial_Device_Table(Config_Store& store) : store_(store), config_resource_(store.serial_table(), "/admin/device_tables/serial/config", store.serial_table_transaction()), rows_("/admin/device_tables/serial/items", make_serial_rows()) {} +Serial_Device_Table::Serial_Device_Table(Config_Store& store) : store_(store), config_resource_(store.serial_table(), "/admin/device_tables/serial/config", store.serial_table_transaction(), Resource_Lock_Scope::root), rows_("/admin/device_tables/serial/items", make_serial_rows()) {} Device_Table_Type Serial_Device_Table::type() const noexcept { return Device_Table_Type::serial; } @@ -40,7 +40,7 @@ void Serial_Device_Table::bind(httplib::Server& server) { config_resource_.bind(server); rows_.bind(server); } -Network_Device_Table::Network_Device_Table(Config_Store& store) : store_(store), config_resource_(store.network_table(), "/admin/device_tables/network/config", store.network_table_transaction()), rows_("/admin/device_tables/network/items", make_network_rows()) {} +Network_Device_Table::Network_Device_Table(Config_Store& store) : store_(store), config_resource_(store.network_table(), "/admin/device_tables/network/config", store.network_table_transaction(), Resource_Lock_Scope::root), rows_("/admin/device_tables/network/items", make_network_rows()) {} Device_Table_Type Network_Device_Table::type() const noexcept { return Device_Table_Type::network; } diff --git a/backend/src/device_tables.hpp b/backend/service/src/device_tables.hpp similarity index 100% rename from backend/src/device_tables.hpp rename to backend/service/src/device_tables.hpp diff --git a/backend/src/example.cpp b/backend/service/src/example.cpp similarity index 100% rename from backend/src/example.cpp rename to backend/service/src/example.cpp diff --git a/backend/src/example.hpp b/backend/service/src/example.hpp similarity index 100% rename from backend/src/example.hpp rename to backend/service/src/example.hpp diff --git a/backend/src/example_descriptors.hpp b/backend/service/src/example_descriptors.hpp similarity index 99% rename from backend/src/example_descriptors.hpp rename to backend/service/src/example_descriptors.hpp index c4782b7..d445de7 100644 --- a/backend/src/example_descriptors.hpp +++ b/backend/service/src/example_descriptors.hpp @@ -105,7 +105,7 @@ struct Type_Descriptor { using T = example::Application_Config; return object( "application_config", - ADMINIVE_FIELD_LABEL(T, default_device_type, "默认设备类型"), + ADMINIVE_FIELD_LABEL(T, default_device_type, "默认设备类型").locked(), ADMINIVE_FIELD_LABEL(T, radio_service, "无线电服务"), ADMINIVE_FIELD_LABEL(T, alerts, "告警"), ADMINIVE_FIELD_LABEL(T, serial_table, "串口表格"), diff --git a/backend/src/main.cpp b/backend/service/src/main.cpp similarity index 100% rename from backend/src/main.cpp rename to backend/service/src/main.cpp diff --git a/backend/src/server_app.cpp b/backend/service/src/server_app.cpp similarity index 93% rename from backend/src/server_app.cpp rename to backend/service/src/server_app.cpp index f376bb3..a08f07f 100644 --- a/backend/src/server_app.cpp +++ b/backend/service/src/server_app.cpp @@ -12,7 +12,7 @@ Radio_State make_radio_state(Radio_Mode mode, int port, int buffer_count, int lo return result; } } -Server_App::Server_App(int port, std::filesystem::path frontend_dist, std::filesystem::path config_file) : port_(port), frontend_dist_(std::move(frontend_dist)), config_store_(std::move(config_file)), state_resource_("/admin/radio_states", make_radio_states()), service_config_resource_(config_store_.radio_service(), "/admin/config/radio", config_store_.radio_service_transaction()), alert_config_resource_(config_store_.alerts(), "/admin/config/alerts", config_store_.alerts_transaction()), device_tables_(config_store_) { +Server_App::Server_App(int port, std::filesystem::path frontend_dist, std::filesystem::path config_file) : port_(port), frontend_dist_(std::move(frontend_dist)), config_store_(std::move(config_file)), state_resource_("/admin/radio_states", make_radio_states()), service_config_resource_(config_store_.radio_service(), "/admin/config/radio", config_store_.radio_service_transaction(), Resource_Lock_Scope::root), alert_config_resource_(config_store_.alerts(), "/admin/config/alerts", config_store_.alerts_transaction(), Resource_Lock_Scope::root), device_tables_(config_store_) { state_resource_.register_overview_status("/admin/status", 2000); state_resource_.register_status<&Radio_State::status>(2000); bind_routes(); diff --git a/backend/src/server_app.hpp b/backend/service/src/server_app.hpp similarity index 100% rename from backend/src/server_app.hpp rename to backend/service/src/server_app.hpp diff --git a/backend/tests/advanced_adapter_test.cpp b/backend/service/tests/advanced_adapter_test.cpp similarity index 94% rename from backend/tests/advanced_adapter_test.cpp rename to backend/service/tests/advanced_adapter_test.cpp index ced154d..79ca14c 100644 --- a/backend/tests/advanced_adapter_test.cpp +++ b/backend/service/tests/advanced_adapter_test.cpp @@ -45,6 +45,12 @@ struct Base_B { std::string beta{"base"}; }; struct Inherited_Config : Base_A, Base_B {}; +struct Editable_Reflected_Fields { + template + auto operator()(Field value) const { + return value.editable(); + } +}; struct Color_Code { std::string value{"#112233"}; }; @@ -158,7 +164,7 @@ template <> struct Type_Descriptor { static auto get() { using T = advanced_test::Inherited_Config; - return reflected_object("inherited", "Inherited"); + return reflected_object_with("inherited", "Inherited", advanced_test::Editable_Reflected_Fields{}); } }; template <> @@ -283,6 +289,15 @@ int main() { const auto inherited = adminive::to_descriptor_json(); assert(inherited.at("fields").at(0).at("name") == "alpha"); assert(inherited.at("fields").at(1).at("name") == "beta"); + adminive::Synchronized_Value synchronized_inherited; + synchronized_inherited.field<0>().write([](int& value) { + value = 12; + }); + synchronized_inherited.field<1>().write([](std::string& value) { + value = "changed"; + }); + assert(synchronized_inherited.field<0>().snapshot() == 12); + assert(synchronized_inherited.field<1>().snapshot() == "changed"); Control_Config controls; const auto control_form = adminive::to_amis_form_schema(controls); assert(control_form.at("body").at(0).at("type") == "input-color"); diff --git a/backend/tests/config_store_test.cpp b/backend/service/tests/config_store_test.cpp similarity index 100% rename from backend/tests/config_store_test.cpp rename to backend/service/tests/config_store_test.cpp diff --git a/backend/tests/drogon_adapter_test.cpp b/backend/service/tests/drogon_adapter_test.cpp similarity index 65% rename from backend/tests/drogon_adapter_test.cpp rename to backend/service/tests/drogon_adapter_test.cpp index e4d0687..29d19b2 100644 --- a/backend/tests/drogon_adapter_test.cpp +++ b/backend/service/tests/drogon_adapter_test.cpp @@ -18,7 +18,7 @@ template <> struct Type_Descriptor { static auto get() { using T = drogon_test::Config; - return object("config", "Config", ADMINIVE_FIELD(T, name).editable()); + return object("config", "Config", ADMINIVE_FIELD(T, name).editable().creatable()); } }; template <> @@ -100,5 +100,49 @@ int main() { response = value; }); assert(response->status == drogon::k200OK); + adminive::Drogon_Collection_Resource collection("/configs", {drogon_test::Config{"first"}}); + collection.bind(app, options); + request->parameters = {{"page", "1"}, {"perPage", "10"}}; + response.reset(); + app.handle("/configs", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) { + response = value; + }); + assert(response->status == drogon::k200OK); + assert(Json::parse(response->body).at("data").at("total") == 1); + response.reset(); + app.handle("/configs/1", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) { + response = value; + }); + assert(response->status == drogon::k200OK); + assert(Json::parse(response->body).at("data").at("name") == "first"); + request->body = R"({"name":"second"})"; + response.reset(); + app.handle("/configs", drogon::Post, request, [&response](const drogon::HttpResponsePtr& value) { + response = value; + }); + assert(!response); + assert(tasks.size() == 1); + tasks.front()(); + tasks.erase(tasks.begin()); + assert(response->status == drogon::k200OK); + assert(collection.size() == 2); + request->body = R"({"name":"changed"})"; + response.reset(); + app.handle("/configs/2", drogon::Patch, request, [&response](const drogon::HttpResponsePtr& value) { + response = value; + }); + tasks.front()(); + tasks.erase(tasks.begin()); + assert(response->status == drogon::k200OK); + assert(Json::parse(response->body).at("data").at("name") == "changed"); + response.reset(); + app.handle("/configs/1", drogon::Delete, request, [&response](const drogon::HttpResponsePtr& value) { + response = value; + }); + tasks.front()(); + tasks.erase(tasks.begin()); + assert(response->status == drogon::k200OK); + assert(collection.size() == 1); + assert(app.filters("/configs/{1:id}", drogon::Patch).size() == 2); return 0; } diff --git a/backend/tests/fake_drogon/drogon/drogon.h b/backend/service/tests/fake_drogon/drogon/drogon.h similarity index 55% rename from backend/tests/fake_drogon/drogon/drogon.h rename to backend/service/tests/fake_drogon/drogon/drogon.h index 7cdd7c8..cc1f3a1 100644 --- a/backend/tests/fake_drogon/drogon/drogon.h +++ b/backend/service/tests/fake_drogon/drogon/drogon.h @@ -1,15 +1,20 @@ #pragma once +#include +#include #include +#include #include #include #include #include +#include #include #include namespace drogon { enum HttpStatusCode { k200OK = 200, k400BadRequest = 400, + k404NotFound = 404, k422UnprocessableEntity = 422, k500InternalServerError = 500 }; @@ -18,7 +23,10 @@ enum ContentType { }; enum HttpMethod { Get, - Post + Post, + Put, + Patch, + Delete }; namespace internal { class HttpConstraint { @@ -64,14 +72,22 @@ public: std::string_view getBody() const noexcept { return body; } + const std::string& getParameter(const std::string& name) const { + const auto iterator = parameters.find(name); + return iterator == parameters.end() ? empty_parameter_ : iterator->second; + } std::string body; std::string user; + std::map parameters; +private: + inline static const std::string empty_parameter_; }; using HttpRequestPtr = std::shared_ptr; class HttpAppFramework { public: using Callback = std::function; using Handler = std::function; + using Parameter_Handler = std::function; template void registerHandler(const std::string& path, Function&& function, const std::vector& constraints) { HttpMethod method = Get; @@ -83,14 +99,31 @@ public: filters.push_back(constraint.filter()); } } - routes_.push_back(Route{path, method, Handler(std::forward(function)), std::move(filters)}); + Route route; + route.path = path; + route.method = method; + route.filters = std::move(filters); + if constexpr(std::is_invocable_v) { + route.parameter_handler = Parameter_Handler(std::forward(function)); + } else { + route.handler = Handler(std::forward(function)); + } + routes_.push_back(std::move(route)); } void handle(const std::string& path, HttpMethod method, const HttpRequestPtr& request, Callback callback) { for(auto& route : routes_) { - if(route.path == path && route.method == method) { + if(route.method != method) { + continue; + } + if(route.handler && route.path == path) { route.handler(request, std::move(callback)); return; } + std::uint64_t parameter{}; + if(route.parameter_handler && match_parameter_route(route.path, path, parameter)) { + route.parameter_handler(request, std::move(callback), parameter); + return; + } } throw std::runtime_error("route is not registered"); } @@ -105,10 +138,29 @@ public: private: struct Route { std::string path; - HttpMethod method; + HttpMethod method{Get}; Handler handler; + Parameter_Handler parameter_handler; std::vector filters; }; + static bool match_parameter_route(const std::string& pattern, const std::string& path, std::uint64_t& parameter) { + const auto begin = pattern.find('{'); + if(begin == std::string::npos) { + return false; + } + const auto end = pattern.find('}', begin); + if(end == std::string::npos) { + return false; + } + const std::string_view prefix(pattern.data(), begin); + const std::string_view suffix(pattern.data() + end + 1, pattern.size() - end - 1); + if(path.size() < prefix.size() + suffix.size() || !std::string_view(path).starts_with(prefix) || !std::string_view(path).ends_with(suffix)) { + return false; + } + const std::string_view value(path.data() + prefix.size(), path.size() - prefix.size() - suffix.size()); + const auto [parsed_end, error] = std::from_chars(value.data(), value.data() + value.size(), parameter); + return error == std::errc{} && parsed_end == value.data() + value.size(); + } std::vector routes_; }; } diff --git a/backend/tests/safety_test.cpp b/backend/service/tests/safety_test.cpp similarity index 100% rename from backend/tests/safety_test.cpp rename to backend/service/tests/safety_test.cpp diff --git a/backend/service/tests/synchronized_test.cpp b/backend/service/tests/synchronized_test.cpp new file mode 100644 index 0000000..177fc00 --- /dev/null +++ b/backend/service/tests/synchronized_test.cpp @@ -0,0 +1,175 @@ +#include "adminive/adapters/nlohmann_json.hpp" +#include "adminive/http.hpp" +#include +#include +#include +#include +#include +#include +namespace synchronized_test { +using Json = nlohmann::json; +struct Section_Config { + int editable_value{1}; + int immutable_value{2}; + int forced_value{3}; + int unlocked_editable_value{4}; +}; +struct Root_Config { + Section_Config section; + int immutable_value{5}; +}; +struct Concurrent_Config { + int first{}; + int second{}; + int immutable_value{7}; +}; +struct Probe_Lock { + static inline int exclusive_locks{}; + static inline int shared_locks{}; + void lock() noexcept { + ++exclusive_locks; + } + void unlock() noexcept {} + void lock_shared() noexcept { + ++shared_locks; + } + void unlock_shared() noexcept {} + static void reset() noexcept { + exclusive_locks = 0; + shared_locks = 0; + } +}; +} +namespace adminive { +template <> +struct Type_Descriptor { + static auto get() { + using T = synchronized_test::Section_Config; + return object("section", "Section", ADMINIVE_FIELD(T, editable_value).editable(), ADMINIVE_FIELD(T, immutable_value), ADMINIVE_FIELD(T, forced_value).locked(), ADMINIVE_FIELD(T, unlocked_editable_value).editable().locked(false)); + } +}; +template <> +struct Type_Descriptor { + static auto get() { + using T = synchronized_test::Root_Config; + return object("root", "Root", ADMINIVE_FIELD(T, section).locked(false), ADMINIVE_FIELD(T, immutable_value)); + } +}; +template <> +struct Type_Descriptor { + static auto get() { + using T = synchronized_test::Concurrent_Config; + return object("concurrent", "Concurrent", ADMINIVE_FIELD(T, first).editable(), ADMINIVE_FIELD(T, second).editable(), ADMINIVE_FIELD(T, immutable_value)); + } +}; +} +int main() { + using namespace synchronized_test; + static_assert(!adminive::Synchronized_Result); + static_assert(!adminive::Synchronized_Result>); + adminive::Synchronized_Value guarded; + Probe_Lock::reset(); + const auto immutable_root = guarded.member<&Root_Config::immutable_value>().read([](const int& value) { + return value; + }); + assert(immutable_root == 5); + assert(Probe_Lock::exclusive_locks == 0); + assert(Probe_Lock::shared_locks == 1); + auto section = guarded.member<&Root_Config::section>(); + Probe_Lock::reset(); + const auto immutable_child = section.member<&Section_Config::immutable_value>().read([](const int& value) { + return value; + }); + assert(immutable_child == 2); + assert(Probe_Lock::exclusive_locks == 0); + assert(Probe_Lock::shared_locks == 1); + Probe_Lock::reset(); + section.member<&Section_Config::editable_value>().write([](int& value) { + value = 7; + }); + assert(Probe_Lock::exclusive_locks == 1); + assert(Probe_Lock::shared_locks >= 2); + Probe_Lock::reset(); + section.member<&Section_Config::forced_value>().write([](int& value) { + value = 8; + }); + assert(Probe_Lock::exclusive_locks == 1); + assert(Probe_Lock::shared_locks >= 2); + Probe_Lock::reset(); + section.member<&Section_Config::unlocked_editable_value>().write([](int& value) { + value = 9; + }); + assert(Probe_Lock::exclusive_locks == 1); + assert(Probe_Lock::shared_locks == 0); + adminive::Resource_Service section_service(section, "/section"); + Probe_Lock::reset(); + const auto section_response = section_service.update_response(R"({"editable_value":10})"); + assert(section_response.status == 200); + assert(Probe_Lock::exclusive_locks == 1); + assert(Probe_Lock::shared_locks == 1); + adminive::Resource_Service root_scope_service(section, "/section-root", {}, adminive::Resource_Lock_Scope::root); + Probe_Lock::reset(); + const auto root_scope_response = root_scope_service.update_response(R"({"editable_value":11})"); + assert(root_scope_response.status == 200); + assert(Probe_Lock::exclusive_locks == 1); + assert(Probe_Lock::shared_locks == 0); + Probe_Lock::reset(); + const Json section_json = adminive::to_json(section); + assert(section_json.at("editable_value") == 11); + assert(section_json.at("forced_value") == 8); + assert(section_json.at("unlocked_editable_value") == 9); + assert(Probe_Lock::exclusive_locks == 1); + assert(Probe_Lock::shared_locks == 1); + Probe_Lock::reset(); + const Json root_json = adminive::to_json(guarded); + assert(root_json.at("section").at("editable_value") == 11); + assert(Probe_Lock::exclusive_locks == 1); + assert(Probe_Lock::shared_locks == 0); + const auto root_descriptor = adminive::to_descriptor_json(); + assert(root_descriptor.at("fields").at(0).at("lock_mode") == "disabled"); + assert(root_descriptor.at("fields").at(0).at("synchronized") == true); + const auto descriptor = adminive::to_descriptor_json(); + assert(descriptor.at("protocol_version") == 3); + assert(descriptor.at("fields").at(0).at("synchronized") == true); + assert(descriptor.at("fields").at(1).at("synchronized") == false); + assert(descriptor.at("fields").at(2).at("lock_mode") == "enabled"); + assert(descriptor.at("fields").at(3).at("lock_mode") == "disabled"); + adminive::Synchronized_Value unlocked; + adminive::Resource_Service unlocked_service(unlocked, "/unlocked"); + const auto response = unlocked_service.update_response(R"({"editable_value":11})"); + assert(response.status == 200); + assert(unlocked.snapshot().editable_value == 11); + adminive::Synchronized_Value concurrent; + std::atomic start{}; + auto first_writer = std::thread([&] { + while(!start.load(std::memory_order_acquire)) {} + for(int index = 0; index < 5000; ++index) { + concurrent.member<&Concurrent_Config::first>().write([index](int& value) { + value = index; + }); + } + }); + auto second_writer = std::thread([&] { + while(!start.load(std::memory_order_acquire)) {} + for(int index = 0; index < 5000; ++index) { + concurrent.member<&Concurrent_Config::second>().write([index](int& value) { + value = index; + }); + } + }); + auto serializer = std::thread([&] { + while(!start.load(std::memory_order_acquire)) {} + for(int index = 0; index < 1000; ++index) { + const Json value = adminive::to_json(concurrent); + assert(value.at("immutable_value") == 7); + } + }); + start.store(true, std::memory_order_release); + first_writer.join(); + second_writer.join(); + serializer.join(); + const auto concurrent_snapshot = concurrent.snapshot(); + assert(concurrent_snapshot.first == 4999); + assert(concurrent_snapshot.second == 4999); + return 0; +} diff --git a/backend/tests/test.cpp b/backend/service/tests/test.cpp similarity index 100% rename from backend/tests/test.cpp rename to backend/service/tests/test.cpp diff --git a/backend/third_party/include/httplib.h b/backend/service/third_party/include/httplib.h similarity index 100% rename from backend/third_party/include/httplib.h rename to backend/service/third_party/include/httplib.h diff --git a/backend/third_party/include/magic_enum/magic_enum.hpp b/backend/service/third_party/include/magic_enum/magic_enum.hpp similarity index 100% rename from backend/third_party/include/magic_enum/magic_enum.hpp rename to backend/service/third_party/include/magic_enum/magic_enum.hpp diff --git a/backend/third_party/include/magic_enum/magic_enum_all.hpp b/backend/service/third_party/include/magic_enum/magic_enum_all.hpp similarity index 100% rename from backend/third_party/include/magic_enum/magic_enum_all.hpp rename to backend/service/third_party/include/magic_enum/magic_enum_all.hpp diff --git a/backend/third_party/include/magic_enum/magic_enum_containers.hpp b/backend/service/third_party/include/magic_enum/magic_enum_containers.hpp similarity index 100% rename from backend/third_party/include/magic_enum/magic_enum_containers.hpp rename to backend/service/third_party/include/magic_enum/magic_enum_containers.hpp diff --git a/backend/third_party/include/magic_enum/magic_enum_flags.hpp b/backend/service/third_party/include/magic_enum/magic_enum_flags.hpp similarity index 100% rename from backend/third_party/include/magic_enum/magic_enum_flags.hpp rename to backend/service/third_party/include/magic_enum/magic_enum_flags.hpp diff --git a/backend/third_party/include/magic_enum/magic_enum_format.hpp b/backend/service/third_party/include/magic_enum/magic_enum_format.hpp similarity index 100% rename from backend/third_party/include/magic_enum/magic_enum_format.hpp rename to backend/service/third_party/include/magic_enum/magic_enum_format.hpp diff --git a/backend/third_party/include/magic_enum/magic_enum_fuse.hpp b/backend/service/third_party/include/magic_enum/magic_enum_fuse.hpp similarity index 100% rename from backend/third_party/include/magic_enum/magic_enum_fuse.hpp rename to backend/service/third_party/include/magic_enum/magic_enum_fuse.hpp diff --git a/backend/third_party/include/magic_enum/magic_enum_iostream.hpp b/backend/service/third_party/include/magic_enum/magic_enum_iostream.hpp similarity index 100% rename from backend/third_party/include/magic_enum/magic_enum_iostream.hpp rename to backend/service/third_party/include/magic_enum/magic_enum_iostream.hpp diff --git a/backend/third_party/include/magic_enum/magic_enum_switch.hpp b/backend/service/third_party/include/magic_enum/magic_enum_switch.hpp similarity index 100% rename from backend/third_party/include/magic_enum/magic_enum_switch.hpp rename to backend/service/third_party/include/magic_enum/magic_enum_switch.hpp diff --git a/backend/third_party/include/magic_enum/magic_enum_utility.hpp b/backend/service/third_party/include/magic_enum/magic_enum_utility.hpp similarity index 100% rename from backend/third_party/include/magic_enum/magic_enum_utility.hpp rename to backend/service/third_party/include/magic_enum/magic_enum_utility.hpp diff --git a/backend/third_party/include/nlohmann/adl_serializer.hpp b/backend/service/third_party/include/nlohmann/adl_serializer.hpp similarity index 100% rename from backend/third_party/include/nlohmann/adl_serializer.hpp rename to backend/service/third_party/include/nlohmann/adl_serializer.hpp diff --git a/backend/third_party/include/nlohmann/byte_container_with_subtype.hpp b/backend/service/third_party/include/nlohmann/byte_container_with_subtype.hpp similarity index 100% rename from backend/third_party/include/nlohmann/byte_container_with_subtype.hpp rename to backend/service/third_party/include/nlohmann/byte_container_with_subtype.hpp diff --git a/backend/third_party/include/nlohmann/detail/abi_macros.hpp b/backend/service/third_party/include/nlohmann/detail/abi_macros.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/abi_macros.hpp rename to backend/service/third_party/include/nlohmann/detail/abi_macros.hpp diff --git a/backend/third_party/include/nlohmann/detail/conversions/from_json.hpp b/backend/service/third_party/include/nlohmann/detail/conversions/from_json.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/conversions/from_json.hpp rename to backend/service/third_party/include/nlohmann/detail/conversions/from_json.hpp diff --git a/backend/third_party/include/nlohmann/detail/conversions/to_chars.hpp b/backend/service/third_party/include/nlohmann/detail/conversions/to_chars.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/conversions/to_chars.hpp rename to backend/service/third_party/include/nlohmann/detail/conversions/to_chars.hpp diff --git a/backend/third_party/include/nlohmann/detail/conversions/to_json.hpp b/backend/service/third_party/include/nlohmann/detail/conversions/to_json.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/conversions/to_json.hpp rename to backend/service/third_party/include/nlohmann/detail/conversions/to_json.hpp diff --git a/backend/third_party/include/nlohmann/detail/exceptions.hpp b/backend/service/third_party/include/nlohmann/detail/exceptions.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/exceptions.hpp rename to backend/service/third_party/include/nlohmann/detail/exceptions.hpp diff --git a/backend/third_party/include/nlohmann/detail/hash.hpp b/backend/service/third_party/include/nlohmann/detail/hash.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/hash.hpp rename to backend/service/third_party/include/nlohmann/detail/hash.hpp diff --git a/backend/third_party/include/nlohmann/detail/input/binary_reader.hpp b/backend/service/third_party/include/nlohmann/detail/input/binary_reader.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/input/binary_reader.hpp rename to backend/service/third_party/include/nlohmann/detail/input/binary_reader.hpp diff --git a/backend/third_party/include/nlohmann/detail/input/input_adapters.hpp b/backend/service/third_party/include/nlohmann/detail/input/input_adapters.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/input/input_adapters.hpp rename to backend/service/third_party/include/nlohmann/detail/input/input_adapters.hpp diff --git a/backend/third_party/include/nlohmann/detail/input/json_sax.hpp b/backend/service/third_party/include/nlohmann/detail/input/json_sax.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/input/json_sax.hpp rename to backend/service/third_party/include/nlohmann/detail/input/json_sax.hpp diff --git a/backend/third_party/include/nlohmann/detail/input/lexer.hpp b/backend/service/third_party/include/nlohmann/detail/input/lexer.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/input/lexer.hpp rename to backend/service/third_party/include/nlohmann/detail/input/lexer.hpp diff --git a/backend/third_party/include/nlohmann/detail/input/parser.hpp b/backend/service/third_party/include/nlohmann/detail/input/parser.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/input/parser.hpp rename to backend/service/third_party/include/nlohmann/detail/input/parser.hpp diff --git a/backend/third_party/include/nlohmann/detail/input/position_t.hpp b/backend/service/third_party/include/nlohmann/detail/input/position_t.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/input/position_t.hpp rename to backend/service/third_party/include/nlohmann/detail/input/position_t.hpp diff --git a/backend/third_party/include/nlohmann/detail/iterators/internal_iterator.hpp b/backend/service/third_party/include/nlohmann/detail/iterators/internal_iterator.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/iterators/internal_iterator.hpp rename to backend/service/third_party/include/nlohmann/detail/iterators/internal_iterator.hpp diff --git a/backend/third_party/include/nlohmann/detail/iterators/iter_impl.hpp b/backend/service/third_party/include/nlohmann/detail/iterators/iter_impl.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/iterators/iter_impl.hpp rename to backend/service/third_party/include/nlohmann/detail/iterators/iter_impl.hpp diff --git a/backend/third_party/include/nlohmann/detail/iterators/iteration_proxy.hpp b/backend/service/third_party/include/nlohmann/detail/iterators/iteration_proxy.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/iterators/iteration_proxy.hpp rename to backend/service/third_party/include/nlohmann/detail/iterators/iteration_proxy.hpp diff --git a/backend/third_party/include/nlohmann/detail/iterators/iterator_traits.hpp b/backend/service/third_party/include/nlohmann/detail/iterators/iterator_traits.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/iterators/iterator_traits.hpp rename to backend/service/third_party/include/nlohmann/detail/iterators/iterator_traits.hpp diff --git a/backend/third_party/include/nlohmann/detail/iterators/json_reverse_iterator.hpp b/backend/service/third_party/include/nlohmann/detail/iterators/json_reverse_iterator.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/iterators/json_reverse_iterator.hpp rename to backend/service/third_party/include/nlohmann/detail/iterators/json_reverse_iterator.hpp diff --git a/backend/third_party/include/nlohmann/detail/iterators/primitive_iterator.hpp b/backend/service/third_party/include/nlohmann/detail/iterators/primitive_iterator.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/iterators/primitive_iterator.hpp rename to backend/service/third_party/include/nlohmann/detail/iterators/primitive_iterator.hpp diff --git a/backend/third_party/include/nlohmann/detail/json_custom_base_class.hpp b/backend/service/third_party/include/nlohmann/detail/json_custom_base_class.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/json_custom_base_class.hpp rename to backend/service/third_party/include/nlohmann/detail/json_custom_base_class.hpp diff --git a/backend/third_party/include/nlohmann/detail/json_pointer.hpp b/backend/service/third_party/include/nlohmann/detail/json_pointer.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/json_pointer.hpp rename to backend/service/third_party/include/nlohmann/detail/json_pointer.hpp diff --git a/backend/third_party/include/nlohmann/detail/json_ref.hpp b/backend/service/third_party/include/nlohmann/detail/json_ref.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/json_ref.hpp rename to backend/service/third_party/include/nlohmann/detail/json_ref.hpp diff --git a/backend/third_party/include/nlohmann/detail/macro_scope.hpp b/backend/service/third_party/include/nlohmann/detail/macro_scope.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/macro_scope.hpp rename to backend/service/third_party/include/nlohmann/detail/macro_scope.hpp diff --git a/backend/third_party/include/nlohmann/detail/macro_unscope.hpp b/backend/service/third_party/include/nlohmann/detail/macro_unscope.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/macro_unscope.hpp rename to backend/service/third_party/include/nlohmann/detail/macro_unscope.hpp diff --git a/backend/third_party/include/nlohmann/detail/meta/call_std/begin.hpp b/backend/service/third_party/include/nlohmann/detail/meta/call_std/begin.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/meta/call_std/begin.hpp rename to backend/service/third_party/include/nlohmann/detail/meta/call_std/begin.hpp diff --git a/backend/third_party/include/nlohmann/detail/meta/call_std/end.hpp b/backend/service/third_party/include/nlohmann/detail/meta/call_std/end.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/meta/call_std/end.hpp rename to backend/service/third_party/include/nlohmann/detail/meta/call_std/end.hpp diff --git a/backend/third_party/include/nlohmann/detail/meta/cpp_future.hpp b/backend/service/third_party/include/nlohmann/detail/meta/cpp_future.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/meta/cpp_future.hpp rename to backend/service/third_party/include/nlohmann/detail/meta/cpp_future.hpp diff --git a/backend/third_party/include/nlohmann/detail/meta/detected.hpp b/backend/service/third_party/include/nlohmann/detail/meta/detected.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/meta/detected.hpp rename to backend/service/third_party/include/nlohmann/detail/meta/detected.hpp diff --git a/backend/third_party/include/nlohmann/detail/meta/identity_tag.hpp b/backend/service/third_party/include/nlohmann/detail/meta/identity_tag.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/meta/identity_tag.hpp rename to backend/service/third_party/include/nlohmann/detail/meta/identity_tag.hpp diff --git a/backend/third_party/include/nlohmann/detail/meta/is_sax.hpp b/backend/service/third_party/include/nlohmann/detail/meta/is_sax.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/meta/is_sax.hpp rename to backend/service/third_party/include/nlohmann/detail/meta/is_sax.hpp diff --git a/backend/third_party/include/nlohmann/detail/meta/std_fs.hpp b/backend/service/third_party/include/nlohmann/detail/meta/std_fs.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/meta/std_fs.hpp rename to backend/service/third_party/include/nlohmann/detail/meta/std_fs.hpp diff --git a/backend/third_party/include/nlohmann/detail/meta/type_traits.hpp b/backend/service/third_party/include/nlohmann/detail/meta/type_traits.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/meta/type_traits.hpp rename to backend/service/third_party/include/nlohmann/detail/meta/type_traits.hpp diff --git a/backend/third_party/include/nlohmann/detail/meta/void_t.hpp b/backend/service/third_party/include/nlohmann/detail/meta/void_t.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/meta/void_t.hpp rename to backend/service/third_party/include/nlohmann/detail/meta/void_t.hpp diff --git a/backend/third_party/include/nlohmann/detail/output/binary_writer.hpp b/backend/service/third_party/include/nlohmann/detail/output/binary_writer.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/output/binary_writer.hpp rename to backend/service/third_party/include/nlohmann/detail/output/binary_writer.hpp diff --git a/backend/third_party/include/nlohmann/detail/output/output_adapters.hpp b/backend/service/third_party/include/nlohmann/detail/output/output_adapters.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/output/output_adapters.hpp rename to backend/service/third_party/include/nlohmann/detail/output/output_adapters.hpp diff --git a/backend/third_party/include/nlohmann/detail/output/serializer.hpp b/backend/service/third_party/include/nlohmann/detail/output/serializer.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/output/serializer.hpp rename to backend/service/third_party/include/nlohmann/detail/output/serializer.hpp diff --git a/backend/third_party/include/nlohmann/detail/string_concat.hpp b/backend/service/third_party/include/nlohmann/detail/string_concat.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/string_concat.hpp rename to backend/service/third_party/include/nlohmann/detail/string_concat.hpp diff --git a/backend/third_party/include/nlohmann/detail/string_escape.hpp b/backend/service/third_party/include/nlohmann/detail/string_escape.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/string_escape.hpp rename to backend/service/third_party/include/nlohmann/detail/string_escape.hpp diff --git a/backend/third_party/include/nlohmann/detail/string_utils.hpp b/backend/service/third_party/include/nlohmann/detail/string_utils.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/string_utils.hpp rename to backend/service/third_party/include/nlohmann/detail/string_utils.hpp diff --git a/backend/third_party/include/nlohmann/detail/value_t.hpp b/backend/service/third_party/include/nlohmann/detail/value_t.hpp similarity index 100% rename from backend/third_party/include/nlohmann/detail/value_t.hpp rename to backend/service/third_party/include/nlohmann/detail/value_t.hpp diff --git a/backend/third_party/include/nlohmann/json.hpp b/backend/service/third_party/include/nlohmann/json.hpp similarity index 100% rename from backend/third_party/include/nlohmann/json.hpp rename to backend/service/third_party/include/nlohmann/json.hpp diff --git a/backend/third_party/include/nlohmann/json_fwd.hpp b/backend/service/third_party/include/nlohmann/json_fwd.hpp similarity index 100% rename from backend/third_party/include/nlohmann/json_fwd.hpp rename to backend/service/third_party/include/nlohmann/json_fwd.hpp diff --git a/backend/third_party/include/nlohmann/ordered_map.hpp b/backend/service/third_party/include/nlohmann/ordered_map.hpp similarity index 100% rename from backend/third_party/include/nlohmann/ordered_map.hpp rename to backend/service/third_party/include/nlohmann/ordered_map.hpp diff --git a/backend/third_party/include/nlohmann/thirdparty/hedley/hedley.hpp b/backend/service/third_party/include/nlohmann/thirdparty/hedley/hedley.hpp similarity index 100% rename from backend/third_party/include/nlohmann/thirdparty/hedley/hedley.hpp rename to backend/service/third_party/include/nlohmann/thirdparty/hedley/hedley.hpp diff --git a/backend/third_party/include/nlohmann/thirdparty/hedley/hedley_undef.hpp b/backend/service/third_party/include/nlohmann/thirdparty/hedley/hedley_undef.hpp similarity index 100% rename from backend/third_party/include/nlohmann/thirdparty/hedley/hedley_undef.hpp rename to backend/service/third_party/include/nlohmann/thirdparty/hedley/hedley_undef.hpp diff --git a/backend/third_party/init.bash b/backend/service/third_party/init.bash similarity index 100% rename from backend/third_party/init.bash rename to backend/service/third_party/init.bash diff --git a/backend/tests/synchronized_test.cpp b/backend/tests/synchronized_test.cpp deleted file mode 100644 index 1fb61d9..0000000 --- a/backend/tests/synchronized_test.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "adminive/adapters/nlohmann_json.hpp" -#include "adminive/http.hpp" -#include -#include -namespace synchronized_test { -using Json = nlohmann::json; -struct Section_Config { - int first{1}; - int second{2}; -}; -struct Root_Config { - Section_Config section; - int unrelated{3}; -}; -struct Counting_Lock { - int depth{}; - int lock_count{}; - int unlock_count{}; - void lock() noexcept { - ++depth; - ++lock_count; - } - void unlock() noexcept { - --depth; - ++unlock_count; - } -}; -} -namespace adminive { -template <> -struct Type_Descriptor { - static auto get() { - using T = synchronized_test::Section_Config; - return object("section", "Section", ADMINIVE_FIELD(T, first).editable(), ADMINIVE_FIELD(T, second).editable()); - } -}; -template <> -struct Type_Descriptor { - static auto get() { - using T = synchronized_test::Root_Config; - return object("root", "Root", ADMINIVE_FIELD(T, section).editable(), ADMINIVE_FIELD(T, unrelated).editable()); - } -}; -} -int main() { - using namespace synchronized_test; - adminive::Synchronized_Value guarded; - const Json initial = adminive::to_json(guarded); - assert(initial.at("section").at("first") == 1); - assert(guarded.mutex().depth == 0); - assert(guarded.mutex().lock_count == 1); - auto section = guarded.member<&Root_Config::section>(); - const Json form = adminive::to_amis_form_schema(section, "/section/data", "Apply"); - assert(form.at("data").at("first") == 1); - const auto patch_result = adminive::apply_frontend_patch(section, Json{{"first", 7}, {"second", 8}}); - assert(patch_result.success); - assert(section.snapshot().first == 7); - bool commit_saw_lock{}; - adminive::Resource_Transaction transaction; - transaction.commit = [&](const Section_Config& value, const adminive::Request_Context&) { - commit_saw_lock = guarded.mutex().depth == 1 && value.first == 9 && value.second == 8; - }; - adminive::Resource_Service service(section, "/section", std::move(transaction)); - const auto response = service.update_response(R"({"first":9})"); - assert(response.status == 200); - assert(commit_saw_lock); - assert(section.snapshot().first == 9); - assert(guarded.mutex().depth == 0); - adminive::Synchronized_Value unlocked; - adminive::Resource_Service unlocked_service(unlocked, "/unlocked"); - const auto unlocked_response = unlocked_service.update_response(R"({"second":11})"); - assert(unlocked_response.status == 200); - assert(unlocked.snapshot().second == 11); - return 0; -} diff --git a/cmake/AdminiveConfig.cmake.in b/cmake/AdminiveConfig.cmake.in index 940d4e9..8f21304 100644 --- a/cmake/AdminiveConfig.cmake.in +++ b/cmake/AdminiveConfig.cmake.in @@ -1,19 +1,39 @@ @PACKAGE_INIT@ include(CMakeFindDependencyMacro) -include("${CMAKE_CURRENT_LIST_DIR}/AdminiveTargets.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/AdminiveCoreTargets.cmake") if(NOT TARGET Adminive::Adminive) add_library(Adminive::Adminive INTERFACE IMPORTED) set_property(TARGET Adminive::Adminive PROPERTY INTERFACE_LINK_LIBRARIES Adminive::Core) endif() +if(NOT TARGET Adminive::Http) + add_library(Adminive::Http INTERFACE IMPORTED) + set_property(TARGET Adminive::Http PROPERTY INTERFACE_LINK_LIBRARIES Adminive::Core) +endif() +set(_adminive_service_loaded FALSE) foreach(component IN LISTS Adminive_FIND_COMPONENTS) - if(component STREQUAL "Drogon") + if(component STREQUAL "Core" OR component STREQUAL "Http") + set(Adminive_${component}_FOUND TRUE) + elseif(component STREQUAL "Service" OR component STREQUAL "Nlohmann" OR component STREQUAL "MagicEnum" OR component STREQUAL "BoostPfr" OR component STREQUAL "Httplib" OR component STREQUAL "Default") + if(component STREQUAL "BoostPfr") + find_dependency(Boost CONFIG) + endif() + if(NOT _adminive_service_loaded) + include("${CMAKE_CURRENT_LIST_DIR}/AdminiveServiceTargets.cmake") + set(_adminive_service_loaded TRUE) + endif() + set(Adminive_${component}_FOUND TRUE) + elseif(component STREQUAL "Drogon") + if(NOT _adminive_service_loaded) + include("${CMAKE_CURRENT_LIST_DIR}/AdminiveServiceTargets.cmake") + set(_adminive_service_loaded TRUE) + endif() find_dependency(Drogon CONFIG) if(TARGET drogon AND NOT TARGET Drogon::Drogon) add_library(Drogon::Drogon ALIAS drogon) endif() include("${CMAKE_CURRENT_LIST_DIR}/AdminiveDrogonTargets.cmake") set(Adminive_Drogon_FOUND TRUE) - elseif(NOT component STREQUAL "Core" AND NOT component STREQUAL "Http" AND NOT component STREQUAL "Httplib" AND NOT component STREQUAL "Default") + else() set(Adminive_${component}_FOUND FALSE) endif() endforeach()