diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef0967b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/cmake-build-debug/ +/.idea +/*.zip diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8cedc60 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.20) +project(Structive LANGUAGES CXX) +include(CTest) +add_subdirectory(core) +add_subdirectory(extensions) diff --git a/README.md b/README.md index 4876b3a..898f2ee 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,329 @@ # Structive -增强版c++ struct \ No newline at end of file +**Structive enhances ordinary C++ structs with an explicit structural metadata and managed-property layer without replacing their native data model.** + +[中文文档](README.zh-CN.md) · [Design Philosophy](docs/DESIGN.md) · [Core Guide](docs/CORE_GUIDE.md) · [Extension Guide](docs/EXTENSIONS.md) + +## What Structive is + +Structive is a C++20 property and structural-description system built around two deliberately separate layers: + +```text +C++ object model + ordinary members, member functions, native layout and direct access + │ + ├── Type_Descriptor → Object_Schema + │ compile-time structure, keys, attributes, constraints, access capabilities + │ + └── Property_Object + per-instance managed access, synchronization, traversal and runtime access +``` + +A type can remain recognizably ordinary C++: + +```cpp +#include +using namespace structive; +struct Device : Property_Object { + double temperature{25.0}; + double pressure{101.3}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + defaults( + external_access, + persistence_access + ), + field<&Device::temperature>( + key<"temperature">, + unit<"C">, + min_value<-50.0>, + max_value<200.0> + ), + field<&Device::pressure>( + key<"pressure">, + unit<"kPa"> + ) + ); + } +}; +``` + +The members are still real members. Structive adds a second, explicit layer that generic systems can understand. + +## Core idea + +Structive follows one central rule: + +> **Enhance the struct; do not replace the struct.** + +This has several consequences: + +- A registered field remains an ordinary C++ member. +- Unregistered members remain completely outside the property system. +- Member pointers are the preferred compile-time identity for business code. +- String keys exist for runtime and adapter boundaries. +- Metadata does not force storage wrappers such as `Property` into every field. +- Validation, synchronization, persistence capability and presentation metadata remain separate concerns. + +The result is intended to let the same C++ type participate in UI, persistence, serialization, RPC or tooling layers without making the core object depend on those systems. + +## Project layers + +```text +core/ +└── structive::property_core + ├── INTERFACE target + ├── Type_Descriptor and Object_Schema + ├── member/computed accessors + ├── unified Attribute protocol + ├── constraints and explicit validation + ├── access capability metadata + ├── synchronization plans and guards + ├── typed and runtime access + └── traversal + +extensions/ +└── structive::property_extensions + ├── STATIC target + ├── depends on property_core + ├── defines extension-owned Attribute categories + └── currently provides presentation metadata interpretation +``` + +The dependency direction is one-way: **extensions depend on core; core never includes or links extensions.** + +## Why not `Property` members? + +Structive intentionally does not require this: + +```cpp +struct Device { + Property temperature; +}; +``` + +Instead, the storage stays native: + +```cpp +struct Device : Property_Object { + double temperature; +}; +``` + +and the structural meaning is declared separately with `Type_Descriptor`. + +This preserves normal C++ member semantics, keeps raw object access available when it is intentionally needed, and lets Structive remain an enhancement layer rather than a replacement object model. + +## Schema and managed object are different concepts + +`Type_Descriptor` describes the **type**. `Property_Object` adds state and behavior to an **instance**. + +The schema contains the registered property tuple, object defaults and the default synchronization plan. A `Property_Object` resolves that plan for each instance and owns its lock topology and mutex storage. + +This distinction matters for performance and architecture: structural description is type-level information; managed synchronization is instance-level state. + +## Unified Attribute model + +There is one Attribute protocol. Core metadata and extension metadata use the same mechanism. + +A single-valued Attribute category can be defined as: + +```cpp +struct Label_Category {}; +template +struct Label_Attribute { + using attribute_category = Label_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = false; + static constexpr auto value = Value; +}; +``` + +Then it can be attached directly to a property: + +```cpp +field<&Device::temperature>( + key<"temperature">, + presentation::label<"Temperature"> +) +``` + +Core stores and traverses the Attribute but does not interpret categories it does not own. The owning extension interprets its own category. + +This is the primary extension boundary of Structive. + +## Managed access is not forced encapsulation + +Both of these are valid but mean different things: + +```cpp +device.temperature = 30.0; +device.write<&Device::temperature>(30.0); +``` + +The first is the **raw C++ path**. It bypasses Structive-managed synchronization, access capabilities and other managed behavior. + +The second is the **managed path**. It resolves the registered property and uses the configured synchronization behavior. + +Structive intentionally keeps both. A codebase should choose the appropriate path according to its ownership and concurrency rules. + +## Access capability views + +Core defines three managed access modes: + +- `internal`: normal managed access from application code. +- `external`: controlled by `External_Access` metadata. +- `persistence`: controlled by `Persistence_Access` metadata. + +Example: + +```cpp +device.write<&Device::temperature>(30.0); +device.external().write<&Device::temperature>(31.0); +device.persistence().load<&Device::temperature>(32.0); +auto stored = device.persistence().store<&Device::temperature>(); +``` + +Capability semantics are checked by the schema: a property cannot advertise readable/writable capabilities that its accessor does not actually support. + +## Validation is explicit + +Constraints are metadata. `write()` does **not** automatically execute them. + +```cpp +auto error = validate_property_value<&Device::temperature>(device.schema(), 500.0); +if (error) { + // error->property_key + // error->code +} +``` + +This is intentional. Field validation, cross-field invariants, transactions and rollback are different concerns and should not be hidden inside a generic setter. + +## Synchronization is explicit and composable + +The default modes are: + +```cpp +sync_all_independent +sync_all_shared +sync_all_unsynchronized +``` + +Properties may then be overridden or grouped: + +```cpp +synchronization( + sync_all_independent, + sync_group<&Device::min_speed, &Device::max_speed>("speed_range") +) +``` + +A group means those properties resolve to the same lock slot. Multi-property guards deduplicate lock slots and acquire them in stable order. + +```cpp +auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); +auto old_min = guard.get<&Device::min_speed>(); +guard.set<&Device::min_speed>(20.0); +``` + +Synchronization does not imply validation, transaction, rollback or event emission. + +## Computed properties + +Structive supports synchronized computed properties: + +```cpp +computed_property([](const auto& view) { + return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); +}, key<"speed_span">, external_access) +``` + +A synchronized computed property reads dependencies through the synchronization view. Those dependencies must resolve to the same synchronization slot as the computed property. This makes the dependency relationship explicit in the synchronization plan. + +Advanced trusted accessor forms also exist for member-function getters and getter/setter pairs. They intentionally operate through trusted object access and therefore should be used only when their synchronization semantics are understood by the caller. + +## Runtime access + +`Property_Object_Base` provides type-erased runtime access for adapter-style code: + +```cpp +Property_Object_Base& erased = device; +auto type = erased.runtime_object_type(); +auto count = erased.runtime_property_count(); +``` + +Runtime reads and writes use property keys, access modes and `std::type_info`, and report one of: + +```text +ok +unknown_property +not_readable +not_writable +type_mismatch +``` + +This path is intended for boundaries such as generic serialization, HTTP/RPC adapters, scripting bridges or tooling. Compile-time business code should generally prefer member pointers. + +## Current core-owned metadata + +The current core defines: + +- `key<"...">` +- `external_access<...>` +- `persistence_access<...>` +- `unit<"...">` +- `sensitive<>` +- `min_value<...>` +- `max_value<...>` +- `finite` +- `constraint<"code">(...)` + +`external_access`, `persistence_access` and `sensitive` are inheritable and can be supplied through `defaults(...)`. Property-level declarations override object defaults for the same category. + +## Current extension metadata + +The presentation extension currently defines: + +- `presentation::label<"...">` +- `presentation::description<"...">` +- `presentation::group<"...">` +- `presentation::order` + +`presentation::describe()` interprets those attributes and falls back to the property key when no label is declared. That fallback belongs to the presentation extension, not to Property Core. + +## Build + +Structive currently exposes CMake targets for use through the project tree: + +```cmake +add_subdirectory(path/to/Structive) +target_link_libraries(my_target PRIVATE structive::property_core) +``` + +For linked extensions: + +```cmake +target_link_libraries(my_target PRIVATE structive::property_extensions) +``` + +Build and test the repository with: + +```bash +cmake -S . -B build -DBUILD_TESTING=ON +cmake --build build +ctest --test-dir build --output-on-failure +``` + +## Documentation + +- [Design Philosophy and Principles](docs/DESIGN.md) +- [Core Guide](docs/CORE_GUIDE.md) +- [Extension Architecture](docs/EXTENSIONS.md) +- [中文首页](README.zh-CN.md) +- [设计理念与原则(中文)](docs/DESIGN.zh-CN.md) +- [核心使用指南(中文)](docs/CORE_GUIDE.zh-CN.md) +- [扩展体系(中文)](docs/EXTENSIONS.zh-CN.md) diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..b6a5f7e --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,327 @@ +# Structive + +**Structive 的目标是在不替换 C++ 原生数据模型的前提下,为普通 `struct` 增加显式的结构描述、属性元数据和受管理访问能力。** + +[English](README.md) · [设计理念](docs/DESIGN.zh-CN.md) · [核心指南](docs/CORE_GUIDE.zh-CN.md) · [扩展体系](docs/EXTENSIONS.zh-CN.md) + +## Structive 是什么 + +Structive 是一个 C++20 属性与结构描述系统。它刻意把“类型结构描述”和“对象实例管理”分成两层: + +```text +C++ 原生对象模型 + 普通成员、成员函数、原生布局、直接访问 + │ + ├── Type_Descriptor → Object_Schema + │ 类型级结构、key、Attribute、约束、访问能力 + │ + └── Property_Object + 实例级受管理访问、同步、遍历、运行时访问 +``` + +业务类型仍然可以保持非常普通: + +```cpp +#include +using namespace structive; +struct Device : Property_Object { + double temperature{25.0}; + double pressure{101.3}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + defaults( + external_access, + persistence_access + ), + field<&Device::temperature>( + key<"temperature">, + unit<"C">, + min_value<-50.0>, + max_value<200.0> + ), + field<&Device::pressure>( + key<"pressure">, + unit<"kPa"> + ) + ); + } +}; +``` + +成员仍然是真实的 C++ 成员。Structive 只是额外建立一层可以被泛型系统理解的结构语义。 + +## 核心思想 + +Structive 最核心的原则只有一句: + +> **增强 struct,而不是替代 struct。** + +因此它坚持: + +- 注册后的字段仍然是普通 C++ 成员。 +- 未注册成员完全不进入属性系统。 +- 业务代码优先使用成员指针作为编译期属性身份。 +- 字符串 `key` 主要服务运行时和适配器边界。 +- 不要求把每个成员改造成 `Property` 之类的包装类型。 +- Validation、Synchronization、Persistence、Presentation 等语义相互分离。 + +这样,同一个业务结构体可以被 UI、持久化、序列化、RPC、脚本或工具系统理解,但业务类型本身不需要依赖这些系统。 + +## 项目分层 + +```text +core/ +└── structive::property_core + ├── INTERFACE target + ├── Type_Descriptor / Object_Schema + ├── member / computed accessor + ├── 统一 Attribute 协议 + ├── constraint 与显式 validation + ├── access capability 元数据 + ├── synchronization plan 与 guard + ├── typed / runtime access + └── traversal + +extensions/ +└── structive::property_extensions + ├── STATIC target + ├── 只依赖 property_core + ├── 定义扩展自己拥有的 Attribute category + └── 当前实现 presentation 元数据解释 +``` + +依赖方向必须保持单向:**Extension 依赖 Core;Core 永远不 include、不 link Extension。** + +## 为什么不使用 `Property` 成员 + +Structive 不要求这样定义对象: + +```cpp +struct Device { + Property temperature; +}; +``` + +而是保留原生存储: + +```cpp +struct Device : Property_Object { + double temperature; +}; +``` + +然后通过 `Type_Descriptor` 单独声明结构语义。 + +这样可以保留真实成员指针、原生成员语义和必要时的直接访问能力。Structive 是增强层,不建立第二套替代 C++ 的对象模型。 + +## Schema 和 Managed Object 是两种概念 + +`Type_Descriptor` 描述的是**类型**;`Property_Object` 管理的是**实例**。 + +Schema 保存注册属性列表、对象默认 Attribute 和默认同步计划。`Property_Object` 在每个实例上解析同步计划,并持有自己的锁拓扑和 mutex 存储。 + +这个边界必须长期保持:结构描述属于类型级;同步状态属于实例级。 + +## 只有一套 Attribute 协议 + +Core Attribute 和 Extension Attribute 使用同一机制,不存在额外的 `hint` 通道。 + +例如一个单值 Attribute category 可以这样定义: + +```cpp +struct Label_Category {}; +template +struct Label_Attribute { + using attribute_category = Label_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = false; + static constexpr auto value = Value; +}; +``` + +然后直接挂到属性上: + +```cpp +field<&Device::temperature>( + key<"temperature">, + presentation::label<"Temperature"> +) +``` + +Core 负责统一保存和遍历,但只解释自己拥有的 category。Presentation、JSON、RPC 等扩展应该自己解释自己的 category。 + +这是 Structive 最主要的扩展边界。 + +## Managed Access 不是强制封装 + +下面两种写法都合法,但语义不同: + +```cpp +device.temperature = 30.0; +device.write<&Device::temperature>(30.0); +``` + +第一种是 **raw C++ path**,不会经过 Structive 的同步和 capability 管理。 + +第二种是 **managed path**,会定位注册属性并按该对象实例的同步计划执行。 + +Structive 有意保留两条路径。业务代码应该根据对象所有权和并发规则选择,而不是假装所有成员访问都能被框架强制拦截。 + +## 访问能力视图 + +Core 当前定义三种受管理访问模式: + +- `internal`:应用内部正常受管理访问。 +- `external`:由 `External_Access` 元数据控制。 +- `persistence`:由 `Persistence_Access` 元数据控制。 + +```cpp +device.write<&Device::temperature>(30.0); +device.external().write<&Device::temperature>(31.0); +device.persistence().load<&Device::temperature>(32.0); +auto stored = device.persistence().store<&Device::temperature>(); +``` + +Schema 会在编译期约束 capability 与 accessor 能力一致,例如只读 computed property 不能声明为可写。 + +## Validation 必须显式 + +Constraint 是结构元数据;`write()` **不会自动执行 constraint**。 + +```cpp +auto error = validate_property_value<&Device::temperature>(device.schema(), 500.0); +if (error) { + // error->property_key + // error->code +} +``` + +这是设计选择,不是缺功能。单字段校验、跨字段不变量、事务和回滚是不同概念,不应该偷偷塞进一个通用 setter。 + +## Synchronization 明确且可组合 + +默认同步模式包括: + +```cpp +sync_all_independent +sync_all_shared +sync_all_unsynchronized +``` + +也可以覆盖单个字段或建立同步组: + +```cpp +synchronization( + sync_all_independent, + sync_group<&Device::min_speed, &Device::max_speed>("speed_range") +) +``` + +同一 group 的属性会解析到同一个 lock slot。多属性 Guard 会对 lock slot 去重,并按稳定顺序获取锁。 + +```cpp +auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); +auto old_min = guard.get<&Device::min_speed>(); +guard.set<&Device::min_speed>(20.0); +``` + +Synchronization 只负责同步,不自动等价于 validation、transaction、rollback 或事件系统。 + +## Computed Property + +Structive 支持基于同步视图读取依赖项的 computed property: + +```cpp +computed_property([](const auto& view) { + return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); +}, key<"speed_span">, external_access) +``` + +这种 computed property 的依赖字段必须和 computed property 本身落在同一个同步 slot,否则运行时会拒绝跨 slot 读取。同步计划因此显式表达了 computed value 的一致性边界。 + +Core 还提供 `trusted_computed_property` 和 `trusted_accessor_property` 作为高级入口。这类 accessor 直接通过受信任对象访问执行成员函数,不经过 synchronized view 的依赖检查,因此只应该在调用方明确掌握同步语义时使用。 + +## 运行时访问 + +`Property_Object_Base` 提供 type-erased runtime access: + +```cpp +Property_Object_Base& erased = device; +auto type = erased.runtime_object_type(); +auto count = erased.runtime_property_count(); +``` + +Runtime read/write 使用 property key、access mode 和 `std::type_info`,返回: + +```text +ok +unknown_property +not_readable +not_writable +type_mismatch +``` + +它适合 JSON、HTTP/RPC、脚本桥接、动态工具等边界。普通编译期业务代码仍然应优先使用成员指针。 + +## Core 当前元数据 + +当前 Core 提供: + +- `key<"...">` +- `external_access<...>` +- `persistence_access<...>` +- `unit<"...">` +- `sensitive<>` +- `min_value<...>` +- `max_value<...>` +- `finite` +- `constraint<"code">(...)` + +其中 `external_access`、`persistence_access`、`sensitive` 支持继承,可以放入 `defaults(...)`。同 category 的 property 级 Attribute 会覆盖 object default。 + +## 当前 Presentation Extension + +当前扩展层定义: + +- `presentation::label<"...">` +- `presentation::description<"...">` +- `presentation::group<"...">` +- `presentation::order` + +`presentation::describe()` 解释这些 Attribute;未提供 label 时由 Presentation Extension 使用 property key 作为 fallback。这个 fallback 不属于 Core。 + +## 构建 + +当前 CMake 目标适用于作为子目录接入: + +```cmake +add_subdirectory(path/to/Structive) +target_link_libraries(my_target PRIVATE structive::property_core) +``` + +使用链接型扩展: + +```cmake +target_link_libraries(my_target PRIVATE structive::property_extensions) +``` + +仓库构建测试: + +```bash +cmake -S . -B build -DBUILD_TESTING=ON +cmake --build build +ctest --test-dir build --output-on-failure +``` + +## 详细文档 + +- [设计理念与原则](docs/DESIGN.zh-CN.md) +- [Core 完整指南](docs/CORE_GUIDE.zh-CN.md) +- [Extension 架构指南](docs/EXTENSIONS.zh-CN.md) +- [English README](README.md) +- [Design Philosophy](docs/DESIGN.md) +- [Core Guide](docs/CORE_GUIDE.md) +- [Extension Architecture](docs/EXTENSIONS.md) diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt new file mode 100644 index 0000000..14546d7 --- /dev/null +++ b/core/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(structive_property_core INTERFACE) +add_library(structive::property_core ALIAS structive_property_core) +target_include_directories(structive_property_core INTERFACE "${CMAKE_CURRENT_LIST_DIR}/include") +target_compile_features(structive_property_core INTERFACE cxx_std_20) +add_executable(structive_property_core_example "${CMAKE_CURRENT_LIST_DIR}/example/main.cpp") +target_link_libraries(structive_property_core_example PRIVATE structive::property_core) +if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(structive_property_core_example PRIVATE -Wall -Wextra -Wpedantic) +endif () +if (BUILD_TESTING) + find_package(Threads REQUIRED) + add_executable(structive_property_core_test "${CMAKE_CURRENT_LIST_DIR}/tests/property_core_test.cpp") + target_link_libraries(structive_property_core_test PRIVATE structive::property_core Threads::Threads) + if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(structive_property_core_test PRIVATE -Wall -Wextra -Wpedantic) + endif () + add_test(NAME structive_property_core_test COMMAND structive_property_core_test) +endif () diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..5b75683 --- /dev/null +++ b/core/README.md @@ -0,0 +1,14 @@ +# Structive Property Core + +The Core is the C++20 header-only structural and managed-property layer of Structive. + +It provides `Type_Descriptor`, `Object_Schema`, member/computed property descriptors, the unified Attribute protocol, explicit constraints and validation, access capability views, synchronization plans/guards, typed traversal and type-erased runtime access. + +The core design rule is that registered properties remain ordinary C++ members. Structive adds a managed structural layer; it does not replace the native object model. + +See the complete documentation: + +- [Core Guide](../docs/CORE_GUIDE.md) +- [Design Philosophy](../docs/DESIGN.md) +- [中文 Core 指南](../docs/CORE_GUIDE.zh-CN.md) +- [中文设计理念](../docs/DESIGN.zh-CN.md) diff --git a/core/README.zh-CN.md b/core/README.zh-CN.md new file mode 100644 index 0000000..81c8b9c --- /dev/null +++ b/core/README.zh-CN.md @@ -0,0 +1,14 @@ +# Structive Property Core + +Core 是 Structive 的 C++20 header-only 结构描述与 managed-property 核心层。 + +它提供 `Type_Descriptor`、`Object_Schema`、member/computed property descriptor、统一 Attribute 协议、显式 constraint/validation、access capability view、synchronization plan/guard、typed traversal,以及 type-erased runtime access。 + +Core 的基本原则是:注册后的 property 仍然是普通 C++ 成员。Structive 增加受管理结构层,但不替代原生对象模型。 + +完整文档: + +- [Core 完整指南](../docs/CORE_GUIDE.zh-CN.md) +- [设计理念与原则](../docs/DESIGN.zh-CN.md) +- [Core Guide](../docs/CORE_GUIDE.md) +- [Design Philosophy](../docs/DESIGN.md) diff --git a/core/example/main.cpp b/core/example/main.cpp new file mode 100644 index 0000000..e66b7d0 --- /dev/null +++ b/core/example/main.cpp @@ -0,0 +1,51 @@ +#include +#include +#include +using namespace structive; +struct Device : Property_Object { + double temperature{25.0}; + double pressure{101.3}; + double min_speed{10.0}; + double max_speed{100.0}; + std::string name{"device-1"}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + defaults(external_access, persistence_access), + synchronization(sync_all_independent, sync_group<&Device::min_speed, &Device::max_speed>("speed_range")), + field<&Device::temperature>(key<"temperature">, unit<"C">, min_value<-50.0>, max_value<200.0>), + field<&Device::pressure>(key<"pressure">, unit<"kPa">), + field<&Device::min_speed>(key<"min_speed">), + field<&Device::max_speed>(key<"max_speed">), + field<&Device::name>(key<"name">) + ); + } +}; +static bool update_speed_range(Device& device, double min_speed, double max_speed) { + auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); + auto old_min = guard.get<&Device::min_speed>(); + auto old_max = guard.get<&Device::max_speed>(); + guard.set < &Device::min_speed > (min_speed); + guard.set < &Device::max_speed > (max_speed); + if (min_speed <= max_speed) { + return true; + } + guard.set < &Device::min_speed > (old_min); + guard.set < &Device::max_speed > (old_max); + return false; +} +int main() { + Device device; + device.external().write < &Device::temperature > (30.0); + std::cout << "temperature=" << device.external().read<&Device::temperature>() << '\n'; + std::cout << "temperature lock slot=" << device.lock_slot<&Device::temperature>() << '\n'; + std::cout << "min/max shared slot=" << device.lock_slot<&Device::min_speed>() << '\n'; + std::cout << "max_speed key=" << device.schema().property<&Device::max_speed>().key() << '\n'; + if (!update_speed_range(device, 120.0, 80.0)) { + std::cout << "invalid range rolled back by business logic\n"; + } + device.temperature = 31.0; + std::cout << "raw temperature=" << device.temperature << '\n'; +} diff --git a/core/include/structive/property/accessor.hpp b/core/include/structive/property/accessor.hpp new file mode 100644 index 0000000..c26791f --- /dev/null +++ b/core/include/structive/property/accessor.hpp @@ -0,0 +1,125 @@ +#pragma once +#include +#include +#include +#include +namespace structive { +template +struct Member_Pointer_Traits; +template +struct Member_Pointer_Traits { + using object_type = Object; + using value_type = Value; +}; +template +struct Member_Function_Traits; +template +struct Member_Function_Traits { + using object_type = Object; + using return_type = Return; +}; +template +struct Member_Function_Traits { + using object_type = Object; + using return_type = Return; +}; +template +struct Member_Function_Traits : Member_Function_Traits {}; +template +struct Member_Function_Traits : Member_Function_Traits {}; +template +struct Member_Storage_Identity {}; +template +concept Trusted_Getter_Function = requires { + typename Member_Function_Traits::object_type; +} && requires(const typename Member_Function_Traits::object_type& object) { + std::invoke(Getter, object); +} && (!std::is_void_v::object_type&>>) && (!std::is_rvalue_reference_v::object_type&>>) && (!(std::is_lvalue_reference_v::object_type&>> && !std::is_const_v::object_type&>>>)); +template +using trusted_getter_object_t = typename Member_Function_Traits::object_type; +template +using trusted_getter_value_t = std::remove_cvref_t&>>; +template +concept Trusted_Setter_Function_For = requires { + typename Member_Function_Traits::object_type; +} && std::same_as::object_type, Object> && requires(Object& object, Value value) { + { std::invoke(Setter, object, std::move(value)) } -> std::same_as; +}; +template +struct Member_Accessor { + using traits = Member_Pointer_Traits; + using object_type = typename traits::object_type; + using value_type = typename traits::value_type; + using storage_identity = Member_Storage_Identity; + static constexpr bool readable = true; + static constexpr bool writable = !std::is_const_v; + static constexpr bool synchronized_view_read = false; + static constexpr bool trusted_object_access = false; + static constexpr auto member = Member; + constexpr const value_type& read(const object_type& object) const { + return object.*Member; + } + constexpr value_type& read(object_type& object) const { + return object.*Member; + } + template + constexpr void write(object_type& object, Value&& value) const requires writable && std::assignable_from { + object.*Member = std::forward(value); + } +}; +template +struct Synchronized_Computed_Accessor { + using object_type = Object; + using value_type = Value; + using storage_identity = void; + static constexpr bool readable = true; + static constexpr bool writable = false; + static constexpr bool synchronized_view_read = true; + static constexpr bool trusted_object_access = false; + [[no_unique_address]] Function function; + template + constexpr value_type read(const View& view) const requires std::invocable && std::constructible_from> { + return value_type(std::invoke(function, view)); + } +}; +template requires Trusted_Getter_Function +struct Trusted_Computed_Accessor { + using object_type = trusted_getter_object_t; + using value_type = trusted_getter_value_t; + using storage_identity = void; + static constexpr bool readable = true; + static constexpr bool writable = false; + static constexpr bool synchronized_view_read = false; + static constexpr bool trusted_object_access = true; + constexpr decltype(auto) read(const object_type& object) const noexcept(std::is_nothrow_invocable_v) { + return std::invoke(Getter, object); + } +}; +template requires Trusted_Getter_Function && Trusted_Setter_Function_For, trusted_getter_value_t> +struct Trusted_Getter_Setter_Accessor { + using object_type = trusted_getter_object_t; + using value_type = trusted_getter_value_t; + using storage_identity = void; + static constexpr bool readable = true; + static constexpr bool writable = true; + static constexpr bool synchronized_view_read = false; + static constexpr bool trusted_object_access = true; + constexpr decltype(auto) read(const object_type& object) const noexcept(std::is_nothrow_invocable_v) { + return std::invoke(Getter, object); + } + template + constexpr void write(object_type& object, Value&& value) const requires std::invocable && std::same_as, void> { + std::invoke(Setter, object, std::forward(value)); + } +}; +template +concept Property_Accessor = requires { + typename Accessor::object_type; + typename Accessor::value_type; + typename Accessor::storage_identity; + { Accessor::readable } -> std::convertible_to; + { Accessor::writable } -> std::convertible_to; + { Accessor::synchronized_view_read } -> std::convertible_to; + { Accessor::trusted_object_access } -> std::convertible_to; +}; +} diff --git a/core/include/structive/property/attributes.hpp b/core/include/structive/property/attributes.hpp new file mode 100644 index 0000000..2a43171 --- /dev/null +++ b/core/include/structive/property/attributes.hpp @@ -0,0 +1,133 @@ +#pragma once +#include "fixed_string.hpp" +#include +#include +#include +#include +#include +namespace structive { +struct Key_Category {}; +struct Access_Category {}; +struct Persistence_Category {}; +struct Unit_Category {}; +struct Sensitive_Category {}; +enum class External_Access { + none, + read, + write, + read_write +}; +enum class Persistence_Access { + none, + load, + store, + load_store +}; +template +struct Key_Attribute { + using attribute_category = Key_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = false; + static constexpr auto value = Value; +}; +template +struct External_Access_Attribute { + using attribute_category = Access_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = true; + static constexpr auto value = Value; +}; +template +struct Persistence_Access_Attribute { + using attribute_category = Persistence_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = true; + static constexpr auto value = Value; +}; +template +struct Unit_Attribute { + using attribute_category = Unit_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = false; + static constexpr auto value = Value; +}; +template +struct Sensitive_Attribute { + using attribute_category = Sensitive_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = true; + static constexpr auto value = Value; +}; +template +struct Min_Constraint { + using property_constraint_tag = void; + static constexpr bool inheritable = false; + static constexpr auto code = Fixed_String{"min_value"}; + template + constexpr bool validate(const T& candidate) const requires requires { candidate >= Value; } { + return candidate >= Value; + } +}; +template +struct Max_Constraint { + using property_constraint_tag = void; + static constexpr bool inheritable = false; + static constexpr auto code = Fixed_String{"max_value"}; + template + constexpr bool validate(const T& candidate) const requires requires { candidate <= Value; } { + return candidate <= Value; + } +}; +struct Finite_Constraint { + using property_constraint_tag = void; + static constexpr bool inheritable = false; + static constexpr auto code = Fixed_String{"finite"}; + template + bool validate(T candidate) const { + return std::isfinite(candidate); + } +}; +template +struct Custom_Constraint { + using property_constraint_tag = void; + static constexpr bool inheritable = false; + static constexpr auto code = Code; + [[no_unique_address]] Function function; + template + constexpr bool validate(const T& candidate) const requires std::predicate { + return static_cast(function(candidate)); + } +}; +template +inline constexpr Key_Attribute key{}; +template +inline constexpr External_Access_Attribute external_access{}; +template +inline constexpr Persistence_Access_Attribute persistence_access{}; +template +inline constexpr Unit_Attribute unit{}; +template +inline constexpr Sensitive_Attribute sensitive{}; +template +inline constexpr Min_Constraint min_value{}; +template +inline constexpr Max_Constraint max_value{}; +inline constexpr Finite_Constraint finite{}; +template +constexpr auto constraint(Function&& function) { + return Custom_Constraint>{std::forward(function)}; +} +template +concept Property_Constraint = requires { + typename Attribute::property_constraint_tag; + { Attribute::code.view() } -> std::convertible_to; +}; +template +concept Property_Constraint_For = Property_Constraint && requires(const Attribute& attribute, const Value& value) { + { attribute.validate(value) } -> std::convertible_to; +}; +template +concept Inheritable_Attribute = requires { + { Attribute::inheritable } -> std::convertible_to; +} && Attribute::inheritable; +} diff --git a/core/include/structive/property/descriptor.hpp b/core/include/structive/property/descriptor.hpp new file mode 100644 index 0000000..030eee4 --- /dev/null +++ b/core/include/structive/property/descriptor.hpp @@ -0,0 +1,101 @@ +#pragma once +#include "accessor.hpp" +#include "attributes.hpp" +#include "meta.hpp" +#include +#include +#include +#include +#include +namespace structive { +template +consteval bool constraints_compatible() { + return (([] { + if constexpr (Property_Constraint) { + return Property_Constraint_For; + } + return true; + }()) && ...); +} +template +struct Property_Descriptor { + using property_descriptor_tag = void; + using accessor_type = Accessor; + using object_type = typename Accessor::object_type; + using value_type = typename Accessor::value_type; + using storage_identity = typename Accessor::storage_identity; + using attribute_types = Type_List; + static constexpr bool readable = Accessor::readable; + static constexpr bool writable = Accessor::writable; + static constexpr bool synchronized_view_read = Accessor::synchronized_view_read; + static constexpr bool trusted_object_access = Accessor::trusted_object_access; + static_assert(unique_single_value_categories()); + using key_type = find_attribute_in_list_t; + static_assert(!std::same_as); + static_assert(key_type::value.view().size() > 0); + static_assert(constraints_compatible()); + [[no_unique_address]] Accessor accessor{}; + std::tuple attributes; + template + using attribute_type = find_attribute_in_list_t; + template + static constexpr bool has_attribute = !std::same_as, void>; + template + constexpr decltype(auto) attribute() const requires has_attribute { + using target = attribute_type; + return std::get(attributes); + } + constexpr std::string_view key() const noexcept { + return key_type::value.view(); + } + template + constexpr void for_each_attribute(Function&& function) const { + std::apply([&](const auto&... values) { + (function(values), ...); + }, attributes); + } + template + constexpr void for_each_constraint(Function&& function) const { + std::apply([&](const auto&... values) { + ([&] { + using type = std::remove_cvref_t; + if constexpr (Property_Constraint) { + function(values); + } + }(), ...); + }, attributes); + } +}; +template +concept Property_Descriptor_Type = requires { + typename Type::property_descriptor_tag; + typename Type::object_type; + typename Type::value_type; + typename Type::accessor_type; + typename Type::storage_identity; +}; +template +constexpr auto property(Attributes&&... attributes) { + using accessor = Member_Accessor; + return Property_Descriptor...>{{}, {std::forward(attributes)...}}; +} +template +constexpr auto field(Attributes&&... attributes) { + return property(std::forward(attributes)...); +} +template +constexpr auto computed_property(Function&& function, Attributes&&... attributes) { + using accessor = Synchronized_Computed_Accessor>; + return Property_Descriptor...>{accessor{std::forward(function)}, {std::forward(attributes)...}}; +} +template requires Trusted_Getter_Function +constexpr auto trusted_computed_property(Attributes&&... attributes) { + using accessor = Trusted_Computed_Accessor; + return Property_Descriptor...>{{}, {std::forward(attributes)...}}; +} +template requires Trusted_Getter_Function && Trusted_Setter_Function_For, trusted_getter_value_t> +constexpr auto trusted_accessor_property(Attributes&&... attributes) { + using accessor = Trusted_Getter_Setter_Accessor; + return Property_Descriptor...>{{}, {std::forward(attributes)...}}; +} +} diff --git a/core/include/structive/property/fixed_string.hpp b/core/include/structive/property/fixed_string.hpp new file mode 100644 index 0000000..f1c9b40 --- /dev/null +++ b/core/include/structive/property/fixed_string.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include +namespace structive { +template +struct Fixed_String { + char value[N]{}; + consteval Fixed_String(const char (&text)[N]) { + for (std::size_t i = 0; i < N; ++i) { + value[i] = text[i]; + } + } + constexpr std::string_view view() const { + return {value, N - 1}; + } + constexpr auto operator<=>(const Fixed_String&) const = default; +}; +} diff --git a/core/include/structive/property/meta.hpp b/core/include/structive/property/meta.hpp new file mode 100644 index 0000000..b2688a8 --- /dev/null +++ b/core/include/structive/property/meta.hpp @@ -0,0 +1,85 @@ +#pragma once +#include +#include +#include +#include +namespace structive { +template +struct Type_List {}; +template +struct Type_List_Contains; +template +struct Type_List_Contains> : std::bool_constant<(std::same_as || ...)> {}; +template +inline constexpr bool type_list_contains_v = Type_List_Contains::value; +template +struct Type_List_Push_Unique; +template +struct Type_List_Push_Unique, Type> { + using type = std::conditional_t<(std::same_as || ...), Type_List, Type_List>; +}; +template > +struct Type_List_Unique; +template +struct Type_List_Unique, Output> { + using type = Output; +}; +template +struct Type_List_Unique, Output> { + using next = typename Type_List_Push_Unique::type; + using type = typename Type_List_Unique, next>::type; +}; +template +struct Type_List_Index; +template +struct Type_List_Index> : std::integral_constant {}; +template +struct Type_List_Index> : std::integral_constant>::value> {}; +template +inline constexpr std::size_t type_list_index_v = Type_List_Index::value; +template +struct Attribute_Category_Of { + using type = void; +}; +template +struct Attribute_Category_Of> { + using type = typename Attribute::attribute_category; +}; +template +using attribute_category_of_t = typename Attribute_Category_Of::type; +template +struct Find_Attribute_By_Category; +template +struct Find_Attribute_By_Category { + using type = void; +}; +template +struct Find_Attribute_By_Category { + using type = std::conditional_t>, Head, typename Find_Attribute_By_Category::type>; +}; +template +struct Find_Attribute_In_List; +template +struct Find_Attribute_In_List> { + using type = typename Find_Attribute_By_Category::type; +}; +template +using find_attribute_in_list_t = typename Find_Attribute_In_List::type; +template +consteval std::size_t count_attribute_category() { + return (std::size_t{0} + ... + (std::same_as, Category> ? 1u : 0u)); +} +template +consteval bool unique_single_value_category_for() { + if constexpr (requires { Attribute::single_valued; }) { + if constexpr (Attribute::single_valued && !std::same_as, void>) { + return count_attribute_category, Attributes...>() == 1; + } + } + return true; +} +template +consteval bool unique_single_value_categories() { + return (unique_single_value_category_for() && ...); +} +} diff --git a/core/include/structive/property/property.hpp b/core/include/structive/property/property.hpp new file mode 100644 index 0000000..099c030 --- /dev/null +++ b/core/include/structive/property/property.hpp @@ -0,0 +1,11 @@ +#pragma once +#include "accessor.hpp" +#include "attributes.hpp" +#include "descriptor.hpp" +#include "fixed_string.hpp" +#include "meta.hpp" +#include "schema.hpp" +#include "synchronization.hpp" +#include "type_descriptor.hpp" +#include "property_object.hpp" +#include "validation.hpp" diff --git a/core/include/structive/property/property_object.hpp b/core/include/structive/property/property_object.hpp new file mode 100644 index 0000000..3111758 --- /dev/null +++ b/core/include/structive/property/property_object.hpp @@ -0,0 +1,963 @@ +#pragma once +#include "type_descriptor.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace structive { +struct Null_Shared_Mutex { + void lock() {} + void unlock() {} + void lock_shared() {} + void unlock_shared() {} +}; +struct Shared_Mutex_Policy { + using mutex_type = std::shared_mutex; +}; +struct No_Lock_Policy { + using mutex_type = Null_Shared_Mutex; +}; +template +concept Shared_Lockable = requires(Mutex& mutex) { + mutex.lock(); + mutex.unlock(); + mutex.lock_shared(); + mutex.unlock_shared(); +}; +template +concept Synchronization_Policy = requires { + typename Policy::mutex_type; +} && Shared_Lockable; +enum class Managed_Access_Mode { + internal, + external, + persistence +}; +enum class Runtime_Access_Result { + ok, + unknown_property, + not_readable, + not_writable, + type_mismatch +}; +using Runtime_Read_Callback = void (*)(void*, std::size_t, std::string_view, const std::type_info&, const void*); +class Property_Object_Base { + struct Runtime_Interface { + const std::type_info& (*object_type)() noexcept; + std::size_t (*property_count)() noexcept; + Runtime_Access_Result (*read)(const Property_Object_Base&, Managed_Access_Mode, std::string_view, void*, Runtime_Read_Callback); + Runtime_Access_Result (*write)(Property_Object_Base&, Managed_Access_Mode, std::string_view, const std::type_info&, const void*); + }; + const Runtime_Interface* runtime_interface_{}; +protected: + explicit Property_Object_Base(const Runtime_Interface* runtime_interface) noexcept : runtime_interface_(runtime_interface) {} + Property_Object_Base(const Property_Object_Base&) noexcept = default; + Property_Object_Base(Property_Object_Base&&) noexcept = default; + Property_Object_Base& operator=(const Property_Object_Base&) noexcept = default; + Property_Object_Base& operator=(Property_Object_Base&&) noexcept = default; + ~Property_Object_Base() = default; + template + friend class Property_Object; +public: + const std::type_info& runtime_object_type() const noexcept { + return runtime_interface_->object_type(); + } + std::size_t runtime_property_count() const noexcept { + return runtime_interface_->property_count(); + } + Runtime_Access_Result runtime_read(Managed_Access_Mode mode, std::string_view key, void* context, Runtime_Read_Callback callback) const { + return runtime_interface_->read(*this, mode, key, context, callback); + } + Runtime_Access_Result runtime_write(Managed_Access_Mode mode, std::string_view key, const std::type_info& value_type, const void* value) { + return runtime_interface_->write(*this, mode, key, value_type, value); + } +}; +template +inline constexpr bool property_read_allowed_v = Mode == Managed_Access_Mode::internal ? Schema::template property_type::readable : Mode == Managed_Access_Mode::external ? external_readable_v : persistence_storable_v; +template +inline constexpr bool property_write_allowed_v = Mode == Managed_Access_Mode::internal ? Schema::template property_type::writable : Mode == Managed_Access_Mode::external ? external_writable_v : persistence_loadable_v; +template +inline constexpr bool property_visible_v = property_read_allowed_v || property_write_allowed_v; +struct Property_Synchronization { + Synchronization_Plan plan; +}; +inline Property_Synchronization property_synchronization(Synchronization_Plan plan) { + return {std::move(plan)}; +} +template +Property_Synchronization property_synchronization(Source&& source) requires Property_Described_Object { + using Schema = type_descriptor_schema_t; + return {materialize_synchronization_plan(std::forward(source))}; +} +struct Resolved_Synchronization_View { + static constexpr std::size_t unsynchronized_slot = std::numeric_limits::max(); + std::span lock_slots; + std::size_t lock_count{}; + std::size_t slot(std::size_t index) const noexcept { + return lock_slots[index]; + } + bool uses_lock(std::size_t index) const noexcept { + return slot(index) != unsynchronized_slot; + } +}; +template +class Property_Object : public Property_Object_Base { +public: + using object_type = Derived; + using mutex_type = typename Lock_Policy::mutex_type; +private: + struct Dynamic_Lock_Targets { + std::vector slots; + std::vector unsynchronized_properties; + }; + template + struct Static_Lock_Targets { + std::array slots{}; + std::size_t slot_count{}; + std::array unsynchronized_properties{}; + std::size_t unsynchronized_count{}; + }; + std::vector lock_slots_; + std::size_t lock_count_{}; + std::unique_ptr locks_; + template + void initialize(const Schema& schema, const Synchronization_Plan& plan) { + auto resolved = resolve_synchronization_plan(schema, plan); + lock_slots_.assign(resolved.lock_slots.begin(), resolved.lock_slots.end()); + lock_count_ = resolved.lock_count; + locks_ = lock_count_ ? std::make_unique(lock_count_) : nullptr; + } + void copy_synchronization_from(const Property_Object& other) { + lock_slots_ = other.lock_slots_; + lock_count_ = other.lock_count_; + locks_ = lock_count_ ? std::make_unique(lock_count_) : nullptr; + } + std::size_t slot(std::size_t index) const noexcept { + return lock_slots_[index]; + } + template + static void sort_unique_prefix(std::array& values, std::size_t& count) { + for (std::size_t index = 1; index < count; ++index) { + auto value = values[index]; + auto position = index; + while (position > 0 && value < values[position - 1]) { + values[position] = values[position - 1]; + --position; + } + values[position] = value; + } + if (count == 0) { + return; + } + std::size_t write = 1; + for (std::size_t read = 1; read < count; ++read) { + if (values[read] != values[write - 1]) { + values[write++] = values[read]; + } + } + count = write; + } + template + static bool runtime_property_readable(std::size_t index) { + using Schema = type_descriptor_schema_t; + static const auto table = [](std::index_sequence) { + return std::array{property_read_allowed_v...}; + }(std::make_index_sequence{}); + return table[index]; + } + template + static bool runtime_property_visible(std::size_t index) { + using Schema = type_descriptor_schema_t; + static const auto table = [](std::index_sequence) { + return std::array{property_visible_v...}; + }(std::make_index_sequence{}); + return table[index]; + } + template + Dynamic_Lock_Targets collect_dynamic_lock_targets(std::span keys, bool shared_access) const { + using Schema = type_descriptor_schema_t; + Dynamic_Lock_Targets targets; + targets.slots.reserve(keys.size()); + targets.unsynchronized_properties.reserve(keys.size()); + for (auto key : keys) { + auto index = schema_property_index(key); + if (!index) { + throw std::invalid_argument("Unknown property: " + std::string(key)); + } + bool allowed = shared_access ? runtime_property_readable(*index) : runtime_property_visible(*index); + if (!allowed) { + throw std::invalid_argument("Property is not accessible through this view: " + std::string(key)); + } + auto lock_slot = slot(*index); + if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { + targets.unsynchronized_properties.push_back(*index); + } else { + targets.slots.push_back(lock_slot); + } + } + std::sort(targets.slots.begin(), targets.slots.end()); + targets.slots.erase(std::unique(targets.slots.begin(), targets.slots.end()), targets.slots.end()); + std::sort(targets.unsynchronized_properties.begin(), targets.unsynchronized_properties.end()); + targets.unsynchronized_properties.erase(std::unique(targets.unsynchronized_properties.begin(), targets.unsynchronized_properties.end()), targets.unsynchronized_properties.end()); + return targets; + } + template + Static_Lock_Targets collect_static_lock_targets() const { + using Schema = type_descriptor_schema_t; + static_assert(((Shared_Access ? property_read_allowed_v : property_visible_v) && ...)); + Static_Lock_Targets targets; + auto collect = [&]() { + auto lock_slot = slot(Property_Index); + if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { + targets.unsynchronized_properties[targets.unsynchronized_count++] = Property_Index; + } else { + targets.slots[targets.slot_count++] = lock_slot; + } + }; + (collect.template operator()(), ...); + sort_unique_prefix(targets.slots, targets.slot_count); + sort_unique_prefix(targets.unsynchronized_properties, targets.unsynchronized_count); + return targets; + } + template + auto collect_all_static_lock_targets() const { + using Schema = type_descriptor_schema_t; + Static_Lock_Targets targets; + auto collect = [&]() { + constexpr bool allowed = Shared_Access ? property_read_allowed_v : property_write_allowed_v; + if constexpr (allowed) { + auto lock_slot = slot(Index); + if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { + targets.unsynchronized_properties[targets.unsynchronized_count++] = Index; + } else { + targets.slots[targets.slot_count++] = lock_slot; + } + } + }; + [&](std::index_sequence) { + (collect.template operator()(), ...); + }(std::make_index_sequence{}); + sort_unique_prefix(targets.slots, targets.slot_count); + sort_unique_prefix(targets.unsynchronized_properties, targets.unsynchronized_count); + return targets; + } + template + decltype(auto) read_unlocked(const View& view) const { + using Schema = type_descriptor_schema_t; + const auto& descriptor = type_descriptor().template property(); + using Accessor = typename Schema::template property_type::accessor_type; + if constexpr (Accessor::synchronized_view_read) { + return descriptor.accessor.read(view); + } else { + return descriptor.accessor.read(static_cast(*this)); + } + } + template + void write_unlocked(Value&& value) { + const auto& descriptor = type_descriptor().template property(); + descriptor.accessor.write(static_cast(*this), std::forward(value)); + } + template + class Single_Read_View { + const Property_Object* owner_{}; + std::size_t property_index_{}; + std::size_t lock_slot_{}; + public: + Single_Read_View(const Property_Object& owner, std::size_t property_index, std::size_t lock_slot) : owner_(&owner), property_index_(property_index), lock_slot_(lock_slot) {} + template + decltype(auto) get() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + if (owner_->slot(index) != lock_slot_ || (lock_slot_ == Resolved_Synchronization_View::unsynchronized_slot && index != property_index_)) { + throw std::logic_error("Computed property reads outside its synchronization slot"); + } + return owner_->template read_unlocked(*this); + } + template + decltype(auto) get_key() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + if (owner_->slot(index) != lock_slot_ || (lock_slot_ == Resolved_Synchronization_View::unsynchronized_slot && index != property_index_)) { + throw std::logic_error("Computed property reads outside its synchronization slot"); + } + return owner_->template read_unlocked(*this); + } + }; + template + auto read_one() const { + using Schema = type_descriptor_schema_t; + static_assert(property_read_allowed_v); + using Value = typename Schema::template property_type::value_type; + auto lock_slot = slot(Index); + Single_Read_View view{*this, Index, lock_slot}; + if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { + return Value(read_unlocked(view)); + } + std::shared_lock lock{locks_[lock_slot]}; + return Value(read_unlocked(view)); + } + template + void write_one(Value&& value) { + using Schema = type_descriptor_schema_t; + static_assert(property_write_allowed_v); + auto lock_slot = slot(Index); + if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { + write_unlocked(std::forward(value)); + return; + } + std::unique_lock lock{locks_[lock_slot]}; + write_unlocked(std::forward(value)); + } +public: + template + class Read_Guard { + const Property_Object* owner_{}; + std::vector slots_; + std::vector unsynchronized_properties_; + std::vector> locks_; + bool holds(std::size_t index) const { + auto lock_slot = owner_->slot(index); + if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { + return std::binary_search(unsynchronized_properties_.begin(), unsynchronized_properties_.end(), index); + } + return std::binary_search(slots_.begin(), slots_.end(), lock_slot); + } + friend class Property_Object; + Read_Guard(const Property_Object& owner, Dynamic_Lock_Targets targets) : owner_(&owner), slots_(std::move(targets.slots)), unsynchronized_properties_(std::move(targets.unsynchronized_properties)) { + locks_.reserve(slots_.size()); + for (auto lock_slot : slots_) { + locks_.emplace_back(owner_->locks_[lock_slot]); + } + } + public: + template + decltype(auto) get_index() const { + using Schema = type_descriptor_schema_t; + static_assert(Index < Schema::property_count); + static_assert(property_read_allowed_v); + if (!holds(Index)) { + throw std::logic_error("Property is outside the held synchronization set"); + } + return owner_->template read_unlocked(*this); + } + template + decltype(auto) get() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + return get_index(); + } + template + decltype(auto) get_key() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + return get_index(); + } + }; + template + class Write_Guard { + Property_Object* owner_{}; + std::vector slots_; + std::vector unsynchronized_properties_; + std::vector> locks_; + bool holds(std::size_t index) const { + auto lock_slot = owner_->slot(index); + if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { + return std::binary_search(unsynchronized_properties_.begin(), unsynchronized_properties_.end(), index); + } + return std::binary_search(slots_.begin(), slots_.end(), lock_slot); + } + friend class Property_Object; + Write_Guard(Property_Object& owner, Dynamic_Lock_Targets targets) : owner_(&owner), slots_(std::move(targets.slots)), unsynchronized_properties_(std::move(targets.unsynchronized_properties)) { + locks_.reserve(slots_.size()); + for (auto lock_slot : slots_) { + locks_.emplace_back(owner_->locks_[lock_slot]); + } + } + public: + template + decltype(auto) get_index() const { + using Schema = type_descriptor_schema_t; + static_assert(Index < Schema::property_count); + static_assert(property_read_allowed_v); + if (!holds(Index)) { + throw std::logic_error("Property is outside the held synchronization set"); + } + return owner_->template read_unlocked(*this); + } + template + decltype(auto) get() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + return get_index(); + } + template + decltype(auto) get_key() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + return get_index(); + } + template + void set_index(Value&& value) { + using Schema = type_descriptor_schema_t; + static_assert(Index < Schema::property_count); + static_assert(property_write_allowed_v); + if (!holds(Index)) { + throw std::logic_error("Property is outside the held synchronization set"); + } + owner_->template write_unlocked(std::forward(value)); + } + template + void set(Value&& value) { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + set_index(std::forward(value)); + } + template + void set_key(Value&& value) { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + set_index(std::forward(value)); + } + }; + template + class Static_Read_Guard { + const Property_Object* owner_{}; + Static_Lock_Targets targets_; + std::array, Capacity> locks_{}; + bool holds(std::size_t index) const { + auto lock_slot = owner_->slot(index); + if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { + return std::binary_search(targets_.unsynchronized_properties.begin(), targets_.unsynchronized_properties.begin() + static_cast(targets_.unsynchronized_count), index); + } + return std::binary_search(targets_.slots.begin(), targets_.slots.begin() + static_cast(targets_.slot_count), lock_slot); + } + friend class Property_Object; + Static_Read_Guard(const Property_Object& owner, Static_Lock_Targets targets) : owner_(&owner), targets_(std::move(targets)) { + for (std::size_t index = 0; index < targets_.slot_count; ++index) { + locks_[index] = std::shared_lock{owner_->locks_[targets_.slots[index]]}; + } + } + public: + template + decltype(auto) get_index() const { + using Schema = type_descriptor_schema_t; + static_assert(Index < Schema::property_count); + static_assert(property_read_allowed_v); + if (!holds(Index)) { + throw std::logic_error("Property is outside the held synchronization set"); + } + return owner_->template read_unlocked(*this); + } + template + decltype(auto) get() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + return get_index(); + } + template + decltype(auto) get_key() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + return get_index(); + } + }; + template + class Static_Write_Guard { + Property_Object* owner_{}; + Static_Lock_Targets targets_; + std::array, Capacity> locks_{}; + bool holds(std::size_t index) const { + auto lock_slot = owner_->slot(index); + if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { + return std::binary_search(targets_.unsynchronized_properties.begin(), targets_.unsynchronized_properties.begin() + static_cast(targets_.unsynchronized_count), index); + } + return std::binary_search(targets_.slots.begin(), targets_.slots.begin() + static_cast(targets_.slot_count), lock_slot); + } + friend class Property_Object; + Static_Write_Guard(Property_Object& owner, Static_Lock_Targets targets) : owner_(&owner), targets_(std::move(targets)) { + for (std::size_t index = 0; index < targets_.slot_count; ++index) { + locks_[index] = std::unique_lock{owner_->locks_[targets_.slots[index]]}; + } + } + public: + template + decltype(auto) get_index() const { + using Schema = type_descriptor_schema_t; + static_assert(Index < Schema::property_count); + static_assert(property_read_allowed_v); + if (!holds(Index)) { + throw std::logic_error("Property is outside the held synchronization set"); + } + return owner_->template read_unlocked(*this); + } + template + decltype(auto) get() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + return get_index(); + } + template + decltype(auto) get_key() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + return get_index(); + } + template + void set_index(Value&& value) { + using Schema = type_descriptor_schema_t; + static_assert(Index < Schema::property_count); + static_assert(property_write_allowed_v); + if (!holds(Index)) { + throw std::logic_error("Property is outside the held synchronization set"); + } + owner_->template write_unlocked(std::forward(value)); + } + template + void set(Value&& value) { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + set_index(std::forward(value)); + } + template + void set_key(Value&& value) { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + set_index(std::forward(value)); + } + }; +private: + template + void for_each_readable_impl(Function&& function) const { + using Schema = type_descriptor_schema_t; + const auto& schema = type_descriptor(); + schema.for_each_property([&](auto index, const auto& descriptor) { + constexpr std::size_t property_index = decltype(index)::value; + if constexpr (property_read_allowed_v) { + auto value = read_one(); + std::invoke(function, index, descriptor, value); + } + }); + } + template + void for_each_readable_locked_impl(Function&& function) const { + using Schema = type_descriptor_schema_t; + auto targets = collect_all_static_lock_targets(); + Static_Read_Guard guard{*this, std::move(targets)}; + type_descriptor().for_each_property([&](auto index, const auto& descriptor) { + constexpr std::size_t property_index = decltype(index)::value; + if constexpr (property_read_allowed_v) { + std::invoke(function, index, descriptor, guard.template get_index()); + } + }); + } + template + void with_all_writable_locked_impl(Function&& function) { + using Schema = type_descriptor_schema_t; + auto targets = collect_all_static_lock_targets(); + Static_Write_Guard guard{*this, std::move(targets)}; + std::invoke(std::forward(function), guard); + } + static const std::type_info& runtime_object_type_impl() noexcept { + return typeid(Derived); + } + static std::size_t runtime_property_count_impl() noexcept { + using Schema = type_descriptor_schema_t; + return Schema::property_count; + } + template + Runtime_Access_Result runtime_read_mode(std::string_view key, void* context, Runtime_Read_Callback callback) const { + using Schema = type_descriptor_schema_t; + auto index = schema_property_index(key); + if (!index) { + return Runtime_Access_Result::unknown_property; + } + if (!runtime_property_readable(*index)) { + return Runtime_Access_Result::not_readable; + } + Runtime_Access_Result result = Runtime_Access_Result::unknown_property; + visit_schema_property(type_descriptor(), key, [&](auto property_index_constant, const auto&) { + constexpr std::size_t property_index = decltype(property_index_constant)::value; + using Value = typename Schema::template property_type::value_type; + auto lock_slot = slot(property_index); + Single_Read_View view{*this, property_index, lock_slot}; + auto emit = [&] { + if constexpr (std::is_reference_v(view))>) { + auto&& value = read_unlocked(view); + callback(context, property_index, key, typeid(Value), std::addressof(value)); + } else { + Value value = read_unlocked(view); + callback(context, property_index, key, typeid(Value), std::addressof(value)); + } + }; + if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { + emit(); + } else { + std::shared_lock lock{locks_[lock_slot]}; + emit(); + } + result = Runtime_Access_Result::ok; + }); + return result; + } + static Runtime_Access_Result runtime_read_impl(const Property_Object_Base& base, Managed_Access_Mode mode, std::string_view key, void* context, Runtime_Read_Callback callback) { + const auto& self = static_cast(base); + switch (mode) { + case Managed_Access_Mode::internal: + return self.template runtime_read_mode(key, context, callback); + case Managed_Access_Mode::external: + return self.template runtime_read_mode(key, context, callback); + case Managed_Access_Mode::persistence: + return self.template runtime_read_mode(key, context, callback); + } + return Runtime_Access_Result::not_readable; + } + template + Runtime_Access_Result runtime_write_mode(std::string_view key, const std::type_info& value_type, const void* value) { + using Schema = type_descriptor_schema_t; + auto index = schema_property_index(key); + if (!index) { + return Runtime_Access_Result::unknown_property; + } + if (!runtime_property_visible(*index)) { + return Runtime_Access_Result::not_writable; + } + Runtime_Access_Result result = Runtime_Access_Result::unknown_property; + visit_schema_property(type_descriptor(), key, [&](auto property_index_constant, const auto&) { + constexpr std::size_t property_index = decltype(property_index_constant)::value; + using Property = typename Schema::template property_type; + using Accessor = typename Property::accessor_type; + using Value = typename Property::value_type; + if constexpr (!property_write_allowed_v || !requires(const Accessor& accessor, Derived& object, const Value& candidate) { accessor.write(object, candidate); }) { + result = Runtime_Access_Result::not_writable; + } else if (value_type != typeid(Value)) { + result = Runtime_Access_Result::type_mismatch; + } else { + write_one(*static_cast(value)); + result = Runtime_Access_Result::ok; + } + }); + return result; + } + static Runtime_Access_Result runtime_write_impl(Property_Object_Base& base, Managed_Access_Mode mode, std::string_view key, const std::type_info& value_type, const void* value) { + auto& self = static_cast(base); + switch (mode) { + case Managed_Access_Mode::internal: + return self.template runtime_write_mode(key, value_type, value); + case Managed_Access_Mode::external: + return self.template runtime_write_mode(key, value_type, value); + case Managed_Access_Mode::persistence: + return self.template runtime_write_mode(key, value_type, value); + } + return Runtime_Access_Result::not_writable; + } + static const Property_Object_Base::Runtime_Interface* runtime_interface() noexcept { + static const Property_Object_Base::Runtime_Interface value{ + &runtime_object_type_impl, + &runtime_property_count_impl, + &runtime_read_impl, + &runtime_write_impl + }; + return &value; + } +protected: + Property_Object() : Property_Object_Base(runtime_interface()) { + const auto& schema = type_descriptor(); + initialize(schema, schema.synchronization_plan()); + } + explicit Property_Object(Property_Synchronization synchronization) : Property_Object_Base(runtime_interface()) { + initialize(type_descriptor(), synchronization.plan); + } + Property_Object(const Property_Object& other) : Property_Object_Base(runtime_interface()) { + copy_synchronization_from(other); + } + Property_Object(Property_Object&& other) : Property_Object_Base(runtime_interface()) { + copy_synchronization_from(other); + } + Property_Object& operator=(const Property_Object&) noexcept { + return *this; + } + Property_Object& operator=(Property_Object&&) noexcept { + return *this; + } + ~Property_Object() = default; +public: + const auto& schema() const noexcept { + return type_descriptor(); + } + Resolved_Synchronization_View resolved_synchronization() const noexcept { + return {lock_slots_, lock_count_}; + } + Derived& unsafe_object() noexcept { + return static_cast(*this); + } + const Derived& unsafe_object() const noexcept { + return static_cast(*this); + } + template + auto read() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + return read_one(); + } + template + auto read_key() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + return read_one(); + } + template + void write(Value&& value) { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + write_one(std::forward(value)); + } + template + void write_key(Value&& value) { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + write_one(std::forward(value)); + } + template + std::size_t lock_slot() const noexcept { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + return slot(index); + } + template + Read_Guard lock_shared(std::span keys) const { + return Read_Guard{*this, collect_dynamic_lock_targets(keys, true)}; + } + template + Read_Guard lock_shared(std::initializer_list keys) const { + return lock_shared(std::span{keys.begin(), keys.size()}); + } + Read_Guard lock_shared(std::span keys) const { + return lock_shared(keys); + } + Read_Guard lock_shared(std::initializer_list keys) const { + return lock_shared(keys); + } + template + Write_Guard lock_unique(std::span keys) { + return Write_Guard{*this, collect_dynamic_lock_targets(keys, false)}; + } + template + Write_Guard lock_unique(std::initializer_list keys) { + return lock_unique(std::span{keys.begin(), keys.size()}); + } + Write_Guard lock_unique(std::span keys) { + return lock_unique(keys); + } + Write_Guard lock_unique(std::initializer_list keys) { + return lock_unique(keys); + } + template + auto lock_shared_mode() const { + using Schema = type_descriptor_schema_t; + static_assert((Schema_Property_Member && ...)); + auto targets = collect_static_lock_targets...>(); + return Static_Read_Guard{*this, std::move(targets)}; + } + template + auto lock_shared() const { + return lock_shared_mode(); + } + template + auto lock_unique_mode() { + using Schema = type_descriptor_schema_t; + static_assert((Schema_Property_Member && ...)); + auto targets = collect_static_lock_targets...>(); + return Static_Write_Guard{*this, std::move(targets)}; + } + template + auto lock_unique() { + return lock_unique_mode(); + } + template + void for_each_readable(Function&& function) const { + for_each_readable_impl(std::forward(function)); + } + template + void for_each_readable_locked(Function&& function) const { + for_each_readable_locked_impl(std::forward(function)); + } + template + void with_all_writable_locked(Function&& function) { + with_all_writable_locked_impl(std::forward(function)); + } + template + class Capability_View { + Property_Object* owner_{}; + public: + explicit Capability_View(Property_Object& owner) : owner_(&owner) {} + template + auto read() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + return owner_->template read_one(); + } + template + auto read_key() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + return owner_->template read_one(); + } + template + void write(Value&& value) requires (Mode != Managed_Access_Mode::persistence) { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + owner_->template write_one(std::forward(value)); + } + template + void write_key(Value&& value) requires (Mode != Managed_Access_Mode::persistence) { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + owner_->template write_one(std::forward(value)); + } + template + void load(Value&& value) requires (Mode == Managed_Access_Mode::persistence) { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + owner_->template write_one(std::forward(value)); + } + template + void load_key(Value&& value) requires (Mode == Managed_Access_Mode::persistence) { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + owner_->template write_one(std::forward(value)); + } + template + auto store() const requires (Mode == Managed_Access_Mode::persistence) { + return read(); + } + template + auto store_key() const requires (Mode == Managed_Access_Mode::persistence) { + return read_key(); + } + template + auto lock_shared() const { + return owner_->template lock_shared_mode(); + } + template + auto lock_unique() { + return owner_->template lock_unique_mode(); + } + Read_Guard lock_shared(std::span keys) const { + return owner_->template lock_shared(keys); + } + Write_Guard lock_unique(std::span keys) { + return owner_->template lock_unique(keys); + } + template + void for_each_readable(Function&& function) const { + owner_->template for_each_readable_impl(std::forward(function)); + } + template + void for_each_readable_locked(Function&& function) const { + owner_->template for_each_readable_locked_impl(std::forward(function)); + } + template + void with_all_writable_locked(Function&& function) requires (Mode != Managed_Access_Mode::persistence) { + owner_->template with_all_writable_locked_impl(std::forward(function)); + } + template + void with_all_loadable_locked(Function&& function) requires (Mode == Managed_Access_Mode::persistence) { + owner_->template with_all_writable_locked_impl(std::forward(function)); + } + }; + template + class Const_Capability_View { + const Property_Object* owner_{}; + public: + explicit Const_Capability_View(const Property_Object& owner) : owner_(&owner) {} + template + auto read() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_member_property_index_v; + static_assert(index < Schema::property_count); + return owner_->template read_one(); + } + template + auto read_key() const { + using Schema = type_descriptor_schema_t; + constexpr auto index = schema_property_index_v; + static_assert(index < Schema::property_count); + return owner_->template read_one(); + } + template + auto store() const requires (Mode == Managed_Access_Mode::persistence) { + return read(); + } + template + auto store_key() const requires (Mode == Managed_Access_Mode::persistence) { + return read_key(); + } + template + auto lock_shared() const { + return owner_->template lock_shared_mode(); + } + Read_Guard lock_shared(std::span keys) const { + return owner_->template lock_shared(keys); + } + template + void for_each_readable(Function&& function) const { + owner_->template for_each_readable_impl(std::forward(function)); + } + template + void for_each_readable_locked(Function&& function) const { + owner_->template for_each_readable_locked_impl(std::forward(function)); + } + }; + Capability_View external() { + return Capability_View{*this}; + } + Capability_View persistence() { + return Capability_View{*this}; + } + Const_Capability_View external() const { + return Const_Capability_View{*this}; + } + Const_Capability_View persistence() const { + return Const_Capability_View{*this}; + } +}; +} diff --git a/core/include/structive/property/schema.hpp b/core/include/structive/property/schema.hpp new file mode 100644 index 0000000..f4a83ce --- /dev/null +++ b/core/include/structive/property/schema.hpp @@ -0,0 +1,430 @@ +#pragma once +#include "descriptor.hpp" +#include "meta.hpp" +#include "synchronization.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace structive { +template +consteval std::string_view declared_property_key() { + using key_type = typename Property::template attribute_type; + return key_type::value.view(); +} +template +consteval bool unique_property_keys() { + constexpr std::array keys{declared_property_key()...}; + for (std::size_t i = 0; i < keys.size(); ++i) { + for (std::size_t j = i + 1; j < keys.size(); ++j) { + if (keys[i] == keys[j]) { + return false; + } + } + } + return true; +} +template +consteval bool distinct_storage_identity() { + using left = typename Left::storage_identity; + using right = typename Right::storage_identity; + if constexpr (std::same_as || std::same_as) { + return true; + } + return !std::same_as; +} +template +consteval bool distinct_storage_row(std::index_sequence) { + using left = std::tuple_element_t; + return (distinct_storage_identity>() && ...); +} +template +consteval bool unique_property_storage() { + using tuple = std::tuple; + if constexpr (sizeof...(Properties) < 2) { + return true; + } else { + return [](std::index_sequence) { + return (distinct_storage_row(std::make_index_sequence{}) && ...); + }(std::make_index_sequence{}); + } +} +template +struct Object_Defaults { + using object_defaults_tag = void; + using attribute_types = Type_List; + static_assert(unique_single_value_categories()); + static_assert((Inheritable_Attribute && ...)); + std::tuple attributes; +}; +template +concept Object_Defaults_Type = requires { + typename Type::object_defaults_tag; + typename Type::attribute_types; +}; +template +constexpr auto defaults(Attributes&&... attributes) { + return Object_Defaults...>{{std::forward(attributes)...}}; +} +struct No_Defaults { + using object_defaults_tag = void; + using attribute_types = Type_List<>; + std::tuple<> attributes; +}; +inline constexpr No_Defaults no_defaults{}; +template +class Object_Schema { + [[no_unique_address]] Defaults object_defaults_; + Synchronization_Plan synchronization_plan_; + std::tuple properties_; +public: + using property_schema_tag = void; + using object_type = Object; + using defaults_type = Defaults; + using property_types = Type_List; + static constexpr std::size_t property_count = sizeof...(Properties); + static_assert((std::same_as && ...)); + static_assert(unique_property_keys()); + static_assert(unique_property_storage()); + Object_Schema(Defaults object_defaults, Synchronization_Plan synchronization_plan, std::tuple properties) : object_defaults_(std::move(object_defaults)), synchronization_plan_(std::move(synchronization_plan)), properties_(std::move(properties)) {} + Object_Schema(const Object_Schema&) = default; + Object_Schema(Object_Schema&&) noexcept = default; + Object_Schema& operator=(const Object_Schema&) = delete; + Object_Schema& operator=(Object_Schema&&) = delete; + template + using property_type = std::tuple_element_t>; + const Defaults& object_defaults() const noexcept { + return object_defaults_; + } + template + constexpr const auto& property() const { + if constexpr (std::integral) { + static_assert(Selector >= 0); + constexpr std::size_t index = static_cast(Selector); + static_assert(index < property_count); + return std::get(properties_); + } else { + static_assert(std::is_member_object_pointer_v); + using member_traits = Member_Pointer_Traits; + static_assert(std::same_as); + using identity = Member_Storage_Identity; + constexpr auto matches = [](std::index_sequence) { + return std::array{std::same_as>::storage_identity, identity>...}; + }(std::make_index_sequence{}); + constexpr std::size_t index = [matches] { + for (std::size_t value = 0; value < matches.size(); ++value) { + if (matches[value]) { + return value; + } + } + return property_count; + }(); + static_assert(index < property_count, "member is not registered in this property schema"); + return std::get(properties_); + } + } + template + constexpr void for_each_property(Function&& function) const { + [&](std::index_sequence) { + (function(std::integral_constant{}, std::get(properties_)), ...); + }(std::make_index_sequence{}); + } + const Synchronization_Plan& synchronization_plan() const noexcept { + return synchronization_plan_; + } +}; +template +concept Property_Schema = requires { + typename Schema::property_schema_tag; + typename Schema::object_type; + typename Schema::defaults_type; + typename Schema::property_types; + { Schema::property_count } -> std::convertible_to; +}; +template +consteval auto schema_property_keys() { + return [](std::index_sequence) { + return std::array{declared_property_key>()...}; + }(std::make_index_sequence{}); +} +template +constexpr std::optional schema_property_index(std::string_view key_value) { + constexpr auto keys = schema_property_keys(); + for (std::size_t index = 0; index < keys.size(); ++index) { + if (keys[index] == key_value) { + return index; + } + } + return std::nullopt; +} +template +consteval std::size_t schema_property_index() { + constexpr auto keys = schema_property_keys(); + for (std::size_t index = 0; index < keys.size(); ++index) { + if (keys[index] == Key.view()) { + return index; + } + } + return Schema::property_count; +} +template +inline constexpr std::size_t schema_property_index_v = schema_property_index(); +template +concept Schema_Property_Key = Property_Schema && schema_property_index_v < Schema::property_count; +template +consteval std::size_t schema_member_property_index() { + static_assert(std::is_member_object_pointer_v); + using member_traits = Member_Pointer_Traits; + static_assert(std::same_as); + using identity = Member_Storage_Identity; + constexpr auto matches = [](std::index_sequence) { + return std::array{std::same_as::storage_identity, identity>...}; + }(std::make_index_sequence{}); + for (std::size_t index = 0; index < matches.size(); ++index) { + if (matches[index]) { + return index; + } + } + return Schema::property_count; +} +template +inline constexpr std::size_t schema_member_property_index_v = schema_member_property_index(); +template +concept Schema_Property_Member = Property_Schema && std::is_member_object_pointer_v && schema_member_property_index_v < Schema::property_count; +template +struct Effective_Attribute { +private: + using property_attribute = typename Schema::template property_type::template attribute_type; + using default_attribute = find_attribute_in_list_t; +public: + using type = std::conditional_t, property_attribute, std::conditional_t, default_attribute, Fallback>>; +}; +template +using effective_attribute_t = typename Effective_Attribute::type; +using Default_Access_Attribute = External_Access_Attribute; +using Default_Persistence_Attribute = Persistence_Access_Attribute; +using Default_Sensitive_Attribute = Sensitive_Attribute; +template +inline constexpr External_Access effective_external_access_v = effective_attribute_t::value; +template +inline constexpr Persistence_Access effective_persistence_access_v = effective_attribute_t::value; +template +inline constexpr bool effective_sensitive_v = effective_attribute_t::value; +template +using property_declared_attribute_t = typename Schema::template property_type::template attribute_type; +template +using object_default_attribute_t = find_attribute_in_list_t; +template +inline constexpr bool has_declared_effective_attribute_v = !std::same_as, void> || !std::same_as, void>; +template +constexpr decltype(auto) declared_effective_attribute(const Schema& schema) requires has_declared_effective_attribute_v { + using property_attribute = property_declared_attribute_t; + if constexpr (!std::same_as) { + return schema.template property().template attribute(); + } else { + using default_attribute = object_default_attribute_t; + return std::get(schema.object_defaults().attributes); + } +} +template +inline constexpr bool external_readable_v = effective_external_access_v == External_Access::read || effective_external_access_v == External_Access::read_write; +template +inline constexpr bool external_writable_v = effective_external_access_v == External_Access::write || effective_external_access_v == External_Access::read_write; +template +inline constexpr bool persistence_loadable_v = effective_persistence_access_v == Persistence_Access::load || effective_persistence_access_v == Persistence_Access::load_store; +template +inline constexpr bool persistence_storable_v = effective_persistence_access_v == Persistence_Access::store || effective_persistence_access_v == Persistence_Access::load_store; +template +consteval bool property_capability_semantics_valid() { + using property_type = typename Schema::template property_type; + if constexpr ((external_readable_v || persistence_storable_v) && !property_type::readable) { + return false; + } + if constexpr ((external_writable_v || persistence_loadable_v) && !property_type::writable) { + return false; + } + return true; +} +template +consteval bool schema_capability_semantics_valid_impl(std::index_sequence) { + return (property_capability_semantics_valid() && ...); +} +template +consteval bool schema_capability_semantics_valid() { + return schema_capability_semantics_valid_impl(std::make_index_sequence{}); +} +template +concept Valid_Property_Schema = Property_Schema && requires { + requires schema_capability_semantics_valid(); +}; +template +Resolved_Synchronization_Plan resolve_synchronization_plan(const Schema&, const Synchronization_Plan& plan) { + using resolved_type = Resolved_Synchronization_Plan; + constexpr auto unsynchronized_slot = resolved_type::unsynchronized_slot; + std::array logical{}; + std::array explicitly_configured{}; + std::size_t next_token = Schema::property_count + 1; + if (plan.default_mode() == Synchronization_Default::independent) { + for (std::size_t index = 0; index < Schema::property_count; ++index) { + logical[index] = index; + } + } else if (plan.default_mode() == Synchronization_Default::shared) { + logical.fill(Schema::property_count); + } else { + logical.fill(unsynchronized_slot); + } + auto require_index = [](std::string_view key_value) { + auto index = schema_property_index(key_value); + if (!index) { + throw std::invalid_argument("Synchronization plan references unknown property: " + std::string(key_value)); + } + return *index; + }; + auto mark = [&](std::size_t index, std::string_view key_value) { + if (explicitly_configured[index]) { + throw std::invalid_argument("Synchronization plan configures property more than once: " + std::string(key_value)); + } + explicitly_configured[index] = true; + }; + for (const auto& rule : plan.property_rules()) { + auto index = require_index(rule.property); + mark(index, rule.property); + logical[index] = rule.mode == Sync_Property_Rule::Mode::unsynchronized ? unsynchronized_slot : next_token++; + } + std::vector group_names; + group_names.reserve(plan.group_rules().size()); + for (const auto& rule : plan.group_rules()) { + if (rule.properties.empty()) { + throw std::invalid_argument("Synchronization group cannot be empty: " + rule.name); + } + if (std::find(group_names.begin(), group_names.end(), rule.name) != group_names.end()) { + throw std::invalid_argument("Synchronization group name is duplicated: " + rule.name); + } + group_names.push_back(rule.name); + auto token = next_token++; + for (const auto& key_value : rule.properties) { + auto index = require_index(key_value); + mark(index, key_value); + logical[index] = token; + } + } + resolved_type resolved; + resolved.lock_slots.fill(unsynchronized_slot); + std::vector> token_slots; + token_slots.reserve(Schema::property_count); + for (std::size_t index = 0; index < Schema::property_count; ++index) { + if (logical[index] == unsynchronized_slot) { + continue; + } + auto it = std::find_if(token_slots.begin(), token_slots.end(), [&](const auto& item) { + return item.first == logical[index]; + }); + if (it == token_slots.end()) { + auto slot = resolved.lock_count++; + token_slots.emplace_back(logical[index], slot); + resolved.lock_slots[index] = slot; + } else { + resolved.lock_slots[index] = it->second; + } + } + return resolved; +} +template +consteval std::string_view schema_member_property_key() requires Schema_Property_Member { + constexpr auto index = schema_member_property_index_v; + return declared_property_key>(); +} +template +void apply_synchronization_rule(Synchronization_Plan& plan, Sync_Default_Rule rule) { + plan.apply(rule); +} +template +void apply_synchronization_rule(Synchronization_Plan& plan, Sync_Property_Rule rule) { + plan.apply(std::move(rule)); +} +template +void apply_synchronization_rule(Synchronization_Plan& plan, Sync_Group_Rule rule) { + plan.apply(std::move(rule)); +} +template +void apply_synchronization_rule(Synchronization_Plan& plan, Sync_Member_Rule) { + static_assert(Schema_Property_Member); + constexpr auto key_value = schema_member_property_key(); + if constexpr (Mode == Sync_Property_Rule::Mode::independent) { + plan.independent(key_value); + } else { + plan.unsynchronized(key_value); + } +} +template +void apply_synchronization_rule(Synchronization_Plan& plan, const Sync_Member_Group_Rule& rule) { + static_assert((Schema_Property_Member && ...)); + constexpr std::array keys{schema_member_property_key()...}; + plan.group(rule.name, std::span{keys}); +} +template +Synchronization_Plan materialize_synchronization_plan(Source&& source) { + if constexpr (Synchronization_Plan_Type>) { + return std::forward(source); + } else { + Synchronization_Plan plan; + std::apply([&](auto&&... rules) { + (apply_synchronization_rule(plan, std::forward(rules)), ...); + }, std::forward(source).rules); + return plan; + } +} +template +bool visit_schema_property(const Schema& schema, std::string_view key_value, Function&& function) { + bool found = false; + schema.for_each_property([&](auto index, const auto& descriptor) { + if (!found) { + using property_type = std::remove_cvref_t; + if (declared_property_key() == key_value) { + std::invoke(function, index, descriptor); + found = true; + } + } + }); + return found; +} +template +auto make_object_schema(Defaults&& object_defaults, Source&& synchronization_source, Properties&&... properties) requires Synchronization_Source_Type> { + using schema_type = Object_Schema, std::decay_t...>; + static_assert(Valid_Property_Schema); + auto plan = materialize_synchronization_plan(std::forward(synchronization_source)); + schema_type schema{std::forward(object_defaults), std::move(plan), std::tuple...>{std::forward(properties)...}}; + static_cast(resolve_synchronization_plan(schema, schema.synchronization_plan())); + return schema; +} +template +auto object_schema(Defaults&& object_defaults, Source&& synchronization_source, Properties&&... properties) requires Object_Defaults_Type> && Synchronization_Source_Type> && (Property_Descriptor_Type> && ...) { + return make_object_schema(std::forward(object_defaults), std::forward(synchronization_source), std::forward(properties)...); +} +template +auto object_schema(Source&& synchronization_source, Properties&&... properties) requires Synchronization_Source_Type> && (Property_Descriptor_Type> && ...) { + return make_object_schema(no_defaults, std::forward(synchronization_source), std::forward(properties)...); +} +template +auto object_schema(Defaults&& object_defaults, Properties&&... properties) requires Object_Defaults_Type> && (Property_Descriptor_Type> && ...) { + return make_object_schema(std::forward(object_defaults), Synchronization_Plan{}, std::forward(properties)...); +} +template +auto object_schema(Properties&&... properties) requires (Property_Descriptor_Type> && ...) { + return make_object_schema(no_defaults, Synchronization_Plan{}, std::forward(properties)...); +} +template +auto object(Arguments&&... arguments) { + return object_schema(std::forward(arguments)...); +} +} diff --git a/core/include/structive/property/synchronization.hpp b/core/include/structive/property/synchronization.hpp new file mode 100644 index 0000000..0b37b46 --- /dev/null +++ b/core/include/structive/property/synchronization.hpp @@ -0,0 +1,168 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace structive { +enum class Synchronization_Default { + independent, + shared, + unsynchronized +}; +struct Sync_Default_Rule { + Synchronization_Default value{Synchronization_Default::independent}; +}; +struct Sync_Property_Rule { + enum class Mode { + independent, + unsynchronized + }; + Mode mode{Mode::independent}; + std::string property; +}; +struct Sync_Group_Rule { + std::string name; + std::vector properties; +}; +template +struct Sync_Member_Rule { + static constexpr auto member = Member; + static constexpr auto mode = Mode; +}; +template +struct Sync_Member_Group_Rule { + static_assert(sizeof...(Members) > 0); + std::string name; +}; +inline constexpr Sync_Default_Rule sync_all_independent{Synchronization_Default::independent}; +inline constexpr Sync_Default_Rule sync_all_shared{Synchronization_Default::shared}; +inline constexpr Sync_Default_Rule sync_all_unsynchronized{Synchronization_Default::unsynchronized}; +inline Sync_Property_Rule sync_independent(std::string_view property) { + return {Sync_Property_Rule::Mode::independent, std::string(property)}; +} +inline Sync_Property_Rule sync_unsynchronized(std::string_view property) { + return {Sync_Property_Rule::Mode::unsynchronized, std::string(property)}; +} +template +constexpr auto sync_independent() { + return Sync_Member_Rule{}; +} +template +constexpr auto sync_unsynchronized() { + return Sync_Member_Rule{}; +} +template +Sync_Member_Group_Rule sync_group(std::string_view name) { + return {std::string(name)}; +} +template +Sync_Group_Rule sync_group(std::string_view name, Properties&&... properties) requires (std::convertible_to && ...) { + Sync_Group_Rule rule; + rule.name = name; + rule.properties.reserve(sizeof...(Properties)); + (rule.properties.emplace_back(std::string_view(std::forward(properties))), ...); + return rule; +} +class Synchronization_Plan { + Synchronization_Default default_mode_{Synchronization_Default::independent}; + std::vector property_rules_; + std::vector group_rules_; +public: + using synchronization_plan_tag = void; + Synchronization_Plan() = default; + explicit Synchronization_Plan(Synchronization_Default mode) : default_mode_(mode) {} + Synchronization_Default default_mode() const noexcept { + return default_mode_; + } + const auto& property_rules() const noexcept { + return property_rules_; + } + const auto& group_rules() const noexcept { + return group_rules_; + } + Synchronization_Plan& set_default(Synchronization_Default mode) { + default_mode_ = mode; + return *this; + } + Synchronization_Plan& independent(std::string_view property) { + property_rules_.push_back(sync_independent(property)); + return *this; + } + Synchronization_Plan& unsynchronized(std::string_view property) { + property_rules_.push_back(sync_unsynchronized(property)); + return *this; + } + Synchronization_Plan& group(std::string_view name, std::span properties) { + Sync_Group_Rule rule; + rule.name = name; + rule.properties.reserve(properties.size()); + for (auto property : properties) { + rule.properties.emplace_back(property); + } + group_rules_.push_back(std::move(rule)); + return *this; + } + Synchronization_Plan& group(std::string_view name, std::initializer_list properties) { + return group(name, std::span{properties.begin(), properties.size()}); + } + Synchronization_Plan& apply(Sync_Default_Rule rule) { + default_mode_ = rule.value; + return *this; + } + Synchronization_Plan& apply(Sync_Property_Rule rule) { + property_rules_.push_back(std::move(rule)); + return *this; + } + Synchronization_Plan& apply(Sync_Group_Rule rule) { + group_rules_.push_back(std::move(rule)); + return *this; + } +}; +template +concept Synchronization_Plan_Type = requires { + typename Type::synchronization_plan_tag; +}; +template +concept Runtime_Synchronization_Rule = std::same_as, Sync_Default_Rule> || std::same_as, Sync_Property_Rule> || std::same_as, Sync_Group_Rule>; +template +struct Synchronization_Spec { + using synchronization_spec_tag = void; + std::tuple rules; +}; +template +concept Synchronization_Spec_Type = requires { + typename Type::synchronization_spec_tag; +}; +template +concept Synchronization_Source_Type = Synchronization_Plan_Type || Synchronization_Spec_Type; +template +auto synchronization(Rules&&... rules) { + if constexpr ((Runtime_Synchronization_Rule && ...)) { + Synchronization_Plan plan; + (plan.apply(std::forward(rules)), ...); + return plan; + } else { + return Synchronization_Spec...>{{std::forward(rules)...}}; + } +} +template +struct Resolved_Synchronization_Plan { + static constexpr std::size_t unsynchronized_slot = std::numeric_limits::max(); + std::array lock_slots{}; + std::size_t lock_count{}; + constexpr std::size_t slot(std::size_t property_index) const noexcept { + return lock_slots[property_index]; + } + constexpr bool uses_lock(std::size_t property_index) const noexcept { + return slot(property_index) != unsynchronized_slot; + } +}; +} diff --git a/core/include/structive/property/type_descriptor.hpp b/core/include/structive/property/type_descriptor.hpp new file mode 100644 index 0000000..2615001 --- /dev/null +++ b/core/include/structive/property/type_descriptor.hpp @@ -0,0 +1,21 @@ +#pragma once +#include "schema.hpp" +#include +#include +namespace structive { +template +struct Type_Descriptor; +template +using type_descriptor_schema_t = std::remove_cvref_t::get())>; +template +concept Property_Described_Object = requires { + { Type_Descriptor::get() }; + requires Valid_Property_Schema>; + requires std::same_as::object_type, Object>; +}; +template +const type_descriptor_schema_t& type_descriptor() { + static const auto value = Type_Descriptor::get(); + return value; +} +} diff --git a/core/include/structive/property/validation.hpp b/core/include/structive/property/validation.hpp new file mode 100644 index 0000000..5bcbc22 --- /dev/null +++ b/core/include/structive/property/validation.hpp @@ -0,0 +1,30 @@ +#pragma once +#include "schema.hpp" +#include +#include +#include +namespace structive { +struct Validation_Error { + std::string_view property_key; + std::string_view code; +}; +template +std::optional validate_property_value(const Schema& schema, const Value& value) { + std::optional error; + schema.template property().for_each_constraint([&](const auto& constraint_value) { + if (!error && !static_cast(constraint_value.validate(value))) { + using constraint_type = std::remove_cvref_t; + error = Validation_Error{declared_property_key>(), constraint_type::code.view()}; + } + }); + return error; +} +template +std::optional validate_property_value(const Schema& schema, const Value& value) requires Schema_Property_Member { + return validate_property_value>(schema, value); +} +template +std::optional validate_property_key_value(const Schema& schema, const Value& value) requires Schema_Property_Key { + return validate_property_value>(schema, value); +} +} diff --git a/core/tests/property_core_test.cpp b/core/tests/property_core_test.cpp new file mode 100644 index 0000000..a23280f --- /dev/null +++ b/core/tests/property_core_test.cpp @@ -0,0 +1,223 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace structive; +struct Test_Tag_Category {}; +template +struct Test_Tag_Attribute { + using attribute_category = Test_Tag_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = false; + static constexpr int value = Value; +}; +template +inline constexpr Test_Tag_Attribute test_tag{}; +#define REQUIRE(expression) do { if (!(expression)) { std::fprintf(stderr, "REQUIRE failed: %s:%d: %s\n", __FILE__, __LINE__, #expression); std::abort(); } } while (false) +struct Device : Property_Object { + Device() = default; + explicit Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {} + int temperature{20}; + int pressure{100}; + int min_speed{10}; + int max_speed{100}; + int immutable_id{7}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + defaults(external_access, persistence_access), + synchronization(sync_all_independent, sync_group < &Device::min_speed, &Device::max_speed > ("speed_range"), sync_unsynchronized<&Device::immutable_id>()), + field < &Device::temperature > (key < "temperature" >, min_value < -50 >, max_value < 200 >, unit < "C" >, test_tag<7>), + field < &Device::pressure > (key < "pressure" >), + field < &Device::min_speed > (key < "minimum_speed" >), + field < &Device::max_speed > (key < "maximum_speed" >), + field < &Device::immutable_id > (key < "immutable_id" >, external_access, persistence_access) + ); + } +}; +struct Computed_Device : Property_Object { + int min_speed{10}; + int max_speed{100}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + defaults(external_access), + synchronization(sync_all_independent, sync_group("speed", "min_speed", "max_speed", "speed_span")), + field < &Computed_Device::min_speed > (key < "min_speed" >), + field < &Computed_Device::max_speed > (key < "max_speed" >), + computed_property([](const auto& view) { + return view.template get<&Computed_Device::max_speed>() - view.template get<&Computed_Device::min_speed>(); + }, key < "speed_span" >, external_access) + ); + } +}; +struct Non_Copyable_Device : Property_Object { + std::unique_ptr payload{std::make_unique(42)}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + defaults(external_access), + field < &Non_Copyable_Device::payload > (key < "payload" >) + ); + } +}; +template +concept Has_Write_Temperature = requires(View view) { + view.template write<&Device::temperature>(1); +}; +static bool update_speed_range(Device& device, int min_speed, int max_speed) { + auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); + int old_min = guard.get<&Device::min_speed>(); + int old_max = guard.get<&Device::max_speed>(); + guard.set < &Device::min_speed > (min_speed); + guard.set < &Device::max_speed > (max_speed); + if (min_speed <= max_speed) { + return true; + } + guard.set < &Device::min_speed > (old_min); + guard.set < &Device::max_speed > (old_max); + return false; +} +static void runtime_read_int(void* context, std::size_t, std::string_view, const std::type_info& type, const void* value) { + REQUIRE(type == typeid(int)); + *static_cast(context) = *static_cast(value); +} +int main() { + static_assert(Property_Described_Object); + const auto& schema = type_descriptor(); + using Schema = type_descriptor_schema_t; + static_assert(Valid_Property_Schema); + static_assert(Schema::property_count == 5); + REQUIRE(schema.template property<0>().key() == "temperature"); + REQUIRE(schema.template property<&Device::temperature>().key() == "temperature"); + REQUIRE(schema.template property<&Device::min_speed>().key() == "minimum_speed"); + REQUIRE(schema.template property<&Device::max_speed>().key() == "maximum_speed"); + using Temperature_Property = std::remove_cvref_t())>; + static_assert(Temperature_Property::template has_attribute); + REQUIRE(Temperature_Property::template attribute_type::value == 7); + Device device; + REQUIRE(device.temperature == 20); + device.temperature = 21; + REQUIRE(device.read<&Device::temperature>() == 21); + device.write < &Device::temperature > (22); + REQUIRE(device.temperature == 22); + REQUIRE(&device.unsafe_object() == &device); + REQUIRE(&device.schema() == &schema); + REQUIRE(device.lock_slot<&Device::min_speed>() == device.lock_slot<&Device::max_speed>()); + REQUIRE(device.lock_slot<&Device::temperature>() != device.lock_slot<&Device::pressure>()); + REQUIRE(device.lock_slot<&Device::immutable_id>() == Resolved_Synchronization_View::unsynchronized_slot); + Device shared_device{property_synchronization(synchronization(sync_all_shared))}; + REQUIRE(shared_device.lock_slot<&Device::temperature>() == shared_device.lock_slot<&Device::pressure>()); + REQUIRE(shared_device.lock_slot<&Device::pressure>() == shared_device.lock_slot<&Device::min_speed>()); + Device grouped_device{property_synchronization(synchronization(sync_all_independent, sync_group < &Device::temperature, &Device::pressure > ("environment")))}; + REQUIRE(grouped_device.lock_slot<&Device::temperature>() == grouped_device.lock_slot<&Device::pressure>()); + device.external().write < &Device::temperature > (30); + REQUIRE(device.external().read<&Device::temperature>() == 30); + device.persistence().load < &Device::pressure > (101); + REQUIRE(device.persistence().store<&Device::pressure>() == 101); + auto validation = validate_property_value < &Device::temperature > (device.schema(), 500); + REQUIRE(validation.has_value()); + REQUIRE(validation->code == "max_value"); + REQUIRE(!update_speed_range(device, 200, 100)); + REQUIRE(device.read<&Device::min_speed>() == 10); + REQUIRE(device.read<&Device::max_speed>() == 100); + REQUIRE(update_speed_range(device, 20, 120)); + REQUIRE(device.read<&Device::min_speed>() == 20); + REQUIRE(device.read<&Device::max_speed>() == 120); + std::size_t schema_visits = 0; + schema.for_each_property([&](auto, const auto&) { + ++schema_visits; + }); + REQUIRE(schema_visits == 5); + std::size_t value_visits = 0; + device.external().for_each_readable_locked([&](auto, const auto&, const auto&) { + ++value_visits; + }); + REQUIRE(value_visits == 5); + Computed_Device computed; + REQUIRE(computed.external().read_key<"speed_span">() == 90); + computed.write < &Computed_Device::min_speed > (20); + REQUIRE(computed.external().read_key<"speed_span">() == 80); + Non_Copyable_Device non_copyable; + bool non_copyable_visited = false; + non_copyable.external().for_each_readable_locked([&](auto, const auto&, const auto& value) { + REQUIRE(*value == 42); + non_copyable_visited = true; + }); + REQUIRE(non_copyable_visited); + Property_Object_Base& erased = device; + REQUIRE(erased.runtime_object_type() == typeid(Device)); + REQUIRE(erased.runtime_property_count() == 5); + int runtime_value = 0; + REQUIRE(erased.runtime_read(Managed_Access_Mode::external, "temperature", &runtime_value, &runtime_read_int) == Runtime_Access_Result::ok); + REQUIRE(runtime_value == 30); + int runtime_write_value = 35; + REQUIRE(erased.runtime_write(Managed_Access_Mode::external, "temperature", typeid(int), &runtime_write_value) == Runtime_Access_Result::ok); + REQUIRE(device.temperature == 35); + REQUIRE(erased.runtime_write(Managed_Access_Mode::external, "immutable_id", typeid(int), &runtime_write_value) == Runtime_Access_Result::not_writable); + double wrong_type = 1.0; + REQUIRE(erased.runtime_write(Managed_Access_Mode::external, "temperature", typeid(double), &wrong_type) == Runtime_Access_Result::type_mismatch); + REQUIRE(erased.runtime_write(Managed_Access_Mode::external, "missing", typeid(int), &runtime_write_value) == Runtime_Access_Result::unknown_property); + const Device& const_device = device; + static_assert(!Has_Write_Temperature); + std::binary_semaphore runtime_blocked_done{0}; + std::jthread runtime_writer; + { + auto guard = device.lock_unique<&Device::temperature>(); + runtime_writer = std::jthread([&] { + int value = 36; + REQUIRE(erased.runtime_write(Managed_Access_Mode::external, "temperature", typeid(int), &value) == Runtime_Access_Result::ok); + runtime_blocked_done.release(); + }); + REQUIRE(!runtime_blocked_done.try_acquire_for(std::chrono::milliseconds(20))); + } + REQUIRE(runtime_blocked_done.try_acquire_for(std::chrono::seconds(2))); + runtime_writer.join(); + REQUIRE(device.read<&Device::temperature>() == 36); + std::binary_semaphore independent_done{0}; + { + auto guard = device.lock_unique({"temperature"}); + std::jthread writer([&] { + device.write < &Device::pressure > (200); + independent_done.release(); + }); + REQUIRE(independent_done.try_acquire_for(std::chrono::seconds(2))); + } + std::barrier gate(2); + std::atomic completed{0}; + std::jthread first([&] { + gate.arrive_and_wait(); + auto guard = device.lock_unique({"temperature", "pressure"}); + guard.set < &Device::temperature > (40); + guard.set < &Device::pressure > (140); + completed.fetch_add(1, std::memory_order_release); + }); + std::jthread second([&] { + gate.arrive_and_wait(); + auto guard = device.lock_unique({"pressure", "temperature"}); + guard.set < &Device::pressure > (141); + guard.set < &Device::temperature > (41); + completed.fetch_add(1, std::memory_order_release); + }); + first.join(); + second.join(); + REQUIRE(completed.load(std::memory_order_acquire) == 2); + Device copied = device; + REQUIRE(copied.temperature == device.temperature); + copied.write < &Device::temperature > (99); + REQUIRE(device.read<&Device::temperature>() != copied.read<&Device::temperature>()); + return 0; +} diff --git a/docs/CORE_GUIDE.md b/docs/CORE_GUIDE.md new file mode 100644 index 0000000..f7a2f0a --- /dev/null +++ b/docs/CORE_GUIDE.md @@ -0,0 +1,660 @@ +# Structive Property Core Guide + +[中文](CORE_GUIDE.zh-CN.md) + +## 1. Include and target + +Include the complete Core surface with: + +```cpp +#include +``` + +CMake target: + +```cmake +target_link_libraries(my_target PRIVATE structive::property_core) +``` + +The Core target is header-only and requires C++20. + +## 2. Define a managed object + +A managed object normally derives from `Property_Object`: + +```cpp +using namespace structive; +struct Device : Property_Object { + double temperature{25.0}; + double pressure{101.3}; + double min_speed{10.0}; + double max_speed{100.0}; + std::string name{"device-1"}; +}; +``` + +The members remain ordinary C++ members. + +## 3. Define the type descriptor + +Specialize `Type_Descriptor` and return an `Object_Schema` through `object(...)`: + +```cpp +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + defaults( + external_access, + persistence_access + ), + synchronization( + sync_all_independent, + sync_group<&Device::min_speed, &Device::max_speed>("speed_range") + ), + field<&Device::temperature>( + key<"temperature">, + unit<"C">, + min_value<-50.0>, + max_value<200.0> + ), + field<&Device::pressure>(key<"pressure">, unit<"kPa">), + field<&Device::min_speed>(key<"min_speed">), + field<&Device::max_speed>(key<"max_speed">), + field<&Device::name>(key<"name">) + ); + } +}; +``` + +`field(...)` is an alias of `property(...)` and produces a member-backed `Property_Descriptor`. + +## 4. Schema guarantees + +The schema enforces several structural conditions: + +- every property has a non-empty `key`; +- property keys are unique; +- the same member storage identity cannot be registered twice; +- single-valued Attribute categories cannot appear more than once on the same declaration; +- constraints must be compatible with the property value type; +- object defaults may contain only inheritable Attributes; +- external/persistence capabilities cannot claim operations unsupported by the accessor. + +Many schema errors are therefore compile-time errors. + +## 5. Access the schema + +For a described type: + +```cpp +const auto& schema = type_descriptor(); +``` + +For an instance: + +```cpp +Device device; +const auto& schema = device.schema(); +``` + +Property lookup supports a numeric compile-time index or a member pointer: + +```cpp +const auto& first = schema.property<0>(); +const auto& temperature = schema.property<&Device::temperature>(); +``` + +Use member pointers in typed business code. Use indexes mainly in generic traversal. + +## 6. Property descriptor information + +A property descriptor exposes: + +```cpp +auto key_value = temperature.key(); +``` + +and compile-time traits such as: + +```cpp +using Property = std::remove_cvref_t; +static_assert(Property::readable); +static_assert(Property::writable); +``` + +Category-based Attribute lookup is available through: + +```cpp +static_assert(Property::has_attribute); +const auto& attribute = temperature.attribute(); +``` + +All declared Attributes can be traversed: + +```cpp +temperature.for_each_attribute([](const auto& attribute) { + // inspect attribute type/value +}); +``` + +Constraints can be traversed separately: + +```cpp +temperature.for_each_constraint([](const auto& constraint_value) { + // inspect or evaluate a constraint +}); +``` + +## 7. Object defaults and effective Attributes + +`defaults(...)` provides object-wide values for inheritable Attribute categories: + +```cpp +defaults( + external_access, + persistence_access, + sensitive +) +``` + +A property may override a default: + +```cpp +field<&Device::name>( + key<"name">, + external_access +) +``` + +Core exposes effective access traits such as `effective_external_access_v`, `external_readable_v`, `external_writable_v`, `persistence_loadable_v`, `persistence_storable_v` and `effective_sensitive_v` for generic code. + +Extensions can use `declared_effective_attribute(schema)` for extension-owned inheritable categories when either the property or object defaults declare that category. + +## 8. Core Attributes + +### 8.1 Key + +```cpp +key<"temperature"> +``` + +Required for every property. Non-empty and unique per schema. + +### 8.2 External access + +```cpp +external_access +external_access +external_access +external_access +``` + +This Attribute is inheritable through `defaults(...)`. + +### 8.3 Persistence access + +```cpp +persistence_access +persistence_access +persistence_access +persistence_access +``` + +This Attribute is also inheritable. + +### 8.4 Unit + +```cpp +unit<"C"> +``` + +This is descriptive metadata and is not inheritable. + +### 8.5 Sensitive + +```cpp +sensitive<> +sensitive +``` + +This is inheritable metadata. Core exposes its effective value but does not automatically redact data. + +## 9. Constraints and validation + +Built-in constraints: + +```cpp +min_value<0> +max_value<100> +finite +``` + +Custom constraint: + +```cpp +constraint<"even">([](int value) { + return value % 2 == 0; +}) +``` + +Validation by member pointer: + +```cpp +auto error = validate_property_value<&Device::temperature>(device.schema(), candidate); +``` + +Validation by compile-time key: + +```cpp +auto error = validate_property_key_value<"temperature">(device.schema(), candidate); +``` + +A failure returns: + +```cpp +struct Validation_Error { + std::string_view property_key; + std::string_view code; +}; +``` + +Validation is explicit and is not automatically executed by managed writes. + +## 10. Managed internal read/write + +Typed member-pointer access: + +```cpp +auto temperature = device.read<&Device::temperature>(); +device.write<&Device::temperature>(30.0); +``` + +Compile-time key access: + +```cpp +auto temperature = device.read_key<"temperature">(); +device.write_key<"temperature">(30.0); +``` + +These operations use `Managed_Access_Mode::internal`. + +`read()` returns a value object, not a reference to storage. The read is performed while the configured shared lock is held when synchronization is enabled. + +## 11. External capability view + +Object-level defaults may expose properties externally: + +```cpp +defaults(external_access) +``` + +Use: + +```cpp +device.external().write<&Device::temperature>(30.0); +auto value = device.external().read<&Device::temperature>(); +``` + +A const object produces a const capability view and therefore has no write API. + +The typed API rejects statically inaccessible operations at compile time. + +## 12. Persistence capability view + +Use persistence terminology instead of generic write/read: + +```cpp +device.persistence().load<&Device::temperature>(30.0); +auto value = device.persistence().store<&Device::temperature>(); +``` + +Compile-time key forms are also available: + +```cpp +device.persistence().load_key<"temperature">(30.0); +auto value = device.persistence().store_key<"temperature">(); +``` + +The persistence view is controlled by `Persistence_Access` metadata. + +## 13. Synchronization plans + +### 13.1 Default independent + +The default `Synchronization_Plan` is independent: each synchronized property receives its own logical lock domain. + +Explicit form: + +```cpp +synchronization(sync_all_independent) +``` + +### 13.2 Shared + +```cpp +synchronization(sync_all_shared) +``` + +All properties use the same lock slot unless overridden. + +### 13.3 Unsynchronized + +```cpp +synchronization(sync_all_unsynchronized) +``` + +Properties use the `unsynchronized_slot` and no real Structive mutex protects them. + +### 13.4 Per-property override + +By member pointer: + +```cpp +sync_independent<&Device::temperature>() +sync_unsynchronized<&Device::immutable_id>() +``` + +Runtime string forms also exist: + +```cpp +sync_independent("temperature") +sync_unsynchronized("immutable_id") +``` + +Prefer member-pointer rules when the property is statically known. + +### 13.5 Groups + +Member-pointer form: + +```cpp +sync_group<&Device::min_speed, &Device::max_speed>("speed_range") +``` + +String form: + +```cpp +sync_group("speed_range", "min_speed", "max_speed") +``` + +All properties in a group share one lock slot. + +The resolver rejects unknown runtime keys, duplicate group names, empty groups and multiple explicit configurations of the same property. + +## 14. Per-instance synchronization override + +The type descriptor supplies a default synchronization plan, but an instance may override it through `Property_Synchronization`: + +```cpp +struct Device : Property_Object { + Device() = default; + explicit Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {} +}; +Device shared_device{ + property_synchronization(synchronization(sync_all_shared)) +}; +``` + +For schema-aware compile-time synchronization specifications: + +```cpp +auto policy = property_synchronization( + synchronization( + sync_all_independent, + sync_group<&Device::temperature, &Device::pressure>("environment") + ) +); +``` + +The schema remains the same; only the instance lock topology changes. + +## 15. Inspect resolved synchronization + +For a typed member: + +```cpp +auto slot = device.lock_slot<&Device::temperature>(); +``` + +For the complete view: + +```cpp +auto resolved = device.resolved_synchronization(); +auto count = resolved.lock_count; +auto slot = resolved.slot(0); +auto uses_lock = resolved.uses_lock(0); +``` + +`Resolved_Synchronization_View::unsynchronized_slot` identifies unsynchronized properties. + +## 16. Multi-property static guards + +Shared guard: + +```cpp +auto guard = device.lock_shared<&Device::temperature, &Device::pressure>(); +auto temperature = guard.get<&Device::temperature>(); +auto pressure = guard.get<&Device::pressure>(); +``` + +Unique guard: + +```cpp +auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); +auto old_min = guard.get<&Device::min_speed>(); +guard.set<&Device::min_speed>(20.0); +guard.set<&Device::max_speed>(120.0); +``` + +The guard only permits access to properties inside its held synchronization set. + +Capability views provide equivalent static guards constrained by their access mode: + +```cpp +auto guard = device.external().lock_shared<&Device::temperature>(); +``` + +## 17. Dynamic-key guards + +When a property set is known only at runtime: + +```cpp +std::array keys{"temperature", "pressure"}; +auto guard = device.lock_shared(keys); +``` + +or: + +```cpp +auto guard = device.lock_unique({"min_speed", "max_speed"}); +``` + +Dynamic guards validate keys and capability visibility at runtime. Lock slots are sorted and deduplicated before acquisition. + +Read/write through a dynamic guard can still use compile-time members/keys after the guard is acquired; access outside the held set throws `std::logic_error`. + +## 18. Traversal + +Schema traversal: + +```cpp +schema.for_each_property([](auto index, const auto& property) { + // compile-time index and descriptor +}); +``` + +Managed value traversal: + +```cpp +device.for_each_readable([](auto index, const auto& descriptor, const auto& value) { + // one managed read per property +}); +``` + +Locked traversal acquires the complete readable synchronization set first: + +```cpp +device.external().for_each_readable_locked([](auto index, const auto& descriptor, const auto& value) { + // all selected readable properties are held under the guard +}); +``` + +For coordinated writable access: + +```cpp +device.with_all_writable_locked([](auto& guard) { + // use guard.get / guard.set +}); +``` + +The persistence view provides `with_all_loadable_locked(...)`. + +## 19. Computed property + +A synchronized computed property receives a read view: + +```cpp +computed_property([](const auto& view) { + return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); +}, key<"speed_span">, external_access) +``` + +The computed property itself and every dependency read through the view must resolve to the same lock slot. + +For example: + +```cpp +synchronization( + sync_all_independent, + sync_group("speed", "min_speed", "max_speed", "speed_span") +) +``` + +Computed properties are read-only. + +## 20. Trusted accessor properties + +Core also supports member-function-based trusted access: + +```cpp +trusted_computed_property<&Device::get_temperature>(key<"temperature">) +``` + +and getter/setter pairs: + +```cpp +trusted_accessor_property<&Device::get_temperature, &Device::set_temperature>(key<"temperature">) +``` + +These accessors invoke the object member functions directly and mark themselves as trusted object access. They do not use the synchronized computed-view dependency restriction. Use them when the member functions themselves define the correct synchronization/consistency contract. + +## 21. Runtime type-erased access + +Any `Property_Object` is also a `Property_Object_Base`: + +```cpp +Property_Object_Base& erased = device; +``` + +Introspection: + +```cpp +erased.runtime_object_type(); +erased.runtime_property_count(); +``` + +Runtime write: + +```cpp +double value = 35.0; +auto result = erased.runtime_write( + Managed_Access_Mode::external, + "temperature", + typeid(double), + &value +); +``` + +Runtime read uses a callback: + +```cpp +static void read_double(void* context, std::size_t, std::string_view, const std::type_info& type, const void* value) { + if (type == typeid(double)) { + *static_cast(context) = *static_cast(value); + } +} +double output = 0.0; +auto result = erased.runtime_read( + Managed_Access_Mode::external, + "temperature", + &output, + &read_double +); +``` + +Possible results: + +```cpp +Runtime_Access_Result::ok +Runtime_Access_Result::unknown_property +Runtime_Access_Result::not_readable +Runtime_Access_Result::not_writable +Runtime_Access_Result::type_mismatch +``` + +The runtime path uses the same capability and synchronization rules as typed managed access. + +## 22. Lock policy + +Default: + +```cpp +struct Device : Property_Object { +}; +``` + +Equivalent shorthand: + +```cpp +struct Device : Property_Object { +}; +``` + +A no-op lock policy exists: + +```cpp +struct Device : Property_Object { +}; +``` + +`No_Lock_Policy` uses `Null_Shared_Mutex`; it removes real mutual exclusion and should only be selected when external ownership guarantees make that correct. + +## 23. Raw object access + +`unsafe_object()` exposes the derived object directly: + +```cpp +Device& raw = device.unsafe_object(); +``` + +Direct field access is also normal C++: + +```cpp +device.temperature = 40.0; +``` + +These paths intentionally bypass Structive-managed access. They are useful when the caller already owns the required synchronization or when a field is being handled outside Structive’s managed contract. + +## 24. Recommended usage rules + +- Use member pointers for typed business access. +- Use string keys at runtime integration boundaries. +- Treat keys as protocol identifiers. +- Keep object-wide access policy in `defaults(...)` and override only exceptions. +- Use synchronization groups for fields that must be observed consistently. +- Validate explicitly at the business operation boundary. +- Use static multi-property guards for cross-field invariants. +- Prefer synchronized computed properties over trusted accessors. +- Use runtime access for adapters, not ordinary typed code. +- Use raw access only when the caller intentionally owns the missing managed guarantees. diff --git a/docs/CORE_GUIDE.zh-CN.md b/docs/CORE_GUIDE.zh-CN.md new file mode 100644 index 0000000..7e9e350 --- /dev/null +++ b/docs/CORE_GUIDE.zh-CN.md @@ -0,0 +1,660 @@ +# Structive Property Core 完整指南 + +[English](CORE_GUIDE.md) + +## 1. Include 与 CMake Target + +完整 Core 接口: + +```cpp +#include +``` + +CMake: + +```cmake +target_link_libraries(my_target PRIVATE structive::property_core) +``` + +Core 是 header-only,要求 C++20。 + +## 2. 定义 Managed Object + +通常让业务对象继承 `Property_Object`: + +```cpp +using namespace structive; +struct Device : Property_Object { + double temperature{25.0}; + double pressure{101.3}; + double min_speed{10.0}; + double max_speed{100.0}; + std::string name{"device-1"}; +}; +``` + +这些成员仍然都是普通 C++ 成员。 + +## 3. 定义 Type Descriptor + +特化 `Type_Descriptor`,通过 `object(...)` 返回 `Object_Schema`: + +```cpp +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + defaults( + external_access, + persistence_access + ), + synchronization( + sync_all_independent, + sync_group<&Device::min_speed, &Device::max_speed>("speed_range") + ), + field<&Device::temperature>( + key<"temperature">, + unit<"C">, + min_value<-50.0>, + max_value<200.0> + ), + field<&Device::pressure>(key<"pressure">, unit<"kPa">), + field<&Device::min_speed>(key<"min_speed">), + field<&Device::max_speed>(key<"max_speed">), + field<&Device::name>(key<"name">) + ); + } +}; +``` + +`field(...)` 是 `property(...)` 的别名,产生 member-backed `Property_Descriptor`。 + +## 4. Schema 提供的静态保证 + +Schema 会检查: + +- 每个 property 都必须拥有非空 `key`; +- 同一 Schema 内 key 唯一; +- 同一个成员存储不能重复注册; +- single-valued Attribute category 不能在同一声明中重复; +- constraint 必须能作用于 property value type; +- `defaults(...)` 只能放 inheritable Attribute; +- External/Persistence capability 不能声明 accessor 实际不支持的读写能力。 + +因此大量结构错误会直接成为编译期错误。 + +## 5. 获取 Schema + +类型级: + +```cpp +const auto& schema = type_descriptor(); +``` + +实例级: + +```cpp +Device device; +const auto& schema = device.schema(); +``` + +可以通过编译期 index 或成员指针定位 property: + +```cpp +const auto& first = schema.property<0>(); +const auto& temperature = schema.property<&Device::temperature>(); +``` + +业务 typed code 优先成员指针;index 主要用于泛型遍历。 + +## 6. Property Descriptor + +获取 key: + +```cpp +auto key_value = temperature.key(); +``` + +静态能力: + +```cpp +using Property = std::remove_cvref_t; +static_assert(Property::readable); +static_assert(Property::writable); +``` + +按 category 查询 Attribute: + +```cpp +static_assert(Property::has_attribute); +const auto& attribute = temperature.attribute(); +``` + +遍历全部声明 Attribute: + +```cpp +temperature.for_each_attribute([](const auto& attribute) { + // 根据 attribute 类型处理 +}); +``` + +单独遍历 constraint: + +```cpp +temperature.for_each_constraint([](const auto& constraint_value) { + // 检查 constraint +}); +``` + +## 7. Object Defaults 与 Effective Attribute + +`defaults(...)` 给 inheritable Attribute 提供对象级默认值: + +```cpp +defaults( + external_access, + persistence_access, + sensitive +) +``` + +Property 可以覆盖: + +```cpp +field<&Device::name>( + key<"name">, + external_access +) +``` + +Core 提供 `effective_external_access_v`、`external_readable_v`、`external_writable_v`、`persistence_loadable_v`、`persistence_storable_v`、`effective_sensitive_v` 等有效属性计算入口。 + +Extension 自己的 inheritable category 可以通过 `declared_effective_attribute(schema)` 读取 property 声明或 object default 中的有效值。 + +## 8. Core Attribute + +### 8.1 Key + +```cpp +key<"temperature"> +``` + +每个 property 必须存在,不能为空,同一 Schema 内唯一。 + +### 8.2 External Access + +```cpp +external_access +external_access +external_access +external_access +``` + +支持在 `defaults(...)` 中继承。 + +### 8.3 Persistence Access + +```cpp +persistence_access +persistence_access +persistence_access +persistence_access +``` + +同样支持继承。 + +### 8.4 Unit + +```cpp +unit<"C"> +``` + +描述元数据,不支持 object default 继承。 + +### 8.5 Sensitive + +```cpp +sensitive<> +sensitive +``` + +支持继承。Core 会计算 effective value,但不会自动执行脱敏或隐藏输出。 + +## 9. Constraint 与 Validation + +内置 constraint: + +```cpp +min_value<0> +max_value<100> +finite +``` + +自定义 constraint: + +```cpp +constraint<"even">([](int value) { + return value % 2 == 0; +}) +``` + +按成员指针验证: + +```cpp +auto error = validate_property_value<&Device::temperature>(device.schema(), candidate); +``` + +按编译期 key 验证: + +```cpp +auto error = validate_property_key_value<"temperature">(device.schema(), candidate); +``` + +失败结果: + +```cpp +struct Validation_Error { + std::string_view property_key; + std::string_view code; +}; +``` + +Validation 是显式操作,不会在 managed write 中自动执行。 + +## 10. Internal Managed Read/Write + +成员指针形式: + +```cpp +auto temperature = device.read<&Device::temperature>(); +device.write<&Device::temperature>(30.0); +``` + +编译期 key 形式: + +```cpp +auto temperature = device.read_key<"temperature">(); +device.write_key<"temperature">(30.0); +``` + +这些操作属于 `Managed_Access_Mode::internal`。 + +`read()` 返回值对象而不是底层存储引用。启用同步时,读取发生在配置的 shared lock 持有期间。 + +## 11. External Capability View + +可以通过 object defaults 默认开放 external access: + +```cpp +defaults(external_access) +``` + +使用: + +```cpp +device.external().write<&Device::temperature>(30.0); +auto value = device.external().read<&Device::temperature>(); +``` + +Const object 返回 const capability view,因此没有 write API。 + +Typed API 对静态不可访问操作直接在编译期拒绝。 + +## 12. Persistence Capability View + +Persistence 使用 load/store 语义: + +```cpp +device.persistence().load<&Device::temperature>(30.0); +auto value = device.persistence().store<&Device::temperature>(); +``` + +编译期 key 版本: + +```cpp +device.persistence().load_key<"temperature">(30.0); +auto value = device.persistence().store_key<"temperature">(); +``` + +能力由 `Persistence_Access` 元数据决定。 + +## 13. Synchronization Plan + +### 13.1 Independent + +默认 `Synchronization_Plan` 就是 independent:每个同步 property 拥有独立逻辑锁域。 + +显式写法: + +```cpp +synchronization(sync_all_independent) +``` + +### 13.2 Shared + +```cpp +synchronization(sync_all_shared) +``` + +除显式 override 外,全部 property 共用一个 lock slot。 + +### 13.3 Unsynchronized + +```cpp +synchronization(sync_all_unsynchronized) +``` + +property 使用 `unsynchronized_slot`,Structive 不提供真实 mutex 保护。 + +### 13.4 单 Property Override + +成员指针形式: + +```cpp +sync_independent<&Device::temperature>() +sync_unsynchronized<&Device::immutable_id>() +``` + +运行时字符串形式: + +```cpp +sync_independent("temperature") +sync_unsynchronized("immutable_id") +``` + +静态已知 property 优先成员指针形式。 + +### 13.5 Group + +成员指针形式: + +```cpp +sync_group<&Device::min_speed, &Device::max_speed>("speed_range") +``` + +字符串形式: + +```cpp +sync_group("speed_range", "min_speed", "max_speed") +``` + +同一 group 的 property 共用 lock slot。 + +Resolver 会拒绝未知 key、重复 group name、空 group,以及同一 property 被重复显式配置。 + +## 14. 每实例覆盖同步策略 + +Type Descriptor 提供默认同步计划,但单个实例可以通过 `Property_Synchronization` 覆盖: + +```cpp +struct Device : Property_Object { + Device() = default; + explicit Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {} +}; +Device shared_device{ + property_synchronization(synchronization(sync_all_shared)) +}; +``` + +如果使用成员指针同步规则,可以显式提供对象类型完成 Schema materialization: + +```cpp +auto policy = property_synchronization( + synchronization( + sync_all_independent, + sync_group<&Device::temperature, &Device::pressure>("environment") + ) +); +``` + +Schema 本身不变,只改变该实例的 lock topology。 + +## 15. 查看解析后的同步拓扑 + +单字段: + +```cpp +auto slot = device.lock_slot<&Device::temperature>(); +``` + +完整 view: + +```cpp +auto resolved = device.resolved_synchronization(); +auto count = resolved.lock_count; +auto slot = resolved.slot(0); +auto uses_lock = resolved.uses_lock(0); +``` + +`Resolved_Synchronization_View::unsynchronized_slot` 表示无同步 property。 + +## 16. 静态多属性 Guard + +Shared guard: + +```cpp +auto guard = device.lock_shared<&Device::temperature, &Device::pressure>(); +auto temperature = guard.get<&Device::temperature>(); +auto pressure = guard.get<&Device::pressure>(); +``` + +Unique guard: + +```cpp +auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); +auto old_min = guard.get<&Device::min_speed>(); +guard.set<&Device::min_speed>(20.0); +guard.set<&Device::max_speed>(120.0); +``` + +Guard 只允许访问自己持有同步集合中的 property。 + +Capability view 也提供受其 access mode 限制的静态 guard: + +```cpp +auto guard = device.external().lock_shared<&Device::temperature>(); +``` + +## 17. 动态 Key Guard + +运行时才知道属性集合时: + +```cpp +std::array keys{"temperature", "pressure"}; +auto guard = device.lock_shared(keys); +``` + +或者: + +```cpp +auto guard = device.lock_unique({"min_speed", "max_speed"}); +``` + +动态 guard 会在运行时检查 key 和 capability。获取锁前会对 lock slot 排序、去重。 + +Guard 获取以后仍然可以通过编译期成员或 key 读写,但访问未包含在 held set 中的 property 会抛出 `std::logic_error`。 + +## 18. Traversal + +Schema 遍历: + +```cpp +schema.for_each_property([](auto index, const auto& property) { + // compile-time index + descriptor +}); +``` + +Managed value 遍历: + +```cpp +device.for_each_readable([](auto index, const auto& descriptor, const auto& value) { + // 每个 property 单独 managed read +}); +``` + +Locked traversal 会先获取完整 readable 同步集合: + +```cpp +device.external().for_each_readable_locked([](auto index, const auto& descriptor, const auto& value) { + // 全部选中 property 在 guard 下访问 +}); +``` + +集中 writable 操作: + +```cpp +device.with_all_writable_locked([](auto& guard) { + // guard.get / guard.set +}); +``` + +Persistence view 对应提供 `with_all_loadable_locked(...)`。 + +## 19. Computed Property + +同步 computed property 接收 read view: + +```cpp +computed_property([](const auto& view) { + return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); +}, key<"speed_span">, external_access) +``` + +Computed property 自身与通过 view 读取的每个 dependency 必须解析到同一个 lock slot。 + +例如: + +```cpp +synchronization( + sync_all_independent, + sync_group("speed", "min_speed", "max_speed", "speed_span") +) +``` + +Computed property 是只读属性。 + +## 20. Trusted Accessor Property + +Core 还支持基于成员函数的 trusted access: + +```cpp +trusted_computed_property<&Device::get_temperature>(key<"temperature">) +``` + +以及 getter/setter: + +```cpp +trusted_accessor_property<&Device::get_temperature, &Device::set_temperature>(key<"temperature">) +``` + +它们直接调用对象成员函数,并标记为 trusted object access,不受 synchronized computed view 的 dependency slot 检查。只有当成员函数本身拥有明确同步/一致性契约时才应该使用。 + +## 21. Runtime Type-Erased Access + +任何 `Property_Object` 同时也是 `Property_Object_Base`: + +```cpp +Property_Object_Base& erased = device; +``` + +Runtime introspection: + +```cpp +erased.runtime_object_type(); +erased.runtime_property_count(); +``` + +Runtime write: + +```cpp +double value = 35.0; +auto result = erased.runtime_write( + Managed_Access_Mode::external, + "temperature", + typeid(double), + &value +); +``` + +Runtime read 通过 callback 返回值: + +```cpp +static void read_double(void* context, std::size_t, std::string_view, const std::type_info& type, const void* value) { + if (type == typeid(double)) { + *static_cast(context) = *static_cast(value); + } +} +double output = 0.0; +auto result = erased.runtime_read( + Managed_Access_Mode::external, + "temperature", + &output, + &read_double +); +``` + +结果枚举: + +```cpp +Runtime_Access_Result::ok +Runtime_Access_Result::unknown_property +Runtime_Access_Result::not_readable +Runtime_Access_Result::not_writable +Runtime_Access_Result::type_mismatch +``` + +Runtime path 与 typed managed access 使用同一套 capability 和 synchronization 规则。 + +## 22. Lock Policy + +默认: + +```cpp +struct Device : Property_Object { +}; +``` + +等价简写: + +```cpp +struct Device : Property_Object { +}; +``` + +也提供 no-op lock policy: + +```cpp +struct Device : Property_Object { +}; +``` + +`No_Lock_Policy` 使用 `Null_Shared_Mutex`,它取消真实互斥;只有外部所有权规则能够保证正确性时才应该使用。 + +## 23. Raw Object Access + +`unsafe_object()` 可以拿到底层 derived object: + +```cpp +Device& raw = device.unsafe_object(); +``` + +普通成员访问当然也仍然存在: + +```cpp +device.temperature = 40.0; +``` + +这些入口故意绕过 Structive managed access。适合调用方已经持有正确同步、或者明确在 Structive 管理契约之外操作的情况。 + +## 24. 推荐使用规则 + +- Typed 业务访问优先 member pointer。 +- Runtime integration boundary 使用 string key。 +- Key 按协议标识对待。 +- 稳定的对象级 capability 放 `defaults(...)`,特殊字段单独 override。 +- 需要一致观察的字段放同一个 synchronization group。 +- Validation 在业务操作边界显式执行。 +- 跨字段 invariant 使用静态 multi-property guard。 +- 默认优先 synchronized computed property。 +- Runtime access 用于 adapter,不要把普通 typed code 动态化。 +- Raw access 只在调用方明确拥有被绕过的管理保证时使用。 diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..d3d742f --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,424 @@ +# Structive Design Philosophy and Principles + +[中文](DESIGN.zh-CN.md) + +## 1. Purpose + +Structive exists to give ordinary C++ object types a machine-readable structural layer without forcing those types into a framework-specific storage model. + +The library should make it possible to ask questions such as: + +- Which members are part of the public structural model? +- What is the stable runtime key of a property? +- Which properties are externally readable or writable? +- Which properties participate in persistence? +- Which metadata belongs to presentation, validation or another extension? +- Which properties share a synchronization domain? +- How can generic runtime code read or write a property safely with respect to declared capabilities? + +It should answer those questions while leaving the underlying type recognizably C++. + +## 2. Primary principle: enhance, do not replace + +Structive is not a replacement object model. It is a structural layer attached to an existing C++ type. + +The preferred shape is: + +```cpp +struct Device : structive::Property_Object { + double temperature; + std::string name; +}; +``` + +not: + +```cpp +struct Device { + Framework_Property temperature; + Framework_Property name; +}; +``` + +This principle protects several properties of normal C++ code: + +1. Members remain real members. +2. Member pointers remain meaningful identities. +3. Raw access remains possible where ownership rules permit it. +4. Unregistered members can remain implementation details. +5. Generic Structive metadata can evolve independently from storage representation. + +The cost of this principle is intentional: Structive cannot intercept raw member access. Managed behavior only applies when callers use the managed path. + +## 3. Two layers, not one + +Structive separates type-level description from instance-level management. + +### 3.1 Type layer + +`Type_Descriptor` produces an `Object_Schema` describing: + +- registered properties; +- declared property keys; +- accessors; +- Attributes; +- constraints; +- object-level inheritable defaults; +- a default synchronization plan. + +This is the structural definition of the type. + +### 3.2 Instance layer + +`Property_Object` provides: + +- resolved lock-slot topology; +- per-instance mutex storage; +- managed typed reads and writes; +- capability views; +- static and runtime multi-property guards; +- traversal over managed values; +- type-erased runtime access through `Property_Object_Base`. + +This is instance behavior, not schema identity. + +### 3.3 Design rule + +Do not move instance state into the schema, and do not make schema metadata depend on one particular instance-management policy. + +A future user may want to describe a large number of plain objects without paying per-object synchronization cost. The architecture should continue to leave that possibility open. + +## 4. Registration is explicit + +Structive does not assume every C++ member belongs to the structural model. + +```cpp +struct Device : structive::Property_Object { + int id; + double temperature; + mutable int internal_cache; +}; +``` + +If only `id` and `temperature` are registered, `internal_cache` is invisible to Structive. + +This is deliberate. Structural exposure is an API decision and should not be inferred from physical layout. + +### Rule + +**The schema is the public structural contract; the struct layout is not automatically the schema.** + +## 5. Prefer compile-time identity inside C++ + +Business C++ code should normally identify member-backed properties by member pointer: + +```cpp +device.read<&Device::temperature>(); +device.write<&Device::temperature>(30.0); +device.schema().property<&Device::temperature>(); +``` + +This gives the compiler the strongest relationship between the object type and the selected field. + +Numeric indexes are useful for generic compile-time traversal. String keys are useful at runtime boundaries. + +### Identity hierarchy + +```text +business C++ code → member pointer +compile-time generic code → property index +runtime/adapters → declared string key +``` + +Do not unnecessarily convert compile-time code to strings merely because a key exists. + +## 6. Keys are structural protocol identifiers + +Every `Property_Descriptor` requires a non-empty `key` Attribute. Keys must be unique inside one schema. + +A key is not merely a UI label. It is the runtime identity used by schema lookup, dynamic lock selection and runtime access. + +Changing a key may therefore change an external protocol or persistence contract even when the C++ member name stays unchanged. + +### Rule + +**Treat property keys as protocol-level names. Rename them deliberately.** + +## 7. One Attribute system + +Structive has one Attribute protocol. It should not grow parallel metadata channels such as “hint”, “annotation”, “UI metadata” and “serializer metadata” with separate storage machinery. + +An Attribute may declare: + +- `attribute_category` for category-based lookup; +- `single_valued` when only one Attribute of that category may appear in a declaration; +- `inheritable` when it may participate in object defaults; +- any payload required by the category owner. + +Core and extensions use this same mechanism. + +### Rule + +**Add semantics by adding an Attribute category and an interpreter, not by adding a second metadata framework.** + +## 8. Category ownership + +Core should interpret only categories that belong to Core. + +For example, Core understands access and persistence capability because those affect Core-managed views. It does not need to understand `presentation::label`. + +Presentation owns presentation semantics. A future JSON extension should own JSON-specific semantics. An RPC extension should own RPC-specific semantics. + +The Core still stores and traverses all Attributes uniformly. + +### Dependency rule + +```text +extension implementation + ↓ +Structive Property Core +``` + +Never reverse this dependency merely to make an extension convenient. + +## 9. Defaults are inheritance, not hidden mutation + +`defaults(...)` supports inheritable Attribute categories. Property declarations remain able to override those defaults. + +Current Core inheritable categories include external access, persistence access and sensitivity. + +This mechanism should be used for stable object-wide policy defaults, not as a general mechanism for implicit behavior. + +### Rule + +**Defaults should reduce repetition without making a property’s effective policy impossible to determine from schema rules.** + +## 10. Managed access and raw access are distinct contracts + +Raw access: + +```cpp +device.temperature = 30.0; +``` + +Managed access: + +```cpp +device.write<&Device::temperature>(30.0); +``` + +The raw path is normal C++. It does not acquire Structive locks or enforce Structive capability views. + +The managed path uses the descriptor and synchronization topology. + +This duality is intentional rather than accidental. + +### Rule + +Do not pretend that inheriting `Property_Object` turns public C++ members into encapsulated properties. If a subsystem requires managed synchronization, its coding rules must require the managed path. + +## 11. Capabilities are views, not copies of the object model + +The same property may have different visibility under different managed modes: + +```text +internal +external +persistence +``` + +Core derives those capabilities from Attributes and the accessor’s actual read/write abilities. + +An external view should not become a second schema. A persistence view should not become a second schema. They are projections over one schema. + +### Rule + +**One property definition, multiple capability projections.** + +## 12. Validation is metadata plus an explicit operation + +Structive constraints describe candidate validity, but `write()` does not automatically run them. + +This separation is essential because these are different concerns: + +```text +single-property constraint +cross-property invariant +locking +transaction boundary +rollback strategy +error reporting +side effects +``` + +A generic `write()` cannot correctly guess all of them. + +### Example + +```cpp +auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); +auto old_min = guard.get<&Device::min_speed>(); +auto old_max = guard.get<&Device::max_speed>(); +guard.set<&Device::min_speed>(candidate_min); +guard.set<&Device::max_speed>(candidate_max); +if (candidate_min > candidate_max) { + guard.set<&Device::min_speed>(old_min); + guard.set<&Device::max_speed>(old_max); +} +``` + +The business operation owns the invariant and rollback semantics. + +### Rule + +**Do not turn `write()` into a hidden transaction engine.** + +## 13. Synchronization is synchronization only + +A synchronization plan maps properties to lock slots. It supports independent, shared, unsynchronized and grouped configurations. + +The purpose of a group is to define a consistency domain: properties in the group share a mutex slot. + +Multi-property lock operations deduplicate slots and acquire them in stable slot order. + +### Synchronization does not mean + +- validation; +- transaction; +- rollback; +- event emission; +- dirty tracking; +- persistence commit. + +Those may be built above Structive, but should not be silently coupled to locking. + +## 14. Computed properties must have explicit consistency boundaries + +`computed_property` reads through a synchronized view. The view only permits access to properties in the same resolved lock slot. + +Therefore a computed property that depends on `min_speed` and `max_speed` should share their synchronization group: + +```cpp +synchronization( + sync_all_independent, + sync_group("speed", "min_speed", "max_speed", "speed_span") +) +``` + +This makes consistency explicit rather than relying on a getter that casually reads unrelated fields. + +### Trusted accessors + +`trusted_computed_property` and `trusted_accessor_property` invoke trusted member functions on the object. They bypass synchronized-view dependency checking by design. + +### Rule + +**Use synchronized computed properties by default. Use trusted accessors only when the accessor itself owns or guarantees the required synchronization semantics.** + +## 15. Runtime access is a boundary feature + +`Property_Object_Base` intentionally provides a type-erased runtime interface using: + +- string key; +- `Managed_Access_Mode`; +- `std::type_info`; +- explicit result codes. + +This is appropriate for adapters that do not know the concrete type at compile time. + +It is not a reason to make normal typed C++ code dynamic. + +### Rule + +**Keep compile-time code compile-time; cross dynamic boundaries only where the application actually has a dynamic boundary.** + +## 16. Runtime errors and compile-time errors have different jobs + +Structive uses compile-time rejection where the selection is statically known: + +- missing member registration; +- duplicate keys; +- duplicate member storage identity; +- invalid Attribute category duplication; +- non-inheritable Attributes inside `defaults(...)`; +- impossible access capability for an accessor. + +Runtime failures are reserved for runtime inputs and runtime configuration: + +- unknown dynamic key; +- inaccessible dynamic property; +- invalid synchronization plan key; +- duplicate runtime synchronization configuration; +- runtime type mismatch. + +### Rule + +**Do not defer a statically knowable schema error to runtime. Do not force dynamic adapter input into compile-time machinery.** + +## 17. Synchronization policy should remain replaceable + +`Property_Object` supports a lock policy, with `Shared_Mutex_Policy` as the default and `No_Lock_Policy` available. + +This is an important architectural seam. Core semantics should not become inseparable from one specific mutex type or one global scheduler. + +`No_Lock_Policy` removes actual mutual exclusion; it does not magically make concurrent access safe. + +## 18. Extension design rules + +A good Structive extension should: + +1. Define categories that belong to its own domain. +2. Reuse the Core Attribute protocol. +3. Interpret only the categories it owns or explicitly depends on. +4. Depend on Core, never require Core to depend on it. +5. Keep domain-specific fallback behavior inside the extension. +6. Prefer reading the existing schema over duplicating a parallel descriptor tree. +7. Avoid changing Core behavior merely because an adapter needs a convenience rule. + +## 19. Non-goals + +Structive Core should not become all of the following at once: + +- an ORM; +- a JSON library; +- a GUI framework; +- an RPC framework; +- a signal/slot system; +- a transaction engine; +- a general-purpose reflection replacement for every language feature. + +Structive should provide a strong structural contract that those systems can consume. + +## 20. Evolution rules + +When changing the library, apply these questions in order: + +1. Does this belong to the structural model, instance management, or an extension? +2. Is this new concept really an Attribute category rather than a new metadata channel? +3. Can the error be detected at compile time? +4. Does the change preserve native C++ member semantics? +5. Does it preserve the distinction between raw and managed access? +6. Is synchronization being mixed with validation, transaction or events? +7. Does Core now know something that should belong to an extension? +8. Does the API still make member pointers the natural typed identity? +9. Does a new convenience API duplicate an existing semantic path? +10. Does the change preserve existing function-signature meaning rather than silently redefining it? + +If a feature fails these questions, reconsider its layer before implementing it. + +## 21. Architectural summary + +Structive should remain a small number of strong concepts rather than a large number of magical conveniences: + +```text +ordinary C++ type + + explicit schema + + one Attribute model + + explicit capabilities + + explicit validation + + explicit synchronization + + optional managed instance behavior + + extension-owned interpretation +``` + +The library is strongest when generic systems can understand a type without taking ownership of that type. diff --git a/docs/DESIGN.zh-CN.md b/docs/DESIGN.zh-CN.md new file mode 100644 index 0000000..f9bc691 --- /dev/null +++ b/docs/DESIGN.zh-CN.md @@ -0,0 +1,424 @@ +# Structive 设计理念与原则 + +[English](DESIGN.md) + +## 1. 设计目标 + +Structive 的目标是给普通 C++ 对象增加一层机器可理解的结构语义,同时不强迫业务对象改用框架自己的存储模型。 + +它应该能够回答: + +- 哪些成员属于公开结构模型? +- 某个属性稳定的 runtime key 是什么? +- 哪些属性允许外部读取或写入? +- 哪些属性参与持久化? +- 哪些元数据属于 Presentation、Validation 或其他扩展? +- 哪些属性位于同一个同步域? +- 动态适配器如何在遵守 capability 的前提下读写属性? + +但完成这些事情以后,底层类型仍然应该看起来像正常的 C++。 + +## 2. 第一原则:增强,而不是替代 + +Structive 不是第二套对象模型,而是附加在已有 C++ 类型旁边的结构层。 + +推荐形态: + +```cpp +struct Device : structive::Property_Object { + double temperature; + std::string name; +}; +``` + +而不是: + +```cpp +struct Device { + Framework_Property temperature; + Framework_Property name; +}; +``` + +这个原则保护了原生 C++ 的几个关键性质: + +1. 成员仍然是真实成员。 +2. 成员指针仍然可以作为可靠身份。 +3. 所有权规则允许时仍然可以 raw access。 +4. 未注册成员可以继续作为实现细节存在。 +5. Structive 元数据可以和存储形式独立演进。 + +这个原则也有明确代价:Structive 不可能拦截直接成员访问。只有通过 managed path 的访问才会获得 Structive 管理行为。 + +## 3. 必须保持两层,而不是揉成一层 + +Structive 把类型级描述和实例级管理分开。 + +### 3.1 类型层 + +`Type_Descriptor` 产生 `Object_Schema`,描述: + +- 注册属性; +- 属性 key; +- accessor; +- Attribute; +- constraint; +- object 级可继承默认值; +- 默认 synchronization plan。 + +这是类型的结构定义。 + +### 3.2 实例层 + +`Property_Object` 提供: + +- 解析后的 lock slot 拓扑; +- 每实例 mutex 存储; +- managed typed read/write; +- capability view; +- 静态与运行时多属性 guard; +- managed value traversal; +- `Property_Object_Base` type-erased runtime access。 + +这是实例行为,不是 Schema 身份。 + +### 3.3 设计约束 + +不要把实例状态塞进 Schema,也不要让 Schema 必须依赖某一种特定的实例管理策略。 + +以后完全可能有用户只想描述大量普通对象,却不愿意为每个对象承担同步状态成本。当前架构应该持续保留这种可能性。 + +## 4. 注册必须显式 + +Structive 不认为所有 C++ 成员都天然属于结构模型。 + +```cpp +struct Device : structive::Property_Object { + int id; + double temperature; + mutable int internal_cache; +}; +``` + +如果 Schema 只注册 `id` 和 `temperature`,那么 `internal_cache` 对 Structive 完全不可见。 + +这是故意的。结构暴露本身就是 API 设计,不应该根据物理布局自动推断。 + +### 原则 + +**Schema 才是公开结构契约;struct 的物理成员集合不是自动 Schema。** + +## 5. C++ 内部优先使用编译期身份 + +业务 C++ 代码通常应该通过成员指针定位 member-backed property: + +```cpp +device.read<&Device::temperature>(); +device.write<&Device::temperature>(30.0); +device.schema().property<&Device::temperature>(); +``` + +这样编译器能够建立最强的“对象类型—字段”关系。 + +数字 index 适合编译期泛型遍历;字符串 key 适合运行时边界。 + +### 身份层级 + +```text +业务 C++ 代码 → member pointer +编译期泛型代码 → property index +runtime / adapter → declared string key +``` + +不要因为存在 key,就把本来可以静态确定的 C++ 业务代码全部字符串化。 + +## 6. Key 是结构协议标识,不是显示名称 + +每个 `Property_Descriptor` 都必须拥有非空 `key`,同一 Schema 内 key 必须唯一。 + +Key 被 Schema lookup、动态锁选择、runtime read/write 使用。因此它不只是 UI label。 + +即使 C++ 成员名不变,修改 key 也可能改变外部协议或持久化契约。 + +### 原则 + +**把 property key 当成协议级名字,重命名必须有意识。** + +## 7. 只有一套 Attribute 系统 + +Structive 不应该再长出 `hint`、`annotation`、`ui metadata`、`serializer metadata` 等彼此独立的元数据容器。 + +一个 Attribute 可以声明: + +- `attribute_category`:用于按 category 查询; +- `single_valued`:同一个声明中该 category 是否只能有一个值; +- `inheritable`:是否允许进入 object defaults; +- category 自己需要的 payload。 + +Core 和 Extension 使用完全相同的协议。 + +### 原则 + +**增加语义时优先增加 Attribute category + interpreter,而不是再造一套 metadata framework。** + +## 8. Category 必须有明确所有者 + +Core 只解释属于 Core 的 category。 + +例如 External Access 和 Persistence Access 会直接影响 Core capability view,所以 Core 理解它们是合理的;但 Core 不需要理解 `presentation::label`。 + +Presentation 解释 Presentation;未来 JSON 扩展解释 JSON;RPC 扩展解释 RPC。 + +Core 只负责统一保存、遍历和提供查询机制。 + +### 依赖约束 + +```text +Extension implementation + ↓ +Structive Property Core +``` + +不能为了某个扩展写起来方便,就反过来让 Core 依赖 Extension。 + +## 9. Defaults 是继承,不是隐藏行为 + +`defaults(...)` 用于支持可继承 Attribute。Property 级声明仍然可以覆盖同 category 的默认值。 + +当前 Core 中可继承的 category 包括 External Access、Persistence Access 和 Sensitive。 + +Defaults 应该用于稳定的对象级政策默认值,而不是用来制造大量隐式行为。 + +### 原则 + +**Defaults 用来减少重复,但不能让一个属性最终生效的政策变得无法从 Schema 规则中推导。** + +## 10. Managed Access 与 Raw Access 是两种契约 + +Raw access: + +```cpp +device.temperature = 30.0; +``` + +Managed access: + +```cpp +device.write<&Device::temperature>(30.0); +``` + +Raw path 就是普通 C++,不会自动获得 Structive 锁和 capability 管理。 + +Managed path 会经过 descriptor 和该实例的同步拓扑。 + +两条路径同时存在是设计结果,不是漏洞。 + +### 原则 + +不要假装继承 `Property_Object` 以后 public member 就自动变成强封装属性。如果某个子系统要求受管理同步,那么该子系统自己的编码约束必须要求使用 managed path。 + +## 11. Capability 是 Schema 的投影视图,不是第二套 Schema + +同一个属性在不同 managed mode 下可以拥有不同可见性: + +```text +internal +external +persistence +``` + +Core 根据 Attribute 和 accessor 实际读写能力计算这些 capability。 + +External view 不应该发展成第二份 Schema;Persistence view 也不应该成为第二份 Schema。它们都只是同一 Schema 的能力投影。 + +### 原则 + +**一份 property definition,多种 capability projection。** + +## 12. Validation = 元数据 + 显式操作 + +Constraint 描述候选值是否合法,但 `write()` 不会自动执行它。 + +必须保持分开的概念包括: + +```text +单字段 constraint +跨字段 invariant +locking +transaction boundary +rollback strategy +error reporting +side effects +``` + +一个通用 `write()` 不可能替所有业务正确猜出这些规则。 + +### 示例 + +```cpp +auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); +auto old_min = guard.get<&Device::min_speed>(); +auto old_max = guard.get<&Device::max_speed>(); +guard.set<&Device::min_speed>(candidate_min); +guard.set<&Device::max_speed>(candidate_max); +if (candidate_min > candidate_max) { + guard.set<&Device::min_speed>(old_min); + guard.set<&Device::max_speed>(old_max); +} +``` + +业务操作自己拥有 invariant 和 rollback 语义。 + +### 原则 + +**不要把 `write()` 变成隐藏事务引擎。** + +## 13. Synchronization 就只负责 Synchronization + +Synchronization plan 的本质是把 property 映射到 lock slot,支持 independent、shared、unsynchronized、grouped 等配置。 + +Group 表示一致性域:同一个 group 的 property 使用同一个 mutex slot。 + +多属性锁会对 slot 去重,并按稳定 slot 顺序获取锁。 + +### Synchronization 不等于 + +- validation; +- transaction; +- rollback; +- event emission; +- dirty tracking; +- persistence commit。 + +这些能力可以构建在 Structive 上层,但不能偷偷和锁耦合。 + +## 14. Computed Property 必须明确一致性边界 + +`computed_property` 通过 synchronized view 读取依赖项。这个 view 只允许读取和 computed property 本身处于同一个 resolved lock slot 的属性。 + +所以依赖 `min_speed`、`max_speed` 的 `speed_span` 应该和它们进入同一个同步组: + +```cpp +synchronization( + sync_all_independent, + sync_group("speed", "min_speed", "max_speed", "speed_span") +) +``` + +这样一致性关系由同步计划明确表达,而不是让 getter 随意读取任意字段。 + +### Trusted Accessor + +`trusted_computed_property` 和 `trusted_accessor_property` 会直接调用对象上的受信任成员函数,设计上绕开 synchronized view 的依赖检查。 + +### 原则 + +**默认优先 synchronized computed property。只有 accessor 自身明确拥有或保证同步语义时才使用 trusted accessor。** + +## 15. Runtime Access 是边界能力 + +`Property_Object_Base` 提供 type-erased runtime interface,核心输入包括: + +- string key; +- `Managed_Access_Mode`; +- `std::type_info`; +- 显式 result code。 + +它适用于编译期不知道具体类型的 adapter。 + +它不应该成为把所有正常 typed C++ 代码动态化的理由。 + +### 原则 + +**能在编译期确定的代码就留在编译期;只有真正跨动态边界时才进入 runtime path。** + +## 16. Compile-time Error 和 Runtime Error 分工明确 + +Structive 对静态可知问题尽量在编译期拒绝: + +- member 未注册; +- key 重复; +- 同一成员存储被重复注册; +- single-valued category 重复; +- 非 inheritable Attribute 被放入 `defaults(...)`; +- accessor 实际能力与声明 capability 冲突。 + +Runtime failure 留给 runtime 输入和 runtime 配置: + +- 动态 key 不存在; +- 动态属性对该 view 不可访问; +- synchronization plan 引用了未知 key; +- runtime 同步规则重复配置同一个 property; +- runtime type mismatch。 + +### 原则 + +**静态可知的 Schema 错误不要拖到运行时;真正动态的 adapter 输入也不要硬伪装成编译期问题。** + +## 17. 同步策略必须保持可替换 + +`Property_Object` 支持同步策略,默认是 `Shared_Mutex_Policy`,同时提供 `No_Lock_Policy`。 + +这是重要架构缝隙。Core 语义不应该和某一种 mutex 或某一个全局 scheduler 焊死。 + +`No_Lock_Policy` 只是取消真实互斥,不代表并发访问自动安全。 + +## 18. Extension 设计规则 + +一个好的 Structive Extension 应该: + +1. 定义属于自己领域的 category。 +2. 复用 Core Attribute 协议。 +3. 只解释自己拥有或明确依赖的 category。 +4. 依赖 Core,而不是要求 Core 反向依赖自己。 +5. 把领域 fallback 行为留在扩展里。 +6. 优先消费已有 Schema,不复制第二棵 descriptor tree。 +7. 不因为 adapter 需要便利规则就修改 Core 基础语义。 + +## 19. Core 的非目标 + +Structive Core 不应该同时变成: + +- ORM; +- JSON 库; +- GUI 框架; +- RPC 框架; +- signal/slot 系统; +- 事务引擎; +- 替代所有语言能力的“万能反射”。 + +Structive 应该提供足够强的结构契约,让这些系统来消费它。 + +## 20. 演进原则 + +修改库时依次问: + +1. 这是 structural model、instance management 还是 extension 的职责? +2. 这个新概念本质上是不是一个 Attribute category,而不是新的 metadata channel? +3. 这个错误能否在编译期发现? +4. 修改后是否仍然保留原生 C++ 成员语义? +5. Raw access 与 managed access 的边界是否仍然清晰? +6. 是否把 synchronization 和 validation、transaction、event 混在一起了? +7. Core 是否开始理解本应属于 Extension 的领域概念? +8. Member pointer 是否仍然是 typed API 最自然的身份? +9. 新的 convenience API 是否重复制造了另一条同语义路径? +10. 是否保持了已有函数签名和功能语义,而不是悄悄改义? + +如果一个功能连续违反这些问题,应先重新考虑它属于哪一层,而不是立刻实现。 + +## 21. 架构总结 + +Structive 最强的形态应该是少量强概念,而不是大量魔法便利接口: + +```text +普通 C++ 类型 + + 显式 Schema + + 一套 Attribute 模型 + + 显式 Capability + + 显式 Validation + + 显式 Synchronization + + 可选 Managed Instance 行为 + + Extension 自己解释自己的语义 +``` + +当泛型系统能够理解业务类型、却不需要接管业务类型时,Structive 的价值最大。 diff --git a/docs/EXTENSIONS.md b/docs/EXTENSIONS.md new file mode 100644 index 0000000..6121671 --- /dev/null +++ b/docs/EXTENSIONS.md @@ -0,0 +1,267 @@ +# Structive Extension Architecture + +[中文](EXTENSIONS.zh-CN.md) + +## 1. Extension role + +Structive extensions add domain-specific interpretation without changing the Core structural model. + +The intended dependency is: + +```text +application / adapter + ↓ +Structive extension + ↓ +Structive Property Core +``` + +Core does not include or link extension code. + +## 2. Extension principle + +An extension should normally add two things: + +1. one or more Attribute categories that express domain metadata; +2. interpretation code that consumes those Attributes. + +It should not create a parallel property registry when the existing `Object_Schema` already contains the required structure. + +## 3. Current presentation extension + +The current extension module defines four Attribute categories: + +```cpp +presentation::Label_Category +presentation::Description_Category +presentation::Group_Category +presentation::Order_Category +``` + +Convenience Attribute values: + +```cpp +presentation::label<"Temperature"> +presentation::description<"Current device temperature"> +presentation::group<"Environment"> +presentation::order<10> +``` + +They can be attached directly to a normal Core property: + +```cpp +field<&Device::temperature>( + key<"temperature">, + unit<"C">, + presentation::label<"Temperature">, + presentation::description<"Current device temperature">, + presentation::group<"Environment">, + presentation::order<10> +) +``` + +No wrapper such as `hint(...)` is required. + +## 4. Presentation interpretation + +Include: + +```cpp +#include +``` + +Link: + +```cmake +target_link_libraries(my_target PRIVATE structive::property_extensions) +``` + +Describe a property by member pointer: + +```cpp +auto info = presentation::describe<&Device::temperature>(device.schema()); +``` + +The result contains: + +```cpp +struct Presentation_Info { + std::string_view key; + std::string_view label; + std::string_view description; + std::string_view group; + std::size_t order; + bool has_order; +}; +``` + +If no presentation label is declared, the extension uses the property key as the display label. This fallback is presentation policy and intentionally lives outside Core. + +## 5. Inheritable extension Attributes + +The presentation `group` Attribute is inheritable, so it may be placed in `defaults(...)`: + +```cpp +return object( + defaults( + external_access, + presentation::group<"Environment"> + ), + field<&Device::temperature>( + key<"temperature">, + presentation::label<"Temperature"> + ), + field<&Device::pressure>( + key<"pressure">, + presentation::label<"Pressure"> + ) +); +``` + +The extension resolves an effective declared value through the same Core default mechanism. + +`label`, `description` and `order` are not inheritable and therefore cannot be placed in `defaults(...)`. + +## 6. Designing a new extension Attribute + +Example: + +```cpp +namespace my_adapter { +struct Json_Name_Category {}; +template +struct Json_Name_Attribute { + using attribute_category = Json_Name_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = false; + static constexpr auto value = Value; +}; +template +inline constexpr Json_Name_Attribute json_name{}; +} +``` + +Use it in a schema: + +```cpp +field<&Device::temperature>( + key<"temperature">, + my_adapter::json_name<"temp"> +) +``` + +The Core stores it without needing to know what JSON means. + +## 7. Reading extension Attributes + +For a property descriptor: + +```cpp +using Property = std::remove_cvref_t; +if constexpr (Property::has_attribute) { + const auto& value = property.attribute(); +} +``` + +For inheritable categories, use effective declared lookup: + +```cpp +if constexpr (has_declared_effective_attribute_v) { + const auto& value = declared_effective_attribute(schema); +} +``` + +This checks the property declaration first and then object defaults. + +## 8. Multi-valued metadata + +Core uniqueness is category-driven. An Attribute with `single_valued = true` and a non-void `attribute_category` may only appear once in one declaration. + +When a domain needs repeated annotations, design that metadata so it does not claim single-valued category uniqueness, then consume it through `for_each_attribute(...)`. + +Do not force naturally repeated metadata into one large unrelated object merely to satisfy a single-valued design. + +## 9. Recommended extension boundaries + +A future extension may reasonably own metadata and interpretation for areas such as: + +- serialization naming and omission rules; +- RPC exposure rules; +- UI labels, groups and editor hints; +- database column mapping; +- configuration-file mapping; +- domain documentation generation. + +These are examples of extension domains, not currently implemented features. + +The important boundary is that Core should not gain a direct dependency on their libraries or domain types. + +## 10. Core metadata may still be consumed + +An extension is allowed to interpret Core-owned categories when those categories are part of the extension's input contract. + +For example, an external adapter may use: + +- property `key` as the default protocol name; +- `External_Access` to determine exposure; +- `sensitive` to decide whether its own logs should omit a value. + +The extension may choose a policy based on those attributes, but it should not change what Core itself means by them. + +## 11. Fallback behavior belongs to the consumer + +The presentation extension demonstrates the intended rule: + +```text +no label Attribute + ↓ +presentation extension chooses property key as label +``` + +Core does not invent that fallback because a different consumer may want a different behavior. + +The same principle should apply to future adapters. Defaults that exist only to make one domain pleasant should remain in that domain. + +## 12. Avoid extension-to-Core semantic leakage + +Bad direction: + +```text +JSON adapter needs alias support + ↓ +Core adds JSON_Alias_Category and JSON naming logic +``` + +Preferred direction: + +```text +JSON extension defines Json_Name_Category + ↓ +JSON extension interprets it + ↓ +Core remains domain-neutral +``` + +## 13. Linked code versus header-only metadata + +Attribute definitions can often be header-only. Interpretation may be header-only or linked depending on implementation needs. + +The current presentation extension demonstrates a linked extension target. `presentation::describe()` performs compile-time Attribute selection and calls linked `make_presentation_info()` for the concrete result construction/fallback step. + +Do not move linked implementation into Core merely because the extension is small. + +## 14. Extension review checklist + +Before accepting a new extension feature, ask: + +1. Does this concept belong to Core or to one consumer domain? +2. Can it be represented through the existing Attribute protocol? +3. Does the extension own the Attribute category it interprets? +4. Is an inheritable Attribute truly an object-wide default policy? +5. Is fallback behavior kept inside the extension? +6. Is the existing schema reused rather than duplicated? +7. Does the dependency still point from extension to Core? +8. Has Core remained free of third-party domain types? +9. Does the extension preserve the existing meaning of Core access, validation and synchronization? + +If the answer to the dependency or semantic-boundary questions is no, the layering should be redesigned before code is added. diff --git a/docs/EXTENSIONS.zh-CN.md b/docs/EXTENSIONS.zh-CN.md new file mode 100644 index 0000000..8affe25 --- /dev/null +++ b/docs/EXTENSIONS.zh-CN.md @@ -0,0 +1,267 @@ +# Structive Extension 架构指南 + +[English](EXTENSIONS.md) + +## 1. Extension 的职责 + +Structive Extension 的作用是在不修改 Core 结构模型的前提下增加领域语义解释。 + +推荐依赖方向: + +```text +application / adapter + ↓ +Structive extension + ↓ +Structive Property Core +``` + +Core 不 include、不 link Extension。 + +## 2. Extension 的基本模式 + +一个 Extension 通常只需要增加两类东西: + +1. 一个或多个属于自己领域的 Attribute category; +2. 解释这些 Attribute 的实现代码。 + +如果已有 `Object_Schema` 已经包含需要的结构信息,就不应该再复制第二份 property registry。 + +## 3. 当前 Presentation Extension + +当前扩展模块定义四个 Attribute category: + +```cpp +presentation::Label_Category +presentation::Description_Category +presentation::Group_Category +presentation::Order_Category +``` + +对应便利 Attribute: + +```cpp +presentation::label<"Temperature"> +presentation::description<"Current device temperature"> +presentation::group<"Environment"> +presentation::order<10> +``` + +它们直接挂到普通 Core property 上: + +```cpp +field<&Device::temperature>( + key<"temperature">, + unit<"C">, + presentation::label<"Temperature">, + presentation::description<"Current device temperature">, + presentation::group<"Environment">, + presentation::order<10> +) +``` + +不需要 `hint(...)` 一类额外包装。 + +## 4. Presentation 解释 + +Include: + +```cpp +#include +``` + +Link: + +```cmake +target_link_libraries(my_target PRIVATE structive::property_extensions) +``` + +通过成员指针描述 property: + +```cpp +auto info = presentation::describe<&Device::temperature>(device.schema()); +``` + +返回: + +```cpp +struct Presentation_Info { + std::string_view key; + std::string_view label; + std::string_view description; + std::string_view group; + std::size_t order; + bool has_order; +}; +``` + +没有声明 presentation label 时,Presentation Extension 使用 property key 作为显示 label。这个 fallback 是 Presentation 自己的政策,不属于 Core。 + +## 5. 可继承 Extension Attribute + +`presentation::group` 是 inheritable,因此可以放入 `defaults(...)`: + +```cpp +return object( + defaults( + external_access, + presentation::group<"Environment"> + ), + field<&Device::temperature>( + key<"temperature">, + presentation::label<"Temperature"> + ), + field<&Device::pressure>( + key<"pressure">, + presentation::label<"Pressure"> + ) +); +``` + +Extension 通过 Core 同一套 default 机制解析有效声明。 + +`label`、`description`、`order` 不是 inheritable,因此不能放进 `defaults(...)`。 + +## 6. 设计一个新的 Extension Attribute + +例如: + +```cpp +namespace my_adapter { +struct Json_Name_Category {}; +template +struct Json_Name_Attribute { + using attribute_category = Json_Name_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = false; + static constexpr auto value = Value; +}; +template +inline constexpr Json_Name_Attribute json_name{}; +} +``` + +在 Schema 中直接使用: + +```cpp +field<&Device::temperature>( + key<"temperature">, + my_adapter::json_name<"temp"> +) +``` + +Core 负责存储它,但完全不需要知道 JSON 是什么。 + +## 7. 读取 Extension Attribute + +单个 property descriptor: + +```cpp +using Property = std::remove_cvref_t; +if constexpr (Property::has_attribute) { + const auto& value = property.attribute(); +} +``` + +可继承 category 使用 effective declared lookup: + +```cpp +if constexpr (has_declared_effective_attribute_v) { + const auto& value = declared_effective_attribute(schema); +} +``` + +它先检查 property declaration,再检查 object defaults。 + +## 8. Multi-Valued Metadata + +Core 的唯一性由 category 协议决定。拥有非 void `attribute_category` 且 `single_valued = true` 的 Attribute,在同一个声明中只能出现一次。 + +如果某个领域天然需要重复 annotation,就不要错误地宣称它是 single-valued category;可以通过 `for_each_attribute(...)` 遍历并消费多项数据。 + +不要为了满足 single-valued 设计,把本来可以重复的独立元数据硬塞进一个巨大对象。 + +## 9. 合理的未来 Extension 边界 + +未来 Extension 可以合理拥有例如: + +- serialization name / omission policy; +- RPC exposure rule; +- UI label / group / editor hint; +- database column mapping; +- configuration-file mapping; +- domain documentation generation。 + +这些只是合理领域示例,不代表当前已经实现。 + +关键边界是 Core 不能因此直接依赖这些领域库或类型。 + +## 10. Extension 可以消费 Core Metadata + +Extension 可以读取 Core-owned category,只要这些 category 本来就是它的输入契约。 + +例如 External Adapter 可以使用: + +- property `key` 作为默认协议名字; +- `External_Access` 判断是否暴露; +- `sensitive` 决定自己的日志是否隐藏值。 + +Extension 可以基于这些信息建立自己领域的政策,但不能改变 Core 对这些 Attribute 的基础含义。 + +## 11. Fallback 行为属于 Consumer + +Presentation Extension 当前就是示范: + +```text +没有 label Attribute + ↓ +Presentation Extension 使用 property key 作为 label +``` + +Core 不应该发明这个 fallback,因为另一个 consumer 完全可能需要另一种策略。 + +未来 Adapter 也应该遵守这个原则。只为了某个领域“好用”的默认逻辑应该留在那个领域内部。 + +## 12. 防止 Extension 语义泄漏进 Core + +错误方向: + +```text +JSON Adapter 需要 alias + ↓ +Core 增加 JSON_Alias_Category 和 JSON 命名逻辑 +``` + +正确方向: + +```text +JSON Extension 定义 Json_Name_Category + ↓ +JSON Extension 自己解释 + ↓ +Core 保持领域中立 +``` + +## 13. Header-Only Metadata 与 Linked Implementation + +Attribute 定义通常可以 header-only。具体解释逻辑可以根据实现需要选择 header-only 或 linked。 + +当前 Presentation Extension 使用 linked target:`presentation::describe()` 在模板层完成 Attribute 选择,然后调用链接实现 `make_presentation_info()` 生成最终结果并处理 fallback。 + +不能因为扩展代码量小,就把它的 linked implementation 搬进 Core。 + +## 14. Extension 审核清单 + +新增 Extension 功能前检查: + +1. 这个概念属于 Core,还是只属于某个 consumer domain? +2. 能否直接使用现有 Attribute 协议表达? +3. Extension 是否拥有自己解释的 category? +4. Inheritable Attribute 是否真的属于对象级默认政策? +5. Fallback 是否保留在 Extension 内? +6. 是否复用已有 Schema,而不是复制结构树? +7. 依赖是否仍然从 Extension 指向 Core? +8. Core 是否仍然没有第三方领域类型? +9. Extension 是否保持了 Core access、validation、synchronization 的原有语义? + +如果依赖方向或语义边界不成立,应先重新设计分层,再写代码。 diff --git a/extensions/CMakeLists.txt b/extensions/CMakeLists.txt new file mode 100644 index 0000000..6bbbadd --- /dev/null +++ b/extensions/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(structive_property_extensions STATIC "${CMAKE_CURRENT_LIST_DIR}/src/presentation.cpp") +add_library(structive::property_extensions ALIAS structive_property_extensions) +target_include_directories(structive_property_extensions PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include") +target_link_libraries(structive_property_extensions PUBLIC structive::property_core) +target_compile_features(structive_property_extensions PUBLIC cxx_std_20) +if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(structive_property_extensions PRIVATE -Wall -Wextra -Wpedantic) +endif () +add_executable(structive_property_extensions_example "${CMAKE_CURRENT_LIST_DIR}/example/main.cpp") +target_link_libraries(structive_property_extensions_example PRIVATE structive::property_extensions) +if (BUILD_TESTING) + add_executable(structive_property_extensions_test "${CMAKE_CURRENT_LIST_DIR}/tests/presentation_test.cpp") + target_link_libraries(structive_property_extensions_test PRIVATE structive::property_extensions) + if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(structive_property_extensions_test PRIVATE -Wall -Wextra -Wpedantic) + endif () + add_test(NAME structive_property_extensions_test COMMAND structive_property_extensions_test) +endif () diff --git a/extensions/README.md b/extensions/README.md new file mode 100644 index 0000000..81b0eaa --- /dev/null +++ b/extensions/README.md @@ -0,0 +1,11 @@ +# Structive Property Extensions + +`structive_property_extensions` is the linked extension layer above `structive_property_core`. + +Extensions reuse the same Core Attribute protocol, own their domain categories and interpret those categories without making Core depend on the domain. The current presentation extension provides `label`, `description`, `group` and `order` metadata plus `presentation::describe()`. + +See: + +- [Extension Architecture](../docs/EXTENSIONS.md) +- [Design Philosophy](../docs/DESIGN.md) +- [中文扩展架构](../docs/EXTENSIONS.zh-CN.md) diff --git a/extensions/README.zh-CN.md b/extensions/README.zh-CN.md new file mode 100644 index 0000000..b4de4e1 --- /dev/null +++ b/extensions/README.zh-CN.md @@ -0,0 +1,11 @@ +# Structive Property Extensions + +`structive_property_extensions` 是位于 `structive_property_core` 之上的链接型扩展层。 + +Extension 复用 Core 的同一套 Attribute 协议,拥有自己领域的 category,并在不让 Core 反向依赖领域概念的前提下解释这些 Attribute。当前 Presentation Extension 提供 `label`、`description`、`group`、`order` 以及 `presentation::describe()`。 + +完整文档: + +- [Extension 架构指南](../docs/EXTENSIONS.zh-CN.md) +- [设计理念与原则](../docs/DESIGN.zh-CN.md) +- [Extension Architecture](../docs/EXTENSIONS.md) diff --git a/extensions/example/main.cpp b/extensions/example/main.cpp new file mode 100644 index 0000000..4651e39 --- /dev/null +++ b/extensions/example/main.cpp @@ -0,0 +1,24 @@ +#include +#include +using namespace structive; +struct Device : Property_Object { + double temperature{25.0}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + field < &Device::temperature > ( + key < "temperature" >, + unit < "C" >, + presentation::label < "Temperature" >, + presentation::description < "Current device temperature" > + ) + ); + } +}; +int main() { + Device device; + auto info = presentation::describe < &Device::temperature > (device.schema()); + std::cout << info.key << '=' << info.label << '\n'; +} diff --git a/extensions/include/structive/property/extensions/presentation.hpp b/extensions/include/structive/property/extensions/presentation.hpp new file mode 100644 index 0000000..53e031a --- /dev/null +++ b/extensions/include/structive/property/extensions/presentation.hpp @@ -0,0 +1,88 @@ +#pragma once +#include +#include +#include +#include +namespace structive::presentation { +struct Label_Category {}; +struct Description_Category {}; +struct Group_Category {}; +struct Order_Category {}; +template +struct Label_Attribute { + using attribute_category = Label_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = false; + static constexpr auto value = Value; +}; +template +struct Description_Attribute { + using attribute_category = Description_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = false; + static constexpr auto value = Value; +}; +template +struct Group_Attribute { + using attribute_category = Group_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = true; + static constexpr auto value = Value; +}; +template +struct Order_Attribute { + using attribute_category = Order_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = false; + static constexpr auto value = Value; +}; +template +inline constexpr Label_Attribute label{}; +template +inline constexpr Description_Attribute description{}; +template +inline constexpr Group_Attribute group{}; +template +inline constexpr Order_Attribute order{}; +struct Presentation_Info { + std::string_view key; + std::string_view label; + std::string_view description; + std::string_view group; + std::size_t order{}; + bool has_order{}; +}; +Presentation_Info make_presentation_info(std::string_view key, std::string_view label, std::string_view description, std::string_view group, std::size_t order, bool has_order) noexcept; +template +Presentation_Info describe(const Schema& schema) { + constexpr std::size_t index = [] { + if constexpr (std::integral) { + return static_cast(Selector); + } + else { + return schema_member_property_index_v; + } + }(); + static_assert(index < Schema::property_count); + const auto& property = schema.template property(); + std::string_view label_value = property.key(); + std::string_view description_value; + std::string_view group_value; + std::size_t order_value{}; + bool has_order_value = false; + if constexpr (has_declared_effective_attribute_v < Schema, index, Label_Category >) { + label_value = std::remove_cvref_t (schema))>::value.view(); + } + if constexpr (has_declared_effective_attribute_v < Schema, index, Description_Category >) { + description_value = std::remove_cvref_t (schema))>::value.view(); + } + if constexpr (has_declared_effective_attribute_v < Schema, index, Group_Category >) { + group_value = std::remove_cvref_t (schema))>::value.view(); + } + if constexpr (has_declared_effective_attribute_v < Schema, index, Order_Category >) { + order_value = std::remove_cvref_t (schema))>::value; + has_order_value = true; + } + return make_presentation_info(property.key(), label_value, description_value, group_value, order_value, has_order_value); +} +} diff --git a/extensions/src/presentation.cpp b/extensions/src/presentation.cpp new file mode 100644 index 0000000..67ec239 --- /dev/null +++ b/extensions/src/presentation.cpp @@ -0,0 +1,6 @@ +#include +namespace structive::presentation { +Presentation_Info make_presentation_info(std::string_view key, std::string_view label, std::string_view description, std::string_view group, std::size_t order, bool has_order) noexcept { + return Presentation_Info{key, label.empty() ? key : label, description, group, order, has_order}; +} +} diff --git a/extensions/tests/presentation_test.cpp b/extensions/tests/presentation_test.cpp new file mode 100644 index 0000000..3feae80 --- /dev/null +++ b/extensions/tests/presentation_test.cpp @@ -0,0 +1,33 @@ +#include +#include +#include +#include +using namespace structive; +#define REQUIRE(expression) do { if (!(expression)) { std::fprintf(stderr, "REQUIRE failed: %s:%d: %s\n", __FILE__, __LINE__, #expression); std::abort(); } } while (false) +struct Device : Property_Object { + int temperature{20}; + int pressure{100}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + defaults(external_access, presentation::group < "Environment" >), + field < &Device::temperature > (key < "temperature" >, presentation::label < "Temperature" >, presentation::order < 2 >), + field < &Device::pressure > (key < "pressure" >) + ); + } +}; +int main() { + Device device; + const auto& temperature = device.schema().property<&Device::temperature>(); + static_assert(std::remove_cvref_t::template has_attribute); + auto temperature_info = presentation::describe < &Device::temperature > (device.schema()); + auto pressure_info = presentation::describe < &Device::pressure > (device.schema()); + REQUIRE(temperature_info.label == "Temperature"); + REQUIRE(temperature_info.order == 2); + REQUIRE(temperature_info.has_order); + REQUIRE(pressure_info.label == "pressure"); + REQUIRE(pressure_info.group == "Environment"); + return 0; +}