330 lines
10 KiB
Markdown
330 lines
10 KiB
Markdown
# Structive
|
|
|
|
**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<T> → Object_Schema
|
|
│ compile-time structure, keys, attributes, constraints, access capabilities
|
|
│
|
|
└── Property_Object<T>
|
|
per-instance managed access, synchronization, traversal and runtime access
|
|
```
|
|
|
|
A type can remain recognizably ordinary C++:
|
|
|
|
```cpp
|
|
#include <structive/property/property.hpp>
|
|
using namespace structive;
|
|
struct Device : Property_Object<Device> {
|
|
double temperature{25.0};
|
|
double pressure{101.3};
|
|
};
|
|
template <>
|
|
struct structive::Type_Descriptor<Device> {
|
|
static auto get() {
|
|
return object<Device>(
|
|
defaults(
|
|
external_access<External_Access::read_write>,
|
|
persistence_access<Persistence_Access::load_store>
|
|
),
|
|
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<T>` 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<T> 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<T>` members?
|
|
|
|
Structive intentionally does not require this:
|
|
|
|
```cpp
|
|
struct Device {
|
|
Property<double> temperature;
|
|
};
|
|
```
|
|
|
|
Instead, the storage stays native:
|
|
|
|
```cpp
|
|
struct Device : Property_Object<Device> {
|
|
double temperature;
|
|
};
|
|
```
|
|
|
|
and the structural meaning is declared separately with `Type_Descriptor<Device>`.
|
|
|
|
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<T>` describes the **type**. `Property_Object<T>` adds state and behavior to an **instance**.
|
|
|
|
The schema contains the registered property tuple, object defaults and the default synchronization plan. A `Property_Object<T>` 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 <Fixed_String Value>
|
|
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<Device, double>([](const auto& view) {
|
|
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
|
|
}, key<"speed_span">, external_access<External_Access::read>)
|
|
```
|
|
|
|
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<N>`
|
|
|
|
`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)
|