添加AGENTS SKILL
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
---
|
||||
name: adminive-development
|
||||
description: Use when implementing, modifying, debugging, reviewing, or integrating Adminive descriptors, field presentation, Form/Collection views, Composition manifests, JSON/AMIS generation, managed values, HTTP resources, adapters, Gallery frontend, tests, CMake, install/export, or packaging. Do not use for work confined to Structive internals; use the nested structive-development skill there.
|
||||
---
|
||||
# Adminive Development
|
||||
|
||||
## Goal
|
||||
|
||||
Make changes through Adminive's existing semantic layers instead of bypassing them with framework-specific code. The backend remains the business-schema source of truth; adapters translate that truth into JSON, HTTP, AMIS, or frontend renderer contracts.
|
||||
|
||||
## Start by reading the source of truth
|
||||
|
||||
For public behavior or architecture changes, inspect:
|
||||
|
||||
1. `AGENTS.md`.
|
||||
2. `backend/library/DESIGN.md`.
|
||||
3. The closest public header and its focused tests.
|
||||
4. `README.md` for the user-facing contract.
|
||||
5. If the change reaches `third_party/Structive`, read `third_party/Structive/AGENTS.md` and its skill before editing that subtree.
|
||||
|
||||
Do not infer an API from the Gallery alone. The public headers and tests define the actual contract.
|
||||
|
||||
## Pick the correct layer
|
||||
|
||||
Use this decision table before editing:
|
||||
|
||||
| Need | Owner |
|
||||
|---|---|
|
||||
| Native property identity, intrinsic read/write, constraint, synchronization topology | Structive |
|
||||
| Admin-facing field label/description/control/options/visible condition | Adminive Field Presentation |
|
||||
| Display/create/edit field selection and logical field layout | Form View |
|
||||
| Collection columns, query capability, CRUD layout, table/list/cards mode | Collection View |
|
||||
| Composition of complete Form/Collection/Status components | Composition View |
|
||||
| Slot source/kind/API contract for an independent frontend | Composition Manifest |
|
||||
| JSON encoding/decoding protocol | Adminive Core adapter protocol + concrete JSON adapter |
|
||||
| AMIS renderer JSON | AMIS adapter |
|
||||
| HTTP route/body/query bridging | httplib/Drogon adapter |
|
||||
| External prepare/commit/rollback | `Resource_Transaction` |
|
||||
| In-process mutable consistency | Managed/Structive synchronization |
|
||||
| Pixel styling/responsive layout for a `frontend` node | frontend renderer |
|
||||
|
||||
If a proposed feature crosses several rows, keep each concern in its owner instead of creating one all-purpose descriptor flag.
|
||||
|
||||
## Common implementation recipes
|
||||
|
||||
### Describe a business object
|
||||
|
||||
Prefer explicit field descriptors:
|
||||
|
||||
```cpp
|
||||
struct Config {
|
||||
std::string host;
|
||||
int port{};
|
||||
};
|
||||
template <>
|
||||
struct adminive::Type_Descriptor<Config> {
|
||||
static auto get() {
|
||||
using T = Config;
|
||||
return adminive::object<T>(
|
||||
"config",
|
||||
"Config",
|
||||
ADMINIVE_FIELD(T, host).editable().creatable().label("Host").text_input(),
|
||||
ADMINIVE_FIELD(T, port).editable().creatable().label("Port").number_input()
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Do not add the same field list to React.
|
||||
|
||||
### Define Form semantics
|
||||
|
||||
Put view-specific order/grouping in `Type_View_Descriptor<T>`, not in the field descriptor:
|
||||
|
||||
```cpp
|
||||
template <>
|
||||
struct adminive::Type_View_Descriptor<Config> {
|
||||
static auto edit() {
|
||||
using T = Config;
|
||||
return adminive::edit_form<T>(adminive::vertical(
|
||||
adminive::use<&T::host>(),
|
||||
adminive::use<&T::port>()
|
||||
));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Keep display/create/edit semantics distinct when they differ.
|
||||
|
||||
### Define Collection semantics
|
||||
|
||||
Start from one collection contract and derive the rendering mode:
|
||||
|
||||
```cpp
|
||||
const auto base = adminive::collection_view<Row>(
|
||||
adminive::column<&Row::name>("Name").search(),
|
||||
adminive::column<&Row::score>("Score").sort()
|
||||
).default_sort("score");
|
||||
const auto item = adminive::collection_item<&Row::name>().body<&Row::score>();
|
||||
const auto table = base.as_table();
|
||||
const auto list = base.as_list(item);
|
||||
const auto cards = base.as_cards(item, 3);
|
||||
```
|
||||
|
||||
`sort()`, `search()`, and `filter()` are backend capabilities. Both httplib and Drogon must pass dynamic query fields to `Collection_Service`, which rejects fields not authorized by the View Schema.
|
||||
|
||||
### Compose complete components
|
||||
|
||||
Use Composition instead of hand-building AMIS containers:
|
||||
|
||||
```cpp
|
||||
const auto page = adminive::composition_view("device_page", adminive::compose::vertical(
|
||||
adminive::compose::slot("config"),
|
||||
adminive::compose::slot("devices")
|
||||
));
|
||||
```
|
||||
|
||||
Use `compose::frontend(...)` only when the backend intentionally delegates final positioning. For an independent frontend, expose a Composition Manifest with a unique Slot Contract for every referenced slot.
|
||||
|
||||
### Adapt an external value type
|
||||
|
||||
Use `Value_Adapter<T, Json>` when a wrapper has a stable model value. Use `Control_Adapter<T, Json>` only when the renderer itself needs custom behavior. Keep JSON-framework details out of `backend/library`.
|
||||
|
||||
### Adapt enums or reflected aggregates
|
||||
|
||||
- Enum semantics go through `Enum_Adapter<Enum>`; include `adminive/adapters/magic_enum.hpp` only when that bridge is desired.
|
||||
- Aggregate reflection goes through `Reflection_Adapter<T>`; use the Boost.PFR bridge only in the service layer.
|
||||
- Do not make Adminive Core include magic_enum, Boost.PFR, nlohmann JSON, cpp-httplib, or Drogon.
|
||||
|
||||
### Expose a mutable resource
|
||||
|
||||
Use `Resource_Service` through the transport wrapper. Build `Request_Context` only at the outer HTTP bridge. Put persistence/external side effects in `Resource_Transaction`, and preserve rollback behavior when runtime commit or external commit fails.
|
||||
|
||||
### Work on the React Gallery
|
||||
|
||||
- Read `/descriptor`, `/view`, `/data`, `/amis`, and Composition Manifest contracts from the backend.
|
||||
- Resolve frontend slots from the manifest/renderer registry; never hard-code business slot names or C++ field names into a parallel schema.
|
||||
- When TypeScript receives `unknown`, narrow it before rendering. Do not silence protocol problems with `as any`.
|
||||
- Keep `index.html` non-cacheable and content-hashed/versioned static resources immutable.
|
||||
|
||||
## Invariants that must not regress
|
||||
|
||||
- Descriptors validate legal, unique field names.
|
||||
- Readonly fields can be displayed but are not included in create/edit submit payloads unless the active View permits writing.
|
||||
- Sensitive fields do not appear in frontend data or default-bearing descriptor output.
|
||||
- Collection query fields and sort direction are rejected when the View does not authorize them.
|
||||
- Httplib and Drogon expose the same Collection query contract even though their execution models differ.
|
||||
- `frontend` composition delegates layout; `vertical/horizontal/flow/grid/list/group/card/tabs` retain backend semantic composition.
|
||||
- Slot names in a Manifest have exactly one contract.
|
||||
- AMIS generation translates semantic View/Composition data; it does not invent business layout or permissions.
|
||||
- Managed synchronization and persistence transaction remain separate mechanisms.
|
||||
|
||||
## Testing workflow
|
||||
|
||||
Use the smallest relevant tests first:
|
||||
|
||||
- Core descriptor/JSON/collection behavior: `Adminive_Core_Adapter_Test` or focused library tests.
|
||||
- View/Composition/Manifest behavior: `Adminive_View_Schema_Test`.
|
||||
- Managed behavior: `Adminive_Managed_Test`.
|
||||
- Value/Object/Reflection/Polymorphic adapters: `Adminive_Advanced_Adapter_Test`.
|
||||
- Httplib routes: `Adminive_Httplib_Adapter_Test` with a real localhost server/client.
|
||||
- Drogon routes: `Adminive_Drogon_Adapter_Test`.
|
||||
- Gallery contract: `Adminive_Gallery_Test`.
|
||||
- Install/export: `Adminive_Install_Consumer_Test`.
|
||||
- Source archive: `Adminive_Package_Zip_Test`.
|
||||
|
||||
After focused checks, run the repository gate appropriate to the change. For normal C++ changes:
|
||||
|
||||
```text
|
||||
cmake -S . -B verification/debug -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON
|
||||
cmake --build verification/debug
|
||||
ctest --test-dir verification/debug --output-on-failure
|
||||
```
|
||||
|
||||
For release readiness, use `python scripts/verify.py`. If frontend code changed, run from `frontend/`:
|
||||
|
||||
```text
|
||||
npm run test:unit
|
||||
npm run build
|
||||
```
|
||||
|
||||
Run Playwright when the change affects browser behavior and dependencies are available.
|
||||
|
||||
## Adding a public API
|
||||
|
||||
Before adding a public API:
|
||||
|
||||
1. Confirm an existing semantic layer cannot express the requirement.
|
||||
2. Prefer additive APIs over changing old signature meaning.
|
||||
3. Do not create a compatibility implementation for a replaced design unless explicitly required.
|
||||
4. Add focused tests that prove the old semantics still hold.
|
||||
5. Add/update independent public-header compile coverage if a public header changes.
|
||||
6. Update `backend/library/DESIGN.md` and `README.md` when the contract changes.
|
||||
7. Update the Gallery Capability Case when the feature is user-visible and should be discoverable.
|
||||
|
||||
## Review checklist
|
||||
|
||||
Before handoff, verify:
|
||||
|
||||
- The change is in the correct layer.
|
||||
- No frontend business schema was duplicated.
|
||||
- No sensitive/write/query boundary became weaker.
|
||||
- Httplib/Drogon behavior remains contract-equivalent where they share `Collection_Service`/`Resource_Service`.
|
||||
- Old implementations were removed rather than wrapped when the design was replaced.
|
||||
- Tests use `ADMINIVE_CHECK`, not `assert()`.
|
||||
- CMake paths are local to the owning CMake file.
|
||||
- Install/export/package contents still match public headers and repo-local Codex guidance.
|
||||
- Only verification steps actually executed are reported as passing.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Adminive Codex Instructions
|
||||
|
||||
## Mandatory skill usage
|
||||
|
||||
- Use `$adminive-development` before editing Adminive runtime/API/protocol code, adapters, the Gallery frontend, tests, CMake, install/export, or packaging behavior.
|
||||
- If the task touches `third_party/Structive/`, also read `third_party/Structive/AGENTS.md` and use its `$structive-development` workflow. Structive rules override this file inside that subtree.
|
||||
- Do not use Git commands or modify repository history.
|
||||
|
||||
## Project purpose
|
||||
|
||||
Adminive projects ordinary C++ business objects into backend-admin capabilities. The backend definition is the source of truth for object descriptors, field presentation, form/collection views, page composition, JSON/HTTP contracts, AMIS schemas, managed mutation, status, and transactions. The React frontend consumes generated contracts and must not maintain a second business-field schema.
|
||||
|
||||
Read `README.md` for usage and `backend/library/DESIGN.md` for architecture before changing public behavior.
|
||||
|
||||
## Architecture boundaries
|
||||
|
||||
- `third_party/Structive`: intrinsic property/schema/constraint/synchronization/runtime-structural capability. It must not learn Adminive UI, HTTP, AMIS, CRUD, or persistence semantics.
|
||||
- `backend/library`: Adminive Core protocol. Keep it independent of concrete JSON libraries, HTTP frameworks, enum libraries, PFR, and frontend frameworks.
|
||||
- `backend/service/include/adminive/adapters`: concrete bridges for nlohmann JSON, magic_enum, Boost.PFR, cpp-httplib, and Drogon.
|
||||
- `backend/service/src`: example runtime, Gallery, persistence sample, and server wiring. Do not move reusable protocol behavior here.
|
||||
- `frontend`: renderer/documentation client. It consumes descriptors/views/manifests/AMIS; do not duplicate C++ business fields in React.
|
||||
|
||||
When deciding where a feature belongs, preserve this dependency direction:
|
||||
|
||||
```text
|
||||
Structive -> Adminive Core -> Service adapters -> Example runtime/frontend
|
||||
```
|
||||
|
||||
## Compatibility and implementation rules
|
||||
|
||||
- Preserve the existing function signature and semantic contract whenever modifying an existing function.
|
||||
- If replacing an implementation, delete the old implementation. Do not add compatibility shims unless the design explicitly requires compatibility.
|
||||
- Do not introduce a second implementation path merely to support old behavior.
|
||||
- Add validation at the outer protocol/transport boundary when needed; do not repeat equivalent checks in every inner layer.
|
||||
- Static facts belong in C++20 `constexpr`, concepts, `requires`, or schema validation. Dynamic input belongs in runtime validation.
|
||||
- `managed writable` is not the same as frontend `editable`/`creatable`.
|
||||
- `sensitive` fields must not leak through frontend data or default-bearing descriptors.
|
||||
- Collection query capability is enforced by the backend View Schema. Transport adapters must not silently grant or silently ignore unsupported sort/search/filter fields.
|
||||
- `frontend` composition delegates final layout to the frontend; other composition kinds retain backend semantic positioning.
|
||||
- AMIS is an adapter target, not the business description language.
|
||||
- Synchronization is for in-process mutable consistency. `Resource_Transaction` is for external prepare/commit/rollback. Do not merge those concepts.
|
||||
|
||||
## Coding style
|
||||
|
||||
- C++ uses K&R brace style.
|
||||
- Do not add meaningless blank lines.
|
||||
- Keep comments adjacent to the code they explain; do not separate a comment from its code with a blank line.
|
||||
- Prefer small semantic functions over compatibility wrappers or defensive checks at every layer.
|
||||
- CMake paths must be relative to the `.cmake`/`CMakeLists.txt` that owns them, normally through `CMAKE_CURRENT_LIST_DIR`. Do not base project paths on `CMAKE_SOURCE_DIR`/`PROJECT_SOURCE_DIR` unless the existing design explicitly requires it.
|
||||
- Do not change build/output path patterns or add random path components.
|
||||
- If a PowerShell script is needed, write it for PowerShell 7 and run it with `pwsh`.
|
||||
|
||||
## Tests and verification
|
||||
|
||||
- Tests must use `ADMINIVE_CHECK`, not standard `assert()`, because Release defines `NDEBUG`.
|
||||
- Public headers must remain independently includable; keep header compile tests up to date when adding/removing public headers.
|
||||
- Protocol changes require focused unit/protocol tests and transport tests when the behavior is visible over HTTP.
|
||||
- Changes to install/export/package behavior require install-consumer and package ZIP tests.
|
||||
- Changes to frontend TypeScript/React require `npm run build`; renderer/protocol logic should also run `npm run test:unit`. Browser-visible behavior should run the Playwright E2E gate when dependencies are available.
|
||||
- Do not claim a verification step passed unless it was actually executed.
|
||||
|
||||
Useful commands from the repository root:
|
||||
|
||||
```text
|
||||
cmake -S . -B verification/debug -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON
|
||||
cmake --build verification/debug
|
||||
ctest --test-dir verification/debug --output-on-failure
|
||||
python scripts/verify.py
|
||||
```
|
||||
|
||||
For a focused test, build and run the smallest affected target first, then run the broader gate before handoff.
|
||||
|
||||
## Packaging
|
||||
|
||||
- `package_zip` is part of the release contract. Keep `AGENTS.md`, `.agents/skills/`, and the nested Structive guidance in the source archive.
|
||||
- Generated source ZIP timestamps must use China Standard Time (`UTC+08:00`).
|
||||
- Do not package build directories, `node_modules`, frontend build output, test reports, temporary verification trees, or stale removed implementations.
|
||||
|
||||
## Review priorities
|
||||
|
||||
When reviewing a change, check in this order:
|
||||
|
||||
1. Public signature/semantic compatibility.
|
||||
2. Layer ownership and dependency direction.
|
||||
3. Descriptor/View/Composition/HTTP contract consistency.
|
||||
4. Sensitive/write/query authorization boundaries.
|
||||
5. Managed synchronization and transaction behavior.
|
||||
6. Adapter parity, especially httplib versus Drogon.
|
||||
7. Independent public-header compilation, install consumer, package contents, frontend build, and E2E coverage.
|
||||
@@ -5,7 +5,9 @@ include("${CMAKE_CURRENT_LIST_DIR}/cmake/AdminiveVerification.cmake")
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/third_party/Structive")
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/backend")
|
||||
set(adminive_package_includes
|
||||
"/.agents/"
|
||||
"/.gitignore"
|
||||
"/AGENTS.md"
|
||||
"/backend/"
|
||||
"/cmake/"
|
||||
"/config/"
|
||||
|
||||
@@ -445,7 +445,7 @@ options.context_factory = [](const httplib::Request& request) {
|
||||
resource.bind(server, std::move(options));
|
||||
```
|
||||
|
||||
这个 Context 只在最外层 HTTP bridge 构造,`Resource_Service` 和 transaction 内层不重复检查 transport。真实 `Adminive_Httplib_Adapter_Test` 会绑定 localhost 临时端口并通过 cpp-httplib Client 验证 descriptor/view/data/AMIS、Context 传递、CRUD、paging、sort/search/filter、非法 query、row reorder 和 item status。
|
||||
这个 Context 只在最外层 HTTP bridge 构造,`Resource_Service` 和 transaction 内层不重复检查 transport。`Http_Status_Resource<Status, Json>` 与 Drogon 的状态资源保持同一协议,注册 `/descriptor`、`/amis` 和 `/data`,状态 reader 同样可以读取 `Request_Context`。Context factory 或状态 reader 抛出的异常在 transport 边界转换成结构化 HTTP 500,不允许异常穿过 HTTP 回调。真实 `Adminive_Httplib_Adapter_Test` 会绑定 localhost 临时端口并通过 cpp-httplib Client 验证 descriptor/view/data/AMIS、Context 传递、独立 Status Resource、CRUD、paging、sort/search/filter、非法 query、row reorder 和 item status。
|
||||
|
||||
### Drogon 生命周期和异步提交
|
||||
|
||||
@@ -466,7 +466,7 @@ options.filters = {"LoginFilter", "AdminPermissionFilter"};
|
||||
resource.bind(drogon::app(), std::move(options));
|
||||
```
|
||||
|
||||
整体状态使用 `Drogon_Status_Resource<Status, Json>` 注册 `/descriptor`、`/amis` 和 `/data`。列表 CRUD 使用 `Drogon_Collection_Resource<T, Json>`,和 httplib 的 `Http_Collection_Resource<T, Json>` 共用纯头文件 `Collection_Service<T, Json>`;Drogon/httplib 只负责路由参数、请求体和响应桥接,不再各自实现 CRUD 协议。当前 `Collection_Service` 是线程安全的内存集合服务,尚未定义数据库 repository/transaction 协议;需要把列表直接落数据库时,应在这一层补持久化抽象,而不是把数据库逻辑塞回 Drogon 或 httplib binder。所有 Drogon 路由都会把异常转换成结构化 HTTP 500。
|
||||
整体状态使用 `Drogon_Status_Resource<Status, Json>` 注册 `/descriptor`、`/amis` 和 `/data`。列表 CRUD 使用 `Drogon_Collection_Resource<T, Json>`,和 httplib 的 `Http_Collection_Resource<T, Json>` 共用纯头文件 `Collection_Service<T, Json>`;Drogon/httplib 只负责路由参数、请求体和响应桥接,不再各自实现 CRUD 协议。两个 transport 都把除 `page/perPage/orderBy/orderDir` 之外的动态 query 字段交给 `Collection_Service`,因此未被 View Schema 声明为 searchable/filterable 的字段会统一返回 400,不能在某个 adapter 中静默忽略。当前 `Collection_Service` 是线程安全的内存集合服务,尚未定义数据库 repository/transaction 协议;需要把列表直接落数据库时,应在这一层补持久化抽象,而不是把数据库逻辑塞回 Drogon 或 httplib binder。所有 Drogon 路由都会把异常转换成结构化 HTTP 500。
|
||||
|
||||
### 描述器驱动状态
|
||||
|
||||
|
||||
@@ -893,7 +893,7 @@ magic_enum
|
||||
Boost.PFR
|
||||
```
|
||||
|
||||
放 Adapter/bridge 层。
|
||||
放 Adapter/bridge 层。同一 Core Service 被多个 transport 复用时,transport 只负责把请求完整翻译成 Core 输入;它们不能各自发明更宽松的协议。例如 Collection GET 中除分页/排序保留参数之外的 query 字段必须交给 `Collection_Service` 统一校验,httplib 与 Drogon 都不能静默丢弃 View Schema 未授权的筛选字段。独立状态资源也应保持 `/descriptor`、`/amis`、`/data` 协议对齐,请求上下文与异常只在 transport 边界处理。
|
||||
|
||||
### 21.4 它属于具体业务项目吗?
|
||||
|
||||
|
||||
@@ -199,15 +199,11 @@ public:
|
||||
query.per_page = read_drogon_size_parameter(request, "perPage", 20);
|
||||
query.order_by = request->getParameter("orderBy");
|
||||
query.order_dir = request->getParameter("orderDir");
|
||||
const auto view = describe_table_view<T>();
|
||||
for(const auto& column : view.columns) {
|
||||
if(!column.searchable && !column.filterable) {
|
||||
for(const auto& [name, value] : request->getParameters()) {
|
||||
if(name == "page" || name == "perPage" || name == "orderBy" || name == "orderDir") {
|
||||
continue;
|
||||
}
|
||||
const std::string value = request->getParameter(column.field);
|
||||
if(!value.empty()) {
|
||||
query.fields.insert_or_assign(column.field, value);
|
||||
}
|
||||
query.fields.insert_or_assign(name, value);
|
||||
}
|
||||
complete_drogon_request<Json>(callback, [service, query = std::move(query)]() mutable {
|
||||
return service->list_response(std::move(query));
|
||||
|
||||
@@ -69,13 +69,59 @@ public:
|
||||
write_http_response(response, service->amis_response());
|
||||
});
|
||||
server.Post(service->path() + "/data", [service, context_factory](const httplib::Request& request, httplib::Response& response) {
|
||||
const Request_Context context = context_factory ? context_factory(request) : Request_Context{};
|
||||
write_http_response(response, service->update_response(request.body, context));
|
||||
try {
|
||||
const Request_Context context = context_factory ? context_factory(request) : Request_Context{};
|
||||
write_http_response(response, service->update_response(request.body, context));
|
||||
} catch(const std::exception& error) {
|
||||
write_http_response(response, make_http_error<Json>(500, error.what()));
|
||||
} catch(...) {
|
||||
write_http_response(response, make_http_error<Json>(500, "unknown server error"));
|
||||
}
|
||||
});
|
||||
}
|
||||
private:
|
||||
std::shared_ptr<Service> service_;
|
||||
};
|
||||
template <Described_Type Status, Json_Type Json>
|
||||
class Http_Status_Resource {
|
||||
public:
|
||||
using Reader = std::function<Status(const Request_Context&)>;
|
||||
Http_Status_Resource(std::string path, Reader reader, std::uint64_t interval = 2000) : state_(std::make_shared<State>(State{std::move(path), std::move(reader), interval, to_status_descriptor_json<Json, Status>()})) {}
|
||||
Json amis_schema() const {
|
||||
return make_amis_status_service<Json>(state_->descriptor, state_->path + "/data", state_->interval);
|
||||
}
|
||||
void bind(httplib::Server& server) const {
|
||||
bind(server, {});
|
||||
}
|
||||
void bind(httplib::Server& server, Httplib_Bind_Options options) const {
|
||||
const auto state = state_;
|
||||
const auto context_factory = std::move(options.context_factory);
|
||||
server.Get(state->path + "/descriptor", [state](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_response(response, make_http_success<Json>(state->descriptor));
|
||||
});
|
||||
server.Get(state->path + "/amis", [state](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_response(response, make_http_success<Json>(make_amis_status_service<Json>(state->descriptor, state->path + "/data", state->interval)));
|
||||
});
|
||||
server.Get(state->path + "/data", [state, context_factory](const httplib::Request& request, httplib::Response& response) {
|
||||
try {
|
||||
const Request_Context context = context_factory ? context_factory(request) : Request_Context{};
|
||||
write_http_response(response, make_http_success<Json>(to_status_json<Json>(state->reader(context))));
|
||||
} catch(const std::exception& error) {
|
||||
write_http_response(response, make_http_error<Json>(500, error.what()));
|
||||
} catch(...) {
|
||||
write_http_response(response, make_http_error<Json>(500, "unknown server error"));
|
||||
}
|
||||
});
|
||||
}
|
||||
private:
|
||||
struct State {
|
||||
std::string path;
|
||||
Reader reader;
|
||||
std::uint64_t interval;
|
||||
Json descriptor;
|
||||
};
|
||||
std::shared_ptr<State> state_;
|
||||
};
|
||||
template <Described_Type T, Json_Type Json, Basic_Lock Lock = std::mutex>
|
||||
requires Table_View_Described_Type<T> && std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
class Http_Collection_Resource {
|
||||
|
||||
@@ -365,7 +365,7 @@ Json Component_Gallery::documentation() const {
|
||||
cases.push_back(make_doc_case("validation-boundaries", "safety", "Validation 与协议边界", "字段校验、nested path、object validator、readonly 提交、多态 discriminator、非法 Composition/Manifest 和非法 Collection query 都有可重复的标准错误。", "apply_frontend_patch(...); validate_composition_view(...); validate_composition_slot_contracts(...)", examples["protocol_boundaries"], examples["validation_422"], make_gallery_note("协议边界", "在“协议边界”页面可以逐项执行真实请求并查看标准错误。"), string_array({"validation_422", "field_errors", "nested_path", "object_validator", "readonly_reject", "invalid_grid", "invalid_tab", "missing_slot_contract", "duplicate_slot", "invalid_sort", "invalid_filter"}), string_array({"/admin/gallery/boundaries", "/admin/gallery/items?orderBy=color&orderDir=asc", "/admin/gallery/items?score=92"})));
|
||||
Json status_endpoints = Json::array({"/admin/status", std::string(radio_states_api) + "/{id}/status"});
|
||||
cases.push_back(make_doc_case("status", "runtime", "Status Polling:overview + item", "状态 Descriptor 与轮询 renderer 分离;完整运行时同时展示 overview status 和 Collection item status。", "make_amis_status_service<Json>(descriptor, api, interval); collection.register_status<&T::status>()", status_descriptor, status_amis, status_amis, string_array({"status_polling", "overview_status", "item_status"}), std::move(status_endpoints)));
|
||||
cases.push_back(make_doc_case("http-adapters", "adapters", "HTTP Adapter:Httplib + Drogon", "Httplib 现在与 Drogon 一样可以通过 context_factory 构造 Request_Context;真实 localhost 黑盒测试覆盖 CRUD、查询、状态和上下文传递。", "resource.bind(server, Httplib_Bind_Options{.context_factory = ...}); Drogon_Bind_Options{...}", Json{{"httplib", "context_factory -> Request_Context"}, {"drogon", "context_factory + executor/filter"}}, Json{{"tests", string_array({"Adminive_Httplib_Adapter_Test", "Adminive_Drogon_Adapter_Test"})}}, make_gallery_note("Transport Adapter", "当前 Gallery 本身运行在 Httplib;Drogon 契约由同一 service 层和独立 adapter test 验证。"), string_array({"httplib", "drogon", "request_context", "real_http_test"}), string_array({"/admin/gallery/config/data"})));
|
||||
cases.push_back(make_doc_case("http-adapters", "adapters", "HTTP Adapter:Httplib + Drogon", "Httplib 与 Drogon 都从 transport 构造 Request_Context,并把非保留 Collection query 完整交给同一 Service 校验;Httplib 也提供独立 Http_Status_Resource。真实黑盒测试覆盖 CRUD、严格查询、状态、异常边界和上下文传递。", "resource.bind(server, Httplib_Bind_Options{.context_factory = ...}); Http_Status_Resource<Status, Json>(...); Drogon_Bind_Options{...}", Json{{"httplib", "context_factory + strict query + standalone status"}, {"drogon", "context_factory + strict query + executor/filter"}}, Json{{"tests", string_array({"Adminive_Httplib_Adapter_Test", "Adminive_Drogon_Adapter_Test"})}}, make_gallery_note("Transport Adapter", "当前 Gallery 本身运行在 Httplib;两种 transport 对共享 Service 的 query 与错误语义保持一致,Drogon 由独立 adapter test 验证。"), string_array({"httplib", "drogon", "request_context", "strict_query", "standalone_status", "real_http_test"}), string_array({"/admin/gallery/config/data"})));
|
||||
const Json verification_view = Json{{"ctest_labels", string_array({"unit", "protocol", "http", "install", "package", "header", "concurrency"})}, {"sanitizer_matrix", Json{{"clang_gnu", string_array({"ASan", "UBSan", "TSan"})}, {"msvc", string_array({"ASan"})}, {"unsupported_configuration", "configure_error"}}}, {"configurations", string_array({"Debug", "Release"})}};
|
||||
const Json verification_runtime = Json{{"headers", "all public Adminive headers compile standalone"}, {"install_consumers", string_array({"Adminive::Core", "Adminive::Default + Httplib"})}, {"package_zip", "exact root files + no stale nested project"}, {"frontend", string_array({"node_test", "playwright_chromium", "playwright_msedge"})}};
|
||||
cases.push_back(make_doc_case("verification", "engineering", "工程验证 Gate", "Release-safe test checks、公共头独立编译、源码包结构、Core + Default/Httplib 安装消费、真实 HTTP、Debug/Release 和平台真实支持的 Sanitizer 都有正式入口。", "ctest -L header; ctest -L package; ctest -L install; python scripts/verify.py", verification_view, verification_runtime, make_gallery_note("Verification Gate", "测试断言在 Release 下不会被 NDEBUG 清除;不支持的 sanitizer 配置会明确失败或 SKIP,不会假通过。"), string_array({"release_safe_checks", "public_header_compile", "package_zip", "install_consumer", "debug", "release", "asan", "ubsan", "tsan", "frontend_unit", "playwright", "edge"})));
|
||||
|
||||
@@ -129,6 +129,25 @@ int main() {
|
||||
});
|
||||
ADMINIVE_CHECK(response->status == drogon::k200OK);
|
||||
ADMINIVE_CHECK(Json::parse(response->body).at("data").at("total") == 1);
|
||||
request->parameters = {{"unknown", "value"}};
|
||||
response.reset();
|
||||
app.handle("/configs", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) {
|
||||
response = value;
|
||||
});
|
||||
ADMINIVE_CHECK(response->status == drogon::k400BadRequest);
|
||||
request->parameters = {{"orderBy", "unknown"}, {"orderDir", "asc"}};
|
||||
response.reset();
|
||||
app.handle("/configs", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) {
|
||||
response = value;
|
||||
});
|
||||
ADMINIVE_CHECK(response->status == drogon::k400BadRequest);
|
||||
request->parameters = {{"orderBy", "name"}, {"orderDir", "sideways"}};
|
||||
response.reset();
|
||||
app.handle("/configs", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) {
|
||||
response = value;
|
||||
});
|
||||
ADMINIVE_CHECK(response->status == drogon::k400BadRequest);
|
||||
request->parameters.clear();
|
||||
response.reset();
|
||||
app.handle("/configs/1", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) {
|
||||
response = value;
|
||||
|
||||
@@ -72,6 +72,9 @@ public:
|
||||
std::string_view getBody() const noexcept {
|
||||
return body;
|
||||
}
|
||||
const std::map<std::string, std::string>& getParameters() const noexcept {
|
||||
return parameters;
|
||||
}
|
||||
const std::string& getParameter(const std::string& name) const {
|
||||
const auto iterator = parameters.find(name);
|
||||
return iterator == parameters.end() ? empty_parameter_ : iterator->second;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "adminive/adapters/httplib.hpp"
|
||||
#include "adminive_test.hpp"
|
||||
#include <atomic>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
@@ -14,6 +15,9 @@ struct Config {
|
||||
struct Item_Status {
|
||||
std::string state{"ready"};
|
||||
};
|
||||
struct Overview_Status {
|
||||
std::string user;
|
||||
};
|
||||
struct Row {
|
||||
std::string name;
|
||||
bool enabled{true};
|
||||
@@ -39,6 +43,13 @@ struct Type_Descriptor<httplib_adapter_test::Item_Status> {
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<httplib_adapter_test::Overview_Status> {
|
||||
static auto get() {
|
||||
using T = httplib_adapter_test::Overview_Status;
|
||||
return object<T>("httplib_overview_status", ADMINIVE_FIELD(T, user));
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<httplib_adapter_test::Row> {
|
||||
static auto get() {
|
||||
using T = httplib_adapter_test::Row;
|
||||
@@ -77,17 +88,30 @@ int main() {
|
||||
committed_remote = context.remote_address;
|
||||
};
|
||||
adminive::Http_Resource<Config, Json> resource(config, "/config", transaction);
|
||||
adminive::Http_Status_Resource<Overview_Status, Json> status("/status", [](const adminive::Request_Context& context) {
|
||||
if(context.user == "status-error") {
|
||||
throw std::runtime_error("status failed");
|
||||
}
|
||||
return Overview_Status{context.user};
|
||||
}, 25);
|
||||
adminive::Http_Collection_Resource<Row, Json> collection("/rows", {Row{"alpha", true, 70}, Row{"beta", false, 90}, Row{"gamma", true, 50}});
|
||||
collection.register_status<&Row::status>(25);
|
||||
httplib::Server server;
|
||||
resource.bind(server, adminive::Httplib_Bind_Options{[](const httplib::Request& request) {
|
||||
adminive::Httplib_Bind_Options options;
|
||||
options.context_factory = [](const httplib::Request& request) {
|
||||
const std::string user = request.get_header_value("X-User");
|
||||
if(user == "context-error") {
|
||||
throw std::runtime_error("context failed");
|
||||
}
|
||||
adminive::Request_Context context;
|
||||
context.user = request.get_header_value("X-User");
|
||||
context.user = user;
|
||||
context.request_id = request.get_header_value("X-Request-Id");
|
||||
context.remote_address = request.remote_addr;
|
||||
context.attributes.emplace("transport", "httplib");
|
||||
return context;
|
||||
}});
|
||||
};
|
||||
resource.bind(server, options);
|
||||
status.bind(server, options);
|
||||
collection.bind(server);
|
||||
const int port = server.bind_to_any_port("127.0.0.1");
|
||||
ADMINIVE_CHECK(port > 0);
|
||||
@@ -107,6 +131,15 @@ int main() {
|
||||
ADMINIVE_CHECK(committed_user == "alice");
|
||||
ADMINIVE_CHECK(committed_request_id == "request-42");
|
||||
ADMINIVE_CHECK(committed_remote == "127.0.0.1");
|
||||
httplib::Headers context_error_headers{{"X-User", "context-error"}};
|
||||
check_status(client.Post("/config/data", context_error_headers, R"({"name":"not-applied"})", "application/json"), 500);
|
||||
ADMINIVE_CHECK(config.name == "updated");
|
||||
check_status(client.Get("/status/descriptor"), 200);
|
||||
check_status(client.Get("/status/amis"), 200);
|
||||
ADMINIVE_CHECK(body_json(client.Get("/status/data", headers)).at("data").at("user").at("value") == "alice");
|
||||
httplib::Headers status_error_headers{{"X-User", "status-error"}};
|
||||
check_status(client.Get("/status/data", status_error_headers), 500);
|
||||
check_status(client.Get("/status/data", context_error_headers), 500);
|
||||
ADMINIVE_CHECK(body_json(client.Get("/rows/descriptor")).at("data").at("name") == "httplib_row");
|
||||
ADMINIVE_CHECK(body_json(client.Get("/rows/view")).at("data").at("kind") == "table");
|
||||
ADMINIVE_CHECK(body_json(client.Get("/rows?perPage=2&page=1&orderBy=score&orderDir=desc")).at("data").at("items").at(0).at("name") == "beta");
|
||||
|
||||
@@ -19,6 +19,10 @@ string(REPLACE "\n" ";" archive_entries "${archive_list}")
|
||||
set(root_cmake_count 0)
|
||||
set(root_readme_count 0)
|
||||
set(root_gitignore_count 0)
|
||||
set(root_agents_count 0)
|
||||
set(adminive_skill_count 0)
|
||||
set(structive_agents_count 0)
|
||||
set(structive_skill_count 0)
|
||||
foreach(entry IN LISTS archive_entries)
|
||||
if(entry MATCHES "^Adminive/")
|
||||
message(FATAL_ERROR "package_zip contains nested stale project entry: ${entry}")
|
||||
@@ -29,6 +33,14 @@ foreach(entry IN LISTS archive_entries)
|
||||
math(EXPR root_readme_count "${root_readme_count} + 1")
|
||||
elseif(entry STREQUAL ".gitignore")
|
||||
math(EXPR root_gitignore_count "${root_gitignore_count} + 1")
|
||||
elseif(entry STREQUAL "AGENTS.md")
|
||||
math(EXPR root_agents_count "${root_agents_count} + 1")
|
||||
elseif(entry STREQUAL ".agents/skills/adminive-development/SKILL.md")
|
||||
math(EXPR adminive_skill_count "${adminive_skill_count} + 1")
|
||||
elseif(entry STREQUAL "third_party/Structive/AGENTS.md")
|
||||
math(EXPR structive_agents_count "${structive_agents_count} + 1")
|
||||
elseif(entry STREQUAL "third_party/Structive/.agents/skills/structive-development/SKILL.md")
|
||||
math(EXPR structive_skill_count "${structive_skill_count} + 1")
|
||||
elseif(entry STREQUAL "backend/library/include/adminive/synchronized.hpp" OR entry STREQUAL "backend/service/tests/synchronized_test.cpp")
|
||||
message(FATAL_ERROR "package_zip contains removed synchronized implementation: ${entry}")
|
||||
endif()
|
||||
@@ -36,3 +48,6 @@ endforeach()
|
||||
if(NOT root_cmake_count EQUAL 1 OR NOT root_readme_count EQUAL 1 OR NOT root_gitignore_count EQUAL 1)
|
||||
message(FATAL_ERROR "package_zip root exact-file selection is invalid")
|
||||
endif()
|
||||
if(NOT root_agents_count EQUAL 1 OR NOT adminive_skill_count EQUAL 1 OR NOT structive_agents_count EQUAL 1 OR NOT structive_skill_count EQUAL 1)
|
||||
message(FATAL_ERROR "package_zip is missing Codex repository guidance")
|
||||
endif()
|
||||
|
||||
Reference in New Issue
Block a user