分层重构

This commit is contained in:
2026-08-08 11:14:50 +08:00
parent 6ac872b9b7
commit 2155ff0e17
25 changed files with 2233 additions and 620 deletions
+119 -30
View File
@@ -1,19 +1,32 @@
# Adminive Backend-Driven Complete Example
Adminive 使用 C++20 模板、Concepts 和可替换适配器描述业务类型。同一份后端描述驱动 JSON 双向转换、事务式赋值、前端可编辑性、校验、amis CRUD、枚举列表列、树状配置、联动条件、日期控件、颜色控件和一次性提交。属性能力、Schema 与同步拓扑由 `third_party/Structive` 提供Adminive 只叠加后台管理领域的描述、JSONHTTPAMIS 和事务适配。核心层不依赖具体 JSON 库、枚举反射库或结构体反射库。前端只读取 `/admin/amis` 并渲染后端返回的页面 JSON,不包含业务字段或业务判断
Adminive 使用 C++20 模板、Concepts 和可替换适配器,把后端已有的 C++ 业务结构投影成后台管理界面。同一份后端定义同时驱动对象结构 JSON、数据访问、字段控件、表单/卡片/流式/网格/Tabs/表格等语义组合、查询能力、HTTP 接口和 AMIS 页面。属性能力、Schema 与同步拓扑由 `third_party/Structive` 提供Adminive 在其上分层描述 Field Presentation、View Schema、页面 Composition、JSON/HTTP/AMIS 和事务适配。前端不维护第二份业务字段清单,只消费后端生成的 `/descriptor``/view``/data``/amis`
## 设计理念
Adminive 的核心定位是:**让后端已有的 C++ 结构直接驱动后台管理能力,而不是再建立一套独立的数据模型。**
整体职责分为两层:
整体职责按语义分层:
```text
Structive
负责 Property、Schema、intrinsic read/writesynchronization
Adminive
负责后台领域 metadata、JSON、HTTP、AMIS、managed transaction 与 frontend projection
Structive / Object Schema
Property、类型、intrinsic read/write、constraint、synchronization
Adminive Field Presentation
单个字段的 label、description、control、options、visible_on
Adminive View Schema
Form/Table 的字段选择、顺序、分组和数据交互能力
Adminive Composition View
Form/Table/Status 等组件之间的 card/grid/flow/tabs 等页面组合
AMIS Adapter / Frontend
翻译成具体 AMIS JSON,并负责最终视觉样式和响应式表现
```
当前设计遵守几个核心原则:
@@ -21,14 +34,68 @@ Adminive
- Structive 管“对象本身是什么”,Adminive 管“后台系统怎样消费它”。
- 字段默认 intrinsic read-only,只有 `.editable()``.creatable()``.read_write()` 显式进入 managed mutation path。
- `read_only` 是能够影响真实运行成本的结构信息:只读存储属性不为自身贡献 lock slot/mutexmanaged read 走零锁路径。
- `editable``sensitive``widget` 是 Adminive 领域 metadata,不是通用 Property 访问控制。外部系统是否暴露字段由外部系统自己决定
- `editable``sensitive``Field_Presentation` 是 Adminive 领域 metadata;表格排序、搜索、筛选和页面组合属于独立 `View Schema`,都不是通用 Property 访问控制
- Synchronization 只负责进程内 mutable consistency`Resource_Transaction` 负责外部副作用的 prepare/commit/rollback;数据库事务继续由数据库负责。
- JSON、HTTP、枚举反射和结构体反射都通过 Adapter 接入,第三方依赖不进入 Adminive Core 协议。
- 前端只消费后端描述,不维护第二份业务 Schema。
- 后端拥有页面的语义结构;前端只消费后端描述并负责具体渲染、样式与响应式细节,不维护第二份业务 Schema。
- 静态可知的能力尽量在编译期表达,动态输入才进入 runtime adapter。
完整的设计理念、职责边界、同步原则、事务分层和 API 演进规则见 [backend/library/DESIGN.md](backend/library/DESIGN.md)。
### 后端 UI 的三层描述与组合层
一个字段“是什么”、一个字段“怎么呈现”、一组字段“怎么组成业务视图”,以及多个业务视图“怎么组成页面”是四个不同问题。Adminive 不再把它们塞进同一个 Field Descriptor
```text
Object Schema 结构事实
Field Presentation 单字段呈现
View Schema Form/Table 语义组合
Composition View 多组件页面组合
```
后端保留完整的组合能力,包括 `vertical``horizontal``flow``grid``group``card``tabs` 和表格。区别只在于这些能力现在位于独立层:字段定义不再携带表格列顺序/排序等信息,AMIS Adapter 也不再承担业务组合决策。
例如一个配置对象可以只定义字段事实和单字段控件:
```cpp
ADMINIVE_FIELD(T, host).editable().label("Host").text_input();
ADMINIVE_FIELD(T, port).editable().label("Port").number_input();
ADMINIVE_FIELD(T, color).editable().label("Color").color_input();
```
同一个类型的编辑视图再独立决定组合:
```cpp
return edit_form<T>(vertical(
group("Network", horizontal(use<&T::host>(), use<&T::port>())),
card("Appearance", use<&T::color>())
));
```
如果同一个类型还需要列表,表格能力同样单独定义:
```cpp
return table_view<T>(
column<&T::host>("Host").search().fix(Table_Fixed::left),
column<&T::port>("Port").sort(),
column<&T::color>("Color")
).default_sort("port");
```
其中 `.sort()``.search()``.filter()` 不只是前端提示:Collection Service 只接受后端 View Schema 明确声明的查询能力。前端不能自行把一个后端不支持的字段变成可排序或可搜索字段。
对于“上面一个配置表单、下面一个设备表格”这类跨组件页面,使用 `Composition_View`
```cpp
const auto page = composition_view("device_page", compose::vertical(
compose::heading("Devices"),
compose::slot("config"),
compose::slot("table")
));
```
`slot` 引用已经生成的 Form/Table/Status 等组件,后端仍然决定语义组合,AMIS Adapter 只负责把组合翻译成具体 AMIS JSON。前端可以进一步决定 CSS、断点、列宽等纯视觉细节。
## 后端分层
`backend/library` 是库级协议层,只有头文件,不依赖也不链接任何具体 JSON、HTTP Server、枚举反射或结构体反射实现。它依赖项目内的 `third_party/Structive`,由 Structive 负责 intrinsic property capability、Schema 和 synchronizationAdminive 自身包含 descriptor、JSON 适配协议、AMIS schema、状态协议、managed adapter 和抽象 `Resource_Service`
@@ -358,23 +425,38 @@ Adminive/
`example.hpp` 位于 `backend/service/src`,只作为完整示例实现,不属于公共库头文件。
## 字段名称、显示名称和排序
## 字段呈现与表格列是两层
Field Descriptor 只描述字段自身和单字段呈现:
```cpp
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
.creatable()
.editable()
.required()
.list_label("Mode")
.order(5)
.sortable();
.select_input();
```
- `name` 来自成员名称,用于 JSON 字段和接口参数。
- `label` 用于表单标签。
- `list_label` 用于列表表头,未设置时回退到 `label`
- `order` 控制列表列顺序,未设置时按字段声明顺序生成默认值。
- `sortable` 控制该列能否排序,后端只接受已声明为可排序的字段。
表格列属于 `Table_View`,列顺序就是 `table_view()` 中的声明顺序,排序、搜索、筛选和固定列能力也在这里声明:
```cpp
template <>
struct adminive::Type_View_Descriptor<Radio_State> {
static adminive::Table_View table() {
using T = Radio_State;
return adminive::table_view<T>(
adminive::column<&T::mode>("Mode").sort().filter(),
adminive::column<&T::port>("Port").sort().search().fix(adminive::Table_Fixed::left),
adminive::column<&T::buffer_count>("Buffers").sort()
).default_sort("buffer_count");
}
};
```
- Field Descriptor 的 `name` 是稳定的数据字段名。
- `Field_Presentation::label/control/options` 描述一个字段单独出现时如何呈现。
- `Table_View` 决定哪些字段进入表格、列顺序、列标题、固定位置以及可排序/搜索/筛选能力。
- Collection Service 只接受 `Table_View` 已声明的查询字段,因此 View Schema 同时是前端语义和后端能力契约。
## 枚举列表项
@@ -395,16 +477,14 @@ struct Radio_State {
};
```
描述中不需要手写选项
描述中不需要手写枚举值集合,字段只声明自己的呈现;表格能力由 `Table_View` 另行决定
```cpp
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
.creatable()
.editable()
.required()
.list_label("Mode")
.order(5)
.sortable();
.select_input();
```
示例通过可选的 `magic_enum` 桥接生成;核心只依赖 `Enum_Adapter`
@@ -413,12 +493,16 @@ ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
{
"name": "mode",
"value_type": "enum",
"options": [
{"label": "Receive", "value": "receive"},
{"label": "Transmit", "value": "transmit"},
{"label": "Duplex", "value": "duplex"},
{"label": "Maintenance", "value": "maintenance"}
]
"presentation": {
"label": "Operating Mode",
"control": "select",
"options": [
{"label": "Receive", "value": "receive"},
{"label": "Transmit", "value": "transmit"},
{"label": "Duplex", "value": "duplex"},
{"label": "Maintenance", "value": "maintenance"}
]
}
}
```
@@ -525,11 +609,11 @@ enum class -> select
ADMINIVE_FIELD_LABEL(T, effective_date, "Effective Date")
.editable()
.required()
.widget("input-date");
.date_input();
ADMINIVE_FIELD_LABEL(T, accent_color, "Accent Color")
.editable()
.required()
.widget("input-color");
.color_input();
```
日期控件自动补充:
@@ -610,6 +694,7 @@ resource.register_status<&Radio_State::status>(2000);
GET /admin/amis
GET /admin/status
GET /admin/radio_states/descriptor
GET /admin/radio_states/view
GET /admin/radio_states/status/descriptor
GET /admin/radio_states/amis
GET /admin/radio_states
@@ -620,10 +705,12 @@ PUT /admin/radio_states/{id}
PATCH /admin/radio_states/{id}
DELETE /admin/radio_states/{id}
GET /admin/config/radio/descriptor
GET /admin/config/radio/view
GET /admin/config/radio/data
GET /admin/config/radio/amis
POST /admin/config/radio/data
GET /admin/config/alerts/descriptor
GET /admin/config/alerts/view
GET /admin/config/alerts/data
GET /admin/config/alerts/amis
POST /admin/config/alerts/data
@@ -631,6 +718,8 @@ POST /admin/config/alerts/data
## 前端
默认前端仍然只需要读取 `/admin/amis` 并渲染最终页面;`/descriptor``/view``/data` 是稳定的后端语义接口,供自定义前端、调试器或其他 Adapter 直接消费。业务字段名单、表格列和组合关系都不在前端硬编码。
创建外部 `node_modules` 相对符号链接:
```powershell
+248 -82
View File
@@ -6,14 +6,23 @@
Adminive 的目标不是重新发明对象模型、反射系统或属性系统,而是把后端已经存在的 C++ 结构投影成后台管理能力。
可以把两层职责概括为
可以把职责分成结构层、字段呈现层、视图组合层和最终渲染层
```text
Structive
Structive / Object Schema
描述并管理“这个 C++ 对象本身是什么”
Adminive
描述“后台管理系统应该怎样展示、编辑、传输和提交这个对象”
Field Presentation
描述“单个字段应该以什么语义控件呈现”
View Schema / Composition
描述“字段和业务组件如何组成表单、表格和页面”
AMIS Adapter / Frontend
把语义结构翻译成具体 UI,并负责纯视觉与响应式细节
```
因此 Adminive 更接近一个 backend-driven administration projection layer,而不是一个新的 Property Core。
@@ -27,8 +36,10 @@ C++ business type
Adminive descriptor
├── JSON encode / decode
├── frontend schema
├── AMIS CRUD / form / table
├── field presentation
├── form / table view schema
├── page composition
├── AMIS adapter
├── HTTP resource service
├── validation / error path
├── managed update
@@ -98,16 +109,17 @@ managed read 走零锁路径
Adminive 负责后台管理领域信息和适配:
```text
字段名称与显示名称
字段名称与 Field Presentation
editable / creatable
sensitive
required
widget
visible / visible_on
list column metadata
排序信息
control / visible_on
Form View / Table View
vertical / horizontal / flow / grid / group / card / tabs
表格列、排序、搜索、筛选、固定列与重排语义
Composition View / Slot
JSON 描述协议
AMIS schema
AMIS adapter
HTTP resource semantics
frontend patch
Object_Adapter
@@ -145,17 +157,22 @@ Adminive 字段同时包含两类信息,但这两类信息不能混为一谈
字段 accessor
```
第二类是后台领域元数据
第二类是后台领域信息,但它本身还必须继续分层
```text
是否允许前端编辑
是否允许创建时输入
是否在列表中展示
是否敏感
使用什么 widget
显示什么 label
Field Presentation
label / description / control / options / visible_on
View Schema
字段选择 / 顺序 / group / card / grid / flow / tabs
table columns / sort / search / filter / fixed / reorder
Composition View
Form / Table / Status 等完整组件之间的页面组合
```
字段自身的呈现信息不能携带“它在某个表格里排第几列”这种视图语义;同一个 Property 可以同时出现在 Edit View、Detail View 和多个 Table View 中,而不需要重新定义字段本身。
例如:
```cpp
@@ -181,7 +198,129 @@ managed writable
这条边界必须长期保持。
## 5. 默认只读,显式声明可写
## 5. 后端 UI 的分层模型
Adminive 的目标是让后端修改 C++ 结构和后台描述后,前端无需同步维护业务字段清单即可得到新的界面。因此后端必须拥有页面的**语义结构**,但不应该把所有语义都塞进 Field Descriptor,也不应该直接把 AMIS JSON 当成业务描述语言。
### 5.1 Object Schema:字段是什么
Object Schema 由 Structive 与 Adminive Descriptor 共同建立,负责字段名、真实 C++ 类型、intrinsic writable、constraint、managed synchronization 等结构事实。这一层不知道表格、Card、Tabs 或 AMIS。
### 5.2 Field Presentation:一个字段怎么呈现
`Field_Presentation` 只描述单字段呈现:
```text
label
description
control
visible_on
enum labels/options
```
`control` 使用 `text / multiline_text / number / boolean / select / date / color` 等语义类型,而不是把 `input-text``input-date` 之类 AMIS 组件名写进结构描述。AMIS Adapter 再负责语义控件到 AMIS 组件的映射。
### 5.3 View Schema:这些字段怎么组成一个业务视图
Form View 可以组合:
```text
all_fields
field
vertical
horizontal
flow
grid
group
card
tabs / tab
```
后端因此仍然可以完整决定“这十几个字段如何拼接”,但组合关系不再污染字段定义。一个类型可以分别拥有 Edit/Create/Detail View。
Table View 是一等视图,而不是普通 Field Presentation 的附属标记。它负责:
```text
哪些字段成为列
列顺序与标题
fixed left/right
sortable
searchable
filterable
default sort
row reorder
create/edit form layout
```
这里的查询能力同时约束后端 Collection Service。前端不能因为自己画了一个搜索框,就让一个未声明为 searchable/filterable 的字段获得后端查询能力。
### 5.4 Composition View:完整业务组件怎么组成页面
当页面需要组合已经成型的 Form、Table、Status 或其他业务组件时,使用独立 `Composition_View`。它提供:
```text
slot
heading
vertical
horizontal
flow
grid
group
card
tabs / tab
```
`slot` 只是引用一个已经生成的业务组件。例如“上方配置表单 + 下方设备表格”可以由后端描述为一个 vertical composition,而不需要业务代码直接拼 AMIS `grid/card/tpl` JSON。
### 5.5 后端与前端的最终边界
后端负责:
```text
字段存在与否
字段 intrinsic capability
单字段 presentation
字段属于哪个视图
字段顺序与逻辑分组
form/table/page 的语义组合
表格排序、搜索、筛选等后端能力
```
前端负责:
```text
具体 renderer
CSS / theme
响应式断点
最终列宽和像素级间距
设备尺寸下的视觉降级
```
因此核心原则是:
> **后端拥有页面的语义结构,前端拥有页面的视觉实现。**
这样既满足“删字段、加字段、改配置只改后端”,又避免后端被某个具体前端框架的 CSS/Grid 实现锁死。
### 5.6 AMIS 只是 Adapter
依赖方向必须保持:
```text
Object Schema
Field Presentation
View Schema / Composition
AMIS Adapter
AMIS JSON
```
不能反过来让 `Field_Descriptor` 直接保存任意 AMIS JSON;否则 Adminive 会从 backend-driven schema 退化成“在 C++ 里写前端 JSON”。
## 6. 默认只读,显式声明可写
Adminive 字段默认是 intrinsic read-only。
@@ -215,9 +354,9 @@ Adminive 字段默认是 intrinsic read-only。
同步、事务和适配器可以据此建立正确行为。
## 6. 四种字段声明语义
## 7. 四种字段声明语义
### 6.1 普通字段
### 7.1 普通字段
```cpp
ADMINIVE_FIELD(Config, id)
@@ -233,7 +372,7 @@ intrinsic read-only
它仍然是普通 C++ 成员。直接修改 raw member 属于 raw C++ path,不属于 Adminive/Structive managed contract。
### 6.2 `.editable()`
### 7.2 `.editable()`
```cpp
ADMINIVE_FIELD(Config, worker_count).editable()
@@ -249,7 +388,7 @@ frontend update editable
这是后台表单最常见的可编辑字段。
### 6.3 `.creatable()`
### 7.3 `.creatable()`
```cpp
ADMINIVE_FIELD(Device, address).creatable()
@@ -265,7 +404,7 @@ intrinsic read-write
它表达创建流程的领域能力,不等同于普通 update editable。
### 6.4 `.read_write()`
### 7.4 `.read_write()`
```cpp
ADMINIVE_FIELD(Config, backend_state).read_write()
@@ -281,7 +420,7 @@ intrinsic read-write
适合配置加载、后台代码、服务内部更新等受控路径。
### 6.5 `.unsynchronized()`
### 7.5 `.unsynchronized()`
```cpp
ADMINIVE_FIELD(Config, atomic_counter).read_write().unsynchronized()
@@ -305,7 +444,7 @@ intrinsic writable
`.unsynchronized()` 不是性能开关,更不是“我觉得这里应该没事”。它是对同步责任的显式转移。
## 7. Adminive 不做访问控制系统
## 8. Adminive 不做访问控制系统
Adminive 中的 `editable``readable``sensitive` 等信息服务于具体后台适配语义,但它们不构成一个通用安全边界。
@@ -343,7 +482,7 @@ persistence
`sensitive()` 可以指导 Adminive JSON/HTTP 等适配器避免泄漏敏感内容,但不应被误解为 C++ 内存级权限机制。
## 8. Managed path 与 raw C++ path
## 9. Managed path 与 raw C++ path
Adminive 不试图取代普通 C++ 对象。
@@ -380,7 +519,7 @@ managed path 才承诺:
> 增强原生 C++ 结构,而不是建立一堵无法绕过的对象墙。
## 9. 为什么 Managed 回调不能泄漏引用
## 10. 为什么 Managed 回调不能泄漏引用
Managed read/write 的锁只在回调作用域中有效。
@@ -412,7 +551,7 @@ ranges view
这不是 API 限制,而是锁生命周期本身决定的安全边界。
## 10. 同步模型:只保护 mutable consistency
## 11. 同步模型:只保护 mutable consistency
Structive synchronization 只解决一个问题:
@@ -435,7 +574,7 @@ HTTP 权限
这种选择应该由一致性需求驱动,而不是为了“简单”把所有对象默认放到一把全局锁下。
## 11. 嵌套结构的同步原则
## 12. 嵌套结构的同步原则
嵌套结构不能简单采用“父对象只要有一个 writable 子字段,所有后代访问都锁父对象”的粗粒度规则。
@@ -456,7 +595,7 @@ unsynchronized writable leaf
同步边界来自真实 mutation possibility,而不是单纯来自对象树层级。
## 12. Resource_Service 的职责
## 13. Resource_Service 的职责
`Resource_Service` 是 Adminive 的应用事务编排层,不是新的 Property Core。
@@ -477,11 +616,11 @@ commit external side effect
`Resource_Service` 不应该再创建一棵重复的 per-field mutex 系统。
## 13. 三种“事务”不能混为一谈
## 14. 三种“事务”不能混为一谈
Adminive 中至少存在三类不同一致性问题:
### 13.1 内存对象同步
### 14.1 内存对象同步
由 Structive synchronization 负责。
@@ -491,7 +630,7 @@ lock slot
synchronization topology
```
### 13.2 Adminive 外部副作用事务
### 14.2 Adminive 外部副作用事务
`Resource_Transaction<Model>` 负责。
@@ -503,7 +642,7 @@ rollback
它可以代表配置文件、设备、远端服务等外部动作。
### 13.3 数据库事务
### 14.3 数据库事务
由数据库自身负责。
@@ -511,7 +650,7 @@ rollback
三者可以在一次业务流程中协作,但职责必须保持独立。
## 14. Adapter First
## 15. Adapter First
Adminive Core 不应直接绑定具体第三方库。
@@ -538,7 +677,7 @@ HTTP adapter
如果未来接入新的 JSON/HTTP/Reflection 实现,应优先增加 Adapter,而不是修改 Adminive 的中心算法。
## 15. Object_Adapter:运行时对象与配置模型分离
## 16. Object_Adapter:运行时对象与配置模型分离
不是所有运行时对象都适合作为可复制配置对象。
@@ -575,7 +714,7 @@ frontend schema
这比要求所有业务类型变成“为了后台框架而设计的 DTO”更符合低侵入原则。
## 16. Frontend 应保持被动
## 17. Frontend 应保持被动
Adminive 是 backend-driven 系统,因此前端不应该重新维护:
@@ -601,7 +740,7 @@ CRUD schema
前端可以拥有纯 UI 行为,但不应该成为第二份业务 Schema。
## 17. 编译期与运行期的分工
## 18. 编译期与运行期的分工
Adminive 建议遵守以下规则:
@@ -626,7 +765,7 @@ Adminive 建议遵守以下规则:
更好的优化机会
```
## 18. Metadata 必须具有真实语义
## 19. Metadata 必须具有真实语义
对 Adminive 来说,metadata 不应该只是“最终转成 JSON 的标签集合”。
@@ -649,13 +788,16 @@ sensitive
required
→ 改变输入验证
widget
→ 改变前端控件
Field Presentation control
→ 改变单字段呈现控件
View Schema
→ 改变字段组合、表格查询能力和页面语义结构
```
如果新增 metadata 没有明确消费者,应谨慎加入 Core descriptor,避免逐渐变成无边界的“万能标签袋”。
## 19. Core 的非目标
## 20. Core 的非目标
Adminive Core 不应该演化成:
@@ -674,11 +816,11 @@ Structive 的替代品
保持非目标清晰,是长期保持 API 简洁的重要手段。
## 20. 新功能应该放在哪里
## 21. 新功能应该放在哪里
新增能力时,可以按下面的判断顺序决定归属。
### 20.1 它描述 Property 本身吗?
### 21.1 它描述 Property 本身吗?
例如:
@@ -690,21 +832,22 @@ generic constraint
优先考虑 Structive。
### 20.2 它描述后台管理领域吗?
### 21.2 它描述后台管理领域吗?
例如:
```text
editable
widget
list column
AMIS schema
Field Presentation
Form/Table View
Composition View
AMIS adapter
frontend visible condition
```
属于 Adminive。
### 20.3 它绑定具体第三方库吗?
### 21.3 它绑定具体第三方库吗?
例如:
@@ -718,7 +861,7 @@ Boost.PFR
放 Adapter/bridge 层。
### 20.4 它属于具体业务项目吗?
### 21.4 它属于具体业务项目吗?
例如:
@@ -730,7 +873,7 @@ Boost.PFR
留在业务层,不进入 Adminive Core。
## 21. API 演进原则
## 22. API 演进原则
后续修改 Adminive API 时建议遵守:
@@ -745,13 +888,13 @@ Boost.PFR
9. 高级能力允许高级 API,但应与普通使用路径分层。
10. 对行为语义的修改必须同时补契约测试和文档。
## 22. 测试原则
## 23. 测试原则
Adminive 的测试不应只验证“JSON 长得对”。
至少应该覆盖类契约:
至少应该覆盖类契约:
### 22.1 Descriptor contract
### 23.1 Descriptor contract
```text
字段 metadata
@@ -761,7 +904,18 @@ nested descriptor
schema validation
```
### 22.2 Managed synchronization contract
### 23.2 View contract
```text
Field Presentation 与结构事实分离
Form View 的 group/card/grid/flow/tabs 组合
Table View 的列选择与顺序
sortable/searchable/filterable/fixed 的后端能力约束
Composition View 的 slot 与跨组件组合
/view JSON 与 AMIS Adapter 的语义一致性
```
### 23.3 Managed synchronization contract
```text
read-only zero-lock
@@ -772,7 +926,7 @@ callback lifetime
multi-thread blocking relationship
```
### 22.3 Adapter contract
### 23.4 Adapter contract
```text
JSON encode/decode
@@ -780,9 +934,10 @@ Enum Adapter
Reflection Adapter
Object Adapter
HTTP Adapter
AMIS Adapter
```
### 22.4 Transaction contract
### 23.5 Transaction contract
```text
prepare failure
@@ -795,7 +950,7 @@ Config Store persistence
这些测试保护的是设计边界,而不仅是当前实现细节。
## 23. 设计判断清单
## 24. 设计判断清单
在提交一个新设计前,可以快速检查:
@@ -803,6 +958,7 @@ Config Store persistence
[ ] 这是 Structive 已经解决的问题吗?
[ ] 这是 Adminive 后台领域真正需要的概念吗?
[ ] 它是否把第三方库耦合进 Core?
[ ] 它是否把 Field Presentation、View Schema 和 AMIS Adapter 混成一层?
[ ] 它是否让 metadata 真正改变行为?
[ ] 它是否把访问控制错误塞进 Property Core
[ ] 它是否创建了第二套同步或事务机制?
@@ -814,34 +970,44 @@ Config Store persistence
如果多数问题无法明确回答,通常意味着抽象边界还没有收敛。
## 24. 最终架构图
## 25. 最终架构图
```text
C++ Business Types
Adminive Descriptor
┌─────────────────┴─────────────────┐
│ │
▼ ▼
Adminive domain metadata Structive schema
editable/widget/sensitive/... capability/synchronization
│ │
└─────────────────┬─────────────────┘
Managed object view
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
JSON Adapter Resource Service AMIS Schema
│ │ │
▼ ▼ ▼
serialization HTTP transaction Frontend
Structive / Object Schema
type / capability / synchronization
Resource_Transaction
prepare/commit/rollback
Adminive Field Descriptor
editable / required / sensitive
Field Presentation Layer
label / control / options / visible
┌──────────────┴──────────────┐
▼ ▼
Form / Detail View Table View
group/card/grid/flow/tabs columns/sort/search/filter
│ │
└──────────────┬──────────────┘
Composition View
Form/Table/Status slots + page layout
┌─────────────────┼─────────────────┐
▼ ▼ ▼
View JSON Resource Service AMIS Adapter
Frontend
renderer/theme/responsive layout
JSON / HTTP / Enum / Reflection / AMIS 都位于消费或 Adapter 层;
Structive 和字段结构层不知道具体前端框架。
```
最重要的依赖方向始终是:
@@ -2,10 +2,14 @@
#include "adminive/adapter.hpp"
#include "adminive/amis.hpp"
#include "adminive/collection.hpp"
#include "adminive/composition.hpp"
#include "adminive/concepts.hpp"
#include "adminive/descriptor.hpp"
#include "adminive/http.hpp"
#include "adminive/json.hpp"
#include "adminive/status.hpp"
#include "adminive/view.hpp"
#include "adminive/view_json.hpp"
#include "adminive/managed.hpp"
#include "adminive/presentation.hpp"
#include "adminive/value.hpp"
+452 -130
View File
@@ -1,7 +1,9 @@
#pragma once
#include "adminive/status.hpp"
#include "adminive/view_json.hpp"
#include <algorithm>
#include <cstdint>
#include <map>
#include <stdexcept>
#include <string>
#include <string_view>
@@ -47,9 +49,18 @@ inline std::string resolve_visible_on(std::string expression, std::string_view p
return expression;
}
template <Json_Type Json>
const Json& field_presentation(const Json& field) {
return Json_Adapter<Json>::at(field, "presentation");
}
template <Json_Type Json>
std::string field_label(const Json& field) {
return json_get<Json, std::string>(Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
}
template <Json_Type Json>
void apply_visible_on(Json& control, const Json& field, std::string_view prefix) {
if(Json_Adapter<Json>::contains(field, "visible_on")) {
json_set(control, "visibleOn", resolve_visible_on(json_get<Json, std::string>(Json_Adapter<Json>::at(field, "visible_on")), prefix));
const auto& presentation = field_presentation<Json>(field);
if(Json_Adapter<Json>::contains(presentation, "visible_on")) {
json_set(control, "visibleOn", resolve_visible_on(json_get<Json, std::string>(Json_Adapter<Json>::at(presentation, "visible_on")), prefix));
}
}
template <Json_Type Json>
@@ -99,12 +110,12 @@ Json make_amis_polymorphic_control(const Json& field, std::string_view permissio
}
Json result = json_object<Json>();
json_set(result, "type", "fieldset");
json_set(result, "title", Json_Adapter<Json>::at(field, "label"));
json_set(result, "title", Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
json_set(result, "body", std::move(body));
json_set(result, "collapsable", true);
json_set(result, "collapsed", false);
if(Json_Adapter<Json>::contains(field, "description")) {
json_set(result, "description", Json_Adapter<Json>::at(field, "description"));
if(Json_Adapter<Json>::contains(field_presentation<Json>(field), "description")) {
json_set(result, "description", Json_Adapter<Json>::at(field_presentation<Json>(field), "description"));
}
apply_visible_on(result, field, prefix);
return result;
@@ -138,16 +149,55 @@ Json make_typed_amis_polymorphic_control(const Json& field, std::string_view per
}, Adapter::variants());
Json result = json_object<Json>();
json_set(result, "type", "fieldset");
json_set(result, "title", Json_Adapter<Json>::at(field, "label"));
json_set(result, "title", Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
json_set(result, "body", std::move(body));
json_set(result, "collapsable", true);
json_set(result, "collapsed", false);
if(Json_Adapter<Json>::contains(field, "description")) {
json_set(result, "description", Json_Adapter<Json>::at(field, "description"));
if(Json_Adapter<Json>::contains(field_presentation<Json>(field), "description")) {
json_set(result, "description", Json_Adapter<Json>::at(field_presentation<Json>(field), "description"));
}
apply_visible_on(result, field, prefix);
return result;
}
inline std::string_view amis_control_type(std::string_view control, std::string_view value_type) {
if(control == "text") {
return "input-text";
}
if(control == "multiline_text") {
return "textarea";
}
if(control == "number") {
return "input-number";
}
if(control == "boolean") {
return "switch";
}
if(control == "select") {
return "select";
}
if(control == "date") {
return "input-date";
}
if(control == "color") {
return "input-color";
}
if(control != "automatic") {
throw std::invalid_argument("unknown field control: " + std::string(control));
}
if(value_type == "boolean") {
return "switch";
}
if(value_type == "integer" || value_type == "number") {
return "input-number";
}
if(value_type == "enum") {
return "select";
}
if(value_type == "string") {
return "input-text";
}
return {};
}
template <Json_Type Json>
Json make_default_amis_control(const Json& field, std::string_view permission, std::string_view prefix = {}) {
const std::string name = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "name"));
@@ -158,12 +208,12 @@ Json make_default_amis_control(const Json& field, std::string_view permission, s
json_set(descriptor, "fields", Json_Adapter<Json>::at(field, "children"));
Json control = json_object<Json>();
json_set(control, "type", "fieldset");
json_set(control, "title", Json_Adapter<Json>::at(field, "label"));
json_set(control, "title", Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
json_set(control, "body", make_amis_form_body<Json>(descriptor, permission, full_name));
json_set(control, "collapsable", true);
json_set(control, "collapsed", false);
if(Json_Adapter<Json>::contains(field, "description")) {
json_set(control, "description", Json_Adapter<Json>::at(field, "description"));
if(Json_Adapter<Json>::contains(field_presentation<Json>(field), "description")) {
json_set(control, "description", Json_Adapter<Json>::at(field_presentation<Json>(field), "description"));
}
apply_visible_on(control, field, prefix);
return control;
@@ -173,37 +223,29 @@ Json make_default_amis_control(const Json& field, std::string_view permission, s
}
Json control = json_object<Json>();
json_set(control, "name", full_name);
json_set(control, "label", Json_Adapter<Json>::at(field, "label"));
if(Json_Adapter<Json>::contains(field, "description")) {
json_set(control, "description", Json_Adapter<Json>::at(field, "description"));
json_set(control, "label", Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
if(Json_Adapter<Json>::contains(field_presentation<Json>(field), "description")) {
json_set(control, "description", Json_Adapter<Json>::at(field_presentation<Json>(field), "description"));
}
apply_visible_on(control, field, prefix);
if(!json_get<Json, bool>(Json_Adapter<Json>::at(field, permission))) {
json_set(control, "type", "static");
return control;
}
if(Json_Adapter<Json>::contains(field, "widget")) {
json_set(control, "type", Json_Adapter<Json>::at(field, "widget"));
} else if(value_type == "boolean") {
json_set(control, "type", "switch");
} else if(value_type == "integer" || value_type == "number") {
json_set(control, "type", "input-number");
} else if(value_type == "enum") {
json_set(control, "type", "select");
} else if(value_type == "string") {
json_set(control, "type", "input-text");
} else {
throw std::invalid_argument("field '" + full_name + "' requires an explicit widget or Control_Adapter");
const auto control_type = amis_control_type(json_get<Json, std::string>(Json_Adapter<Json>::at(field_presentation<Json>(field), "control")), value_type);
if(control_type.empty()) {
throw std::invalid_argument("field '" + full_name + "' requires an explicit field control or Control_Adapter");
}
const std::string control_type = json_get<Json, std::string>(Json_Adapter<Json>::at(control, "type"));
json_set(control, "type", std::string(control_type));
const std::string resolved_control_type = json_get<Json, std::string>(Json_Adapter<Json>::at(control, "type"));
if(Json_Adapter<Json>::contains(field, "nullable") && json_get<Json, bool>(Json_Adapter<Json>::at(field, "nullable"))) {
json_set(control, "clearable", true);
}
if(control_type == "input-date") {
if(resolved_control_type == "input-date") {
json_set(control, "valueFormat", "YYYY-MM-DD");
json_set(control, "displayFormat", "YYYY-MM-DD");
json_set(control, "clearable", true);
} else if(control_type == "input-color") {
} else if(resolved_control_type == "input-color") {
json_set(control, "clearable", true);
}
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "required"))) {
@@ -223,8 +265,8 @@ Json make_default_amis_control(const Json& field, std::string_view permission, s
json_set(errors, "isInt", Json_Adapter<Json>::at(field, "validation_message"));
json_set(control, "validationErrors", std::move(errors));
}
if(Json_Adapter<Json>::contains(field, "options")) {
json_set(control, "options", Json_Adapter<Json>::at(field, "options"));
if(Json_Adapter<Json>::contains(field_presentation<Json>(field), "options")) {
json_set(control, "options", Json_Adapter<Json>::at(field_presentation<Json>(field), "options"));
}
return control;
}
@@ -253,12 +295,12 @@ Json make_typed_amis_control(const Json& field, std::string_view permission, std
const Json descriptor = to_descriptor_json<Json, Control_Value>();
Json control = json_object<Json>();
json_set(control, "type", "fieldset");
json_set(control, "title", Json_Adapter<Json>::at(field, "label"));
json_set(control, "title", Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
json_set(control, "body", make_typed_amis_form_body<Json, Model>(descriptor, permission, full_name));
json_set(control, "collapsable", true);
json_set(control, "collapsed", false);
if(Json_Adapter<Json>::contains(field, "description")) {
json_set(control, "description", Json_Adapter<Json>::at(field, "description"));
if(Json_Adapter<Json>::contains(field_presentation<Json>(field), "description")) {
json_set(control, "description", Json_Adapter<Json>::at(field_presentation<Json>(field), "description"));
}
apply_visible_on(control, field, prefix);
return control;
@@ -272,9 +314,7 @@ Json make_amis_form_body(const Json& descriptor, std::string_view permission, st
const auto& fields = Json_Adapter<Json>::at(descriptor, "fields");
for(std::size_t index = 0; index < Json_Adapter<Json>::size(fields); ++index) {
const auto& field = Json_Adapter<Json>::at(fields, index);
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "visible"))) {
json_append(body, make_default_amis_control<Json>(field, permission, prefix));
}
json_append(body, make_default_amis_control<Json>(field, permission, prefix));
}
return body;
}
@@ -284,10 +324,8 @@ void append_typed_amis_form_fields(Json& body, const Json& fields, const Descrip
if constexpr(Index < std::tuple_size_v<Fields>) {
const auto& item = std::get<Index>(descriptor.fields());
const auto& field = Json_Adapter<Json>::at(fields, Index);
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "visible"))) {
using Field = std::remove_cvref_t<decltype(item)>;
json_append(body, make_typed_amis_control<Json, typename Field::member_type>(field, permission, prefix));
}
using Field = std::remove_cvref_t<decltype(item)>;
json_append(body, make_typed_amis_control<Json, typename Field::member_type>(field, permission, prefix));
append_typed_amis_form_fields<Index + 1>(body, fields, descriptor, permission, prefix);
}
}
@@ -303,20 +341,14 @@ template <Json_Type Json>
Json make_default_amis_column(const Json& field) {
Json column = json_object<Json>();
json_set(column, "name", Json_Adapter<Json>::at(field, "name"));
json_set(column, "label", Json_Adapter<Json>::at(field, "list_label"));
if(Json_Adapter<Json>::contains(field, "order")) {
json_set(column, "order", Json_Adapter<Json>::at(field, "order"));
}
if(Json_Adapter<Json>::contains(field, "sortable")) {
json_set(column, "sortable", Json_Adapter<Json>::at(field, "sortable"));
}
json_set(column, "label", Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
const std::string value_type = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "value_type"));
if(value_type == "boolean") {
json_set(column, "type", "status");
} else if(value_type == "enum" && Json_Adapter<Json>::contains(field, "options")) {
} else if(value_type == "enum" && Json_Adapter<Json>::contains(field_presentation<Json>(field), "options")) {
json_set(column, "type", "mapping");
Json map = json_object<Json>();
const auto& options = Json_Adapter<Json>::at(field, "options");
const auto& options = Json_Adapter<Json>::at(field_presentation<Json>(field), "options");
for(std::size_t index = 0; index < Json_Adapter<Json>::size(options); ++index) {
const auto& option = Json_Adapter<Json>::at(options, index);
json_set(map, json_get<Json, std::string>(Json_Adapter<Json>::at(option, "value")), Json_Adapter<Json>::at(option, "label"));
@@ -340,31 +372,316 @@ Json make_typed_amis_column(const Json& field) {
return make_default_amis_column<Json>(field);
}
}
template <Json_Type Json>
struct Amis_Ordered_Column {
int order{};
Json column;
};
template <std::size_t Index, Json_Type Json, class Descriptor>
void append_typed_amis_columns(std::vector<Amis_Ordered_Column<Json>>& columns, const Json& fields, const Descriptor& descriptor) {
template <std::size_t Index = 0, Json_Type Json, class Descriptor>
Json make_typed_amis_control_by_name(const Json& descriptor_json, const Descriptor& descriptor, std::string_view field_name, std::string_view permission, std::string_view prefix) {
using Fields = std::remove_cvref_t<decltype(descriptor.fields())>;
if constexpr(Index < std::tuple_size_v<Fields>) {
const auto& item = std::get<Index>(descriptor.fields());
const auto& field = Json_Adapter<Json>::at(fields, Index);
using Field = std::remove_cvref_t<decltype(item)>;
using Member = typename Field::member_type;
using Storage = std::remove_cvref_t<Member>;
using Value = Adapted_Value_Type<Storage, Json>;
using Control_Value = Optional_Unwrapped_Type<Value>;
constexpr bool custom_column = Control_Adapter_With_Column<Storage, Json> || (!std::same_as<Storage, Value> && Control_Adapter_With_Column<Value, Json>) || (!std::same_as<Value, Control_Value> && Control_Adapter_With_Column<Control_Value, Json>);
const std::string value_type = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "value_type"));
const bool structural = value_type == "object" || value_type == "array" || value_type == "map" || value_type == "polymorphic";
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "readable")) && json_get<Json, bool>(Json_Adapter<Json>::at(field, "list_visible")) && (!structural || custom_column)) {
columns.push_back(Amis_Ordered_Column<Json>{json_get<Json, int>(Json_Adapter<Json>::at(field, "order")), make_typed_amis_column<Json, Member>(field)});
if(item.name() == field_name) {
using Field = std::remove_cvref_t<decltype(item)>;
return make_typed_amis_control<Json, typename Field::member_type>(Json_Adapter<Json>::at(Json_Adapter<Json>::at(descriptor_json, "fields"), Index), permission, prefix);
}
append_typed_amis_columns<Index + 1>(columns, fields, descriptor);
return make_typed_amis_control_by_name<Index + 1>(descriptor_json, descriptor, field_name, permission, prefix);
} else {
throw std::invalid_argument("view field does not exist: " + std::string(field_name));
}
}
template <std::size_t Index = 0, Json_Type Json, class Descriptor>
Json make_typed_amis_column_by_name(const Json& descriptor_json, const Descriptor& descriptor, std::string_view field_name) {
using Fields = std::remove_cvref_t<decltype(descriptor.fields())>;
if constexpr(Index < std::tuple_size_v<Fields>) {
const auto& item = std::get<Index>(descriptor.fields());
if(item.name() == field_name) {
using Field = std::remove_cvref_t<decltype(item)>;
return make_typed_amis_column<Json, typename Field::member_type>(Json_Adapter<Json>::at(Json_Adapter<Json>::at(descriptor_json, "fields"), Index));
}
return make_typed_amis_column_by_name<Index + 1>(descriptor_json, descriptor, field_name);
} else {
throw std::invalid_argument("table column field does not exist: " + std::string(field_name));
}
}
inline std::string_view form_permission(Form_Mode mode) noexcept {
switch(mode) {
case Form_Mode::display:
return "readable";
case Form_Mode::edit:
return "editable";
case Form_Mode::create:
return "creatable";
}
return "readable";
}
template <Json_Type Json>
void apply_view_node_options(Json& result, const View_Node& node, std::string_view prefix) {
if(!node.description.empty()) {
json_set(result, "description", node.description);
}
if(!node.visible_on.empty()) {
json_set(result, "visibleOn", resolve_visible_on(node.visible_on, prefix));
}
}
template <Json_Type Json, Described_Type T>
Json make_amis_view_body(const View_Node& node, const Json& descriptor_json, std::string_view permission, std::string_view prefix);
template <Json_Type Json, Described_Type T>
Json make_amis_view_node(const View_Node& node, const Json& descriptor_json, std::string_view permission, std::string_view prefix) {
const auto descriptor = describe<T>();
if(node.kind == View_Node_Kind::field) {
return make_typed_amis_control_by_name(descriptor_json, descriptor, node.field, permission, prefix);
}
if(node.kind == View_Node_Kind::group) {
Json result = json_object<Json>();
json_set(result, "type", "fieldset");
json_set(result, "title", node.title);
json_set(result, "body", make_amis_view_body<Json, T>(View_Node{View_Node_Kind::vertical, {}, {}, {}, {}, 0, false, false, node.children}, descriptor_json, permission, prefix));
if(node.collapsible) {
json_set(result, "collapsable", true);
json_set(result, "collapsed", node.collapsed);
}
apply_view_node_options(result, node, prefix);
return result;
}
if(node.kind == View_Node_Kind::card) {
Json result = json_object<Json>();
json_set(result, "type", "card");
if(!node.title.empty()) {
Json header = json_object<Json>();
json_set(header, "title", node.title);
json_set(result, "header", std::move(header));
}
json_set(result, "body", make_amis_view_body<Json, T>(View_Node{View_Node_Kind::vertical, {}, {}, {}, {}, 0, false, false, node.children}, descriptor_json, permission, prefix));
apply_view_node_options(result, node, prefix);
return result;
}
if(node.kind == View_Node_Kind::horizontal || node.kind == View_Node_Kind::grid) {
Json columns = json_array<Json>();
const std::size_t count = node.kind == View_Node_Kind::grid && node.columns != 0 ? node.columns : std::max<std::size_t>(1, node.children.size());
const std::size_t width = std::max<std::size_t>(1, 12 / count);
for(const auto& child : node.children) {
Json column = json_object<Json>();
json_set(column, "md", width);
json_set(column, "body", make_amis_view_body<Json, T>(child, descriptor_json, permission, prefix));
json_append(columns, std::move(column));
}
Json result = json_object<Json>();
json_set(result, "type", "grid");
json_set(result, "columns", std::move(columns));
apply_view_node_options(result, node, prefix);
return result;
}
if(node.kind == View_Node_Kind::flow) {
Json items = json_array<Json>();
for(const auto& child : node.children) {
Json item = json_object<Json>();
json_set(item, "body", make_amis_view_body<Json, T>(child, descriptor_json, permission, prefix));
json_append(items, std::move(item));
}
Json result = json_object<Json>();
json_set(result, "type", "flex");
json_set(result, "items", std::move(items));
apply_view_node_options(result, node, prefix);
return result;
}
if(node.kind == View_Node_Kind::tabs) {
Json tab_items = json_array<Json>();
for(const auto& child : node.children) {
Json item = json_object<Json>();
json_set(item, "title", child.title);
json_set(item, "body", make_amis_view_body<Json, T>(View_Node{View_Node_Kind::vertical, {}, {}, {}, {}, 0, false, false, child.children}, descriptor_json, permission, prefix));
if(!child.visible_on.empty()) {
json_set(item, "visibleOn", resolve_visible_on(child.visible_on, prefix));
}
json_append(tab_items, std::move(item));
}
Json result = json_object<Json>();
json_set(result, "type", "tabs");
json_set(result, "tabs", std::move(tab_items));
apply_view_node_options(result, node, prefix);
return result;
}
if(node.kind == View_Node_Kind::tab) {
throw std::invalid_argument("tab view can only be used inside tabs");
}
Json result = json_object<Json>();
json_set(result, "type", "container");
json_set(result, "body", make_amis_view_body<Json, T>(View_Node{View_Node_Kind::vertical, {}, {}, {}, {}, 0, false, false, node.children}, descriptor_json, permission, prefix));
apply_view_node_options(result, node, prefix);
return result;
}
template <Json_Type Json, Described_Type T>
void append_amis_view_node(Json& body, const View_Node& node, const Json& descriptor_json, std::string_view permission, std::string_view prefix) {
if(node.kind == View_Node_Kind::all_fields) {
Json fields = make_typed_amis_form_body<Json, T>(descriptor_json, permission, prefix);
for(std::size_t index = 0; index < Json_Adapter<Json>::size(fields); ++index) {
json_append(body, Json_Adapter<Json>::at(fields, index));
}
return;
}
if(node.kind == View_Node_Kind::vertical) {
for(const auto& child : node.children) {
append_amis_view_node<Json, T>(body, child, descriptor_json, permission, prefix);
}
return;
}
json_append(body, make_amis_view_node<Json, T>(node, descriptor_json, permission, prefix));
}
template <Json_Type Json, Described_Type T>
Json make_amis_view_body(const View_Node& node, const Json& descriptor_json, std::string_view permission, std::string_view prefix) {
Json result = json_array<Json>();
append_amis_view_node<Json, T>(result, node, descriptor_json, permission, prefix);
return result;
}
template <Json_Type Json>
void apply_composition_node_options(Json& result, const Composition_Node& node) {
if(!node.description.empty()) {
json_set(result, "description", node.description);
}
if(!node.visible_on.empty()) {
json_set(result, "visibleOn", node.visible_on);
}
}
template <Json_Type Json>
Json make_amis_composition_body(const Composition_Node& node, const std::map<std::string, Json>& slots);
template <Json_Type Json>
Json make_amis_composition_node(const Composition_Node& node, const std::map<std::string, Json>& slots) {
if(node.kind == Composition_Node_Kind::slot) {
const auto iterator = slots.find(node.slot);
if(iterator == slots.end()) {
throw std::invalid_argument("composition slot is not provided: " + node.slot);
}
return iterator->second;
}
if(node.kind == Composition_Node_Kind::heading) {
Json result = json_object<Json>();
json_set(result, "type", "tpl");
std::string html = "<h3 style=\"margin:0";
if(!node.accent_color.empty()) {
html += ";color:" + node.accent_color;
}
html += "\">" + node.title + "</h3>";
json_set(result, "tpl", std::move(html));
apply_composition_node_options(result, node);
return result;
}
if(node.kind == Composition_Node_Kind::group) {
Json result = json_object<Json>();
json_set(result, "type", "panel");
json_set(result, "title", node.title);
json_set(result, "body", make_amis_composition_body<Json>(Composition_Node{Composition_Node_Kind::vertical, {}, {}, {}, {}, {}, 0, false, false, node.children}, slots));
if(node.collapsible) {
json_set(result, "collapsable", true);
json_set(result, "collapsed", node.collapsed);
}
apply_composition_node_options(result, node);
return result;
}
if(node.kind == Composition_Node_Kind::card) {
Json result = json_object<Json>();
json_set(result, "type", "card");
if(!node.title.empty()) {
Json header = json_object<Json>();
json_set(header, "title", node.title);
json_set(result, "header", std::move(header));
}
json_set(result, "body", make_amis_composition_body<Json>(Composition_Node{Composition_Node_Kind::vertical, {}, {}, {}, {}, {}, 0, false, false, node.children}, slots));
apply_composition_node_options(result, node);
return result;
}
if(node.kind == Composition_Node_Kind::horizontal || node.kind == Composition_Node_Kind::grid) {
Json columns = json_array<Json>();
const std::size_t count = node.kind == Composition_Node_Kind::grid && node.columns != 0 ? node.columns : std::max<std::size_t>(1, node.children.size());
const std::size_t width = std::max<std::size_t>(1, 12 / count);
for(const auto& child : node.children) {
Json column = json_object<Json>();
json_set(column, "md", width);
json_set(column, "body", make_amis_composition_body<Json>(child, slots));
json_append(columns, std::move(column));
}
Json result = json_object<Json>();
json_set(result, "type", "grid");
json_set(result, "columns", std::move(columns));
apply_composition_node_options(result, node);
return result;
}
if(node.kind == Composition_Node_Kind::flow) {
Json items = json_array<Json>();
for(const auto& child : node.children) {
Json item = json_object<Json>();
json_set(item, "body", make_amis_composition_body<Json>(child, slots));
json_append(items, std::move(item));
}
Json result = json_object<Json>();
json_set(result, "type", "flex");
json_set(result, "items", std::move(items));
apply_composition_node_options(result, node);
return result;
}
if(node.kind == Composition_Node_Kind::tabs) {
Json tab_items = json_array<Json>();
for(const auto& child : node.children) {
Json item = json_object<Json>();
json_set(item, "title", child.title);
json_set(item, "body", make_amis_composition_body<Json>(Composition_Node{Composition_Node_Kind::vertical, {}, {}, {}, {}, {}, 0, false, false, child.children}, slots));
if(!child.visible_on.empty()) {
json_set(item, "visibleOn", child.visible_on);
}
json_append(tab_items, std::move(item));
}
Json result = json_object<Json>();
json_set(result, "type", "tabs");
json_set(result, "tabs", std::move(tab_items));
apply_composition_node_options(result, node);
return result;
}
if(node.kind == Composition_Node_Kind::tab) {
throw std::invalid_argument("composition tab can only be used inside tabs");
}
Json result = json_object<Json>();
json_set(result, "type", "container");
json_set(result, "body", make_amis_composition_body<Json>(Composition_Node{Composition_Node_Kind::vertical, {}, {}, {}, {}, {}, 0, false, false, node.children}, slots));
apply_composition_node_options(result, node);
return result;
}
template <Json_Type Json>
void append_amis_composition_node(Json& body, const Composition_Node& node, const std::map<std::string, Json>& slots) {
if(node.kind == Composition_Node_Kind::vertical) {
for(const auto& child : node.children) {
append_amis_composition_node(body, child, slots);
}
return;
}
if(node.kind == Composition_Node_Kind::slot) {
const auto iterator = slots.find(node.slot);
if(iterator == slots.end()) {
throw std::invalid_argument("composition slot is not provided: " + node.slot);
}
if(Json_Adapter<Json>::is_array(iterator->second)) {
for(std::size_t index = 0; index < Json_Adapter<Json>::size(iterator->second); ++index) {
json_append(body, Json_Adapter<Json>::at(iterator->second, index));
}
} else {
json_append(body, iterator->second);
}
return;
}
json_append(body, make_amis_composition_node<Json>(node, slots));
}
template <Json_Type Json>
Json make_amis_composition_body(const Composition_Node& node, const std::map<std::string, Json>& slots) {
Json result = json_array<Json>();
append_amis_composition_node(result, node, slots);
return result;
}
template <Json_Type Json>
Json to_amis_composition_schema(const Composition_View& view, const std::map<std::string, Json>& slots) {
validate_composition_view(view);
Json result = json_object<Json>();
json_set(result, "type", view.title.empty() ? "container" : "panel");
if(!view.title.empty()) {
json_set(result, "title", view.title);
}
json_set(result, "body", make_amis_composition_body<Json>(view.body, slots));
return result;
}
template <Json_Type Json>
Json make_amis_status_body(const Json& descriptor) {
Json body = json_array<Json>();
@@ -441,64 +758,75 @@ Json make_amis_object_status_column(const Json& descriptor, std::string status_a
}
template <Json_Type Json, class T>
requires Described_Type<std::remove_cvref_t<T>> || Snapshot_Adapted_Object<T>
Json to_amis_form_schema(const T& value, std::string submit_api = {}, std::string submit_label = "Apply") {
Json to_amis_form_schema(const T& value, const Form_View& view, std::string submit_api = {}) {
using Model = Object_Model_Type<T>;
validate_form_view<Model>(view);
const Json descriptor = to_descriptor_json<Json, T>();
Json result = json_object<Json>();
json_set(result, "type", "form");
json_set(result, "title", Json_Adapter<Json>::at(descriptor, "label"));
json_set(result, "title", view.title);
json_set(result, "data", to_frontend_json<Json>(value));
json_set(result, "body", make_typed_amis_form_body<Json, Model>(descriptor, "editable"));
json_set(result, "actions", make_amis_form_actions<Json>(std::move(submit_label)));
json_set(result, "affixFooter", true);
if(!submit_api.empty()) {
Json api = json_object<Json>();
json_set(api, "method", "post");
json_set(api, "url", std::move(submit_api));
json_set(result, "api", std::move(api));
json_set(result, "body", make_amis_view_body<Json, Model>(view.body, descriptor, form_permission(view.mode), {}));
if(view.mode != Form_Mode::display) {
json_set(result, "actions", make_amis_form_actions<Json>(view.submit_label));
json_set(result, "affixFooter", view.affix_footer);
if(!submit_api.empty()) {
Json api = json_object<Json>();
json_set(api, "method", "post");
json_set(api, "url", std::move(submit_api));
json_set(result, "api", std::move(api));
}
}
return result;
}
template <Json_Type Json, Managed_Value_Type T>
Json to_amis_form_schema(const T& value, std::string submit_api = {}, std::string submit_label = "Apply") {
Json to_amis_form_schema(const T& value, const Form_View& view, std::string submit_api = {}) {
return value.read([&](const auto& item) {
return adminive::to_amis_form_schema<Json>(item, std::move(submit_api), std::move(submit_label));
return adminive::to_amis_form_schema<Json>(item, view, std::move(submit_api));
});
}
template <Json_Type Json, Managed_Field_Type T>
Json to_amis_form_schema(const T& value, std::string submit_api = {}, std::string submit_label = "Apply") {
Json to_amis_form_schema(const T& value, const Form_View& view, std::string submit_api = {}) {
return value.read([&](const auto& item) {
return adminive::to_amis_form_schema<Json>(item, std::move(submit_api), std::move(submit_label));
return adminive::to_amis_form_schema<Json>(item, view, std::move(submit_api));
});
}
template <Json_Type Json, class T>
requires Described_Type<Object_Model_Type<T>>
Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = nullptr, std::uint64_t status_interval = 2000) {
Json to_amis_table_schema(std::string base_api, const Table_View& view, const Json* item_status_descriptor = nullptr, std::uint64_t item_status_interval = 2000, const Json* overview_status_descriptor = nullptr, std::string overview_status_api = {}, std::uint64_t overview_status_interval = 2000) {
using Model = Object_Model_Type<T>;
validate_table_view<Model>(view);
const Json descriptor = to_descriptor_json<Json, T>();
const Json& list = Json_Adapter<Json>::at(descriptor, "list");
const std::string crud_id = json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "name")) + "_crud";
const auto object_descriptor = describe<Model>();
const std::string crud_id = view.name + "_crud";
Json columns = json_array<Json>();
Json id_column = json_object<Json>();
json_set(id_column, "name", "id");
json_set(id_column, "label", Json_Adapter<Json>::at(list, "id_label"));
json_set(id_column, "label", view.id_label);
json_set(id_column, "sortable", true);
json_append(columns, std::move(id_column));
std::vector<Amis_Ordered_Column<Json>> ordered_columns;
const auto& fields = Json_Adapter<Json>::at(descriptor, "fields");
const auto object_descriptor = describe<Model>();
append_typed_amis_columns<0>(ordered_columns, fields, object_descriptor);
std::stable_sort(ordered_columns.begin(), ordered_columns.end(), [](const Amis_Ordered_Column<Json>& left, const Amis_Ordered_Column<Json>& right) {
return left.order < right.order;
});
for(auto& column : ordered_columns) {
json_append(columns, std::move(column.column));
for(const auto& definition : view.columns) {
Json column_json = make_typed_amis_column_by_name(descriptor, object_descriptor, definition.field);
if(!definition.label.empty()) {
json_set(column_json, "label", definition.label);
}
json_set(column_json, "sortable", definition.sortable);
if(definition.searchable) {
json_set(column_json, "searchable", true);
}
if(definition.filterable) {
json_set(column_json, "filterable", true);
}
if(definition.fixed == Table_Fixed::left) {
json_set(column_json, "fixed", "left");
} else if(definition.fixed == Table_Fixed::right) {
json_set(column_json, "fixed", "right");
}
json_append(columns, std::move(column_json));
}
if(status_descriptor) {
json_append(columns, make_amis_object_status_column<Json>(*status_descriptor, base_api + "/${id}/status", status_interval, json_get<Json, std::string>(Json_Adapter<Json>::at(list, "view_status_label"))));
if(item_status_descriptor) {
json_append(columns, make_amis_object_status_column<Json>(*item_status_descriptor, base_api + "/${id}/status", item_status_interval, view.view_status_label));
}
const std::string create_label = json_get<Json, std::string>(Json_Adapter<Json>::at(list, "create_label"));
const std::string edit_label = json_get<Json, std::string>(Json_Adapter<Json>::at(list, "edit_label"));
Json create_api = json_object<Json>();
json_set(create_api, "method", "post");
json_set(create_api, "url", base_api);
@@ -506,8 +834,8 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
json_set(create_form, "type", "form");
json_set(create_form, "api", std::move(create_api));
json_set(create_form, "reload", crud_id);
json_set(create_form, "body", make_typed_amis_form_body<Json, Model>(descriptor, "creatable"));
json_set(create_form, "actions", make_amis_form_actions<Json>(create_label));
json_set(create_form, "body", make_amis_view_body<Json, Model>(view.create_body, descriptor, "creatable", {}));
json_set(create_form, "actions", make_amis_form_actions<Json>(view.create_label));
Json edit_api = json_object<Json>();
json_set(edit_api, "method", "put");
json_set(edit_api, "url", base_api + "/${id}");
@@ -515,23 +843,23 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
json_set(edit_form, "type", "form");
json_set(edit_form, "api", std::move(edit_api));
json_set(edit_form, "reload", crud_id);
json_set(edit_form, "body", make_typed_amis_form_body<Json, Model>(descriptor, "editable"));
json_set(edit_form, "actions", make_amis_form_actions<Json>(json_get<Json, std::string>(Json_Adapter<Json>::at(list, "confirm_label"))));
json_set(edit_form, "body", make_amis_view_body<Json, Model>(view.edit_body, descriptor, "editable", {}));
json_set(edit_form, "actions", make_amis_form_actions<Json>(view.confirm_label));
Json create_dialog = json_object<Json>();
json_set(create_dialog, "title", create_label + " " + json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "label")));
json_set(create_dialog, "title", view.create_label + " " + json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "label")));
json_set(create_dialog, "body", std::move(create_form));
Json create_button = json_object<Json>();
json_set(create_button, "type", "button");
json_set(create_button, "label", create_label);
json_set(create_button, "label", view.create_label);
json_set(create_button, "level", "primary");
json_set(create_button, "actionType", "dialog");
json_set(create_button, "dialog", std::move(create_dialog));
Json edit_dialog = json_object<Json>();
json_set(edit_dialog, "title", edit_label + " " + json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "label")));
json_set(edit_dialog, "title", view.edit_label + " " + json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "label")));
json_set(edit_dialog, "body", std::move(edit_form));
Json edit_button = json_object<Json>();
json_set(edit_button, "type", "button");
json_set(edit_button, "label", edit_label);
json_set(edit_button, "label", view.edit_label);
json_set(edit_button, "level", "link");
json_set(edit_button, "actionType", "dialog");
json_set(edit_button, "dialog", std::move(edit_dialog));
@@ -540,11 +868,11 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
json_set(delete_api, "url", base_api + "/${id}");
Json delete_button = json_object<Json>();
json_set(delete_button, "type", "button");
json_set(delete_button, "label", Json_Adapter<Json>::at(list, "delete_label"));
json_set(delete_button, "label", view.delete_label);
json_set(delete_button, "level", "link");
json_set(delete_button, "className", "text-danger");
json_set(delete_button, "actionType", "ajax");
json_set(delete_button, "confirmText", Json_Adapter<Json>::at(list, "delete_confirm"));
json_set(delete_button, "confirmText", view.delete_confirm);
json_set(delete_button, "api", std::move(delete_api));
json_set(delete_button, "reload", crud_id);
Json buttons = json_array<Json>();
@@ -552,12 +880,12 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
json_append(buttons, std::move(delete_button));
Json operation = json_object<Json>();
json_set(operation, "type", "operation");
json_set(operation, "label", Json_Adapter<Json>::at(list, "actions_label"));
json_set(operation, "label", view.actions_label);
json_set(operation, "buttons", std::move(buttons));
json_append(columns, std::move(operation));
Json toggler = json_object<Json>();
json_set(toggler, "type", "columns-toggler");
json_set(toggler, "draggable", Json_Adapter<Json>::at(list, "column_reorderable"));
json_set(toggler, "draggable", view.column_reorderable);
Json toolbar = json_array<Json>();
json_append(toolbar, std::move(toggler));
json_append(toolbar, "reload");
@@ -573,35 +901,29 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
json_set(crud, "primaryField", "id");
json_set(crud, "headerToolbar", std::move(toolbar));
json_set(crud, "columns", std::move(columns));
const std::string default_order_by = json_get<Json, std::string>(Json_Adapter<Json>::at(list, "default_order_by"));
if(!default_order_by.empty()) {
if(!view.default_order_by.empty()) {
Json params = json_object<Json>();
json_set(params, "orderBy", default_order_by);
json_set(params, "orderDir", Json_Adapter<Json>::at(list, "default_order_dir"));
json_set(params, "orderBy", view.default_order_by);
json_set(params, "orderDir", view.default_order_dir);
json_set(crud, "defaultParams", std::move(params));
}
if(json_get<Json, bool>(Json_Adapter<Json>::at(list, "user_reorderable"))) {
if(view.user_reorderable) {
json_set(crud, "draggable", true);
Json order_api = json_object<Json>();
json_set(order_api, "method", "post");
json_set(order_api, "url", base_api + "/order");
json_set(crud, "saveOrderApi", std::move(order_api));
}
Json page_body = json_array<Json>();
if(overview_status_descriptor && !overview_status_api.empty()) {
json_append(page_body, make_amis_status_service<Json>(*overview_status_descriptor, std::move(overview_status_api), overview_status_interval));
}
json_append(page_body, std::move(crud));
Json result = json_object<Json>();
json_set(result, "type", "page");
json_set(result, "title", Json_Adapter<Json>::at(list, "management_label"));
json_set(result, "body", std::move(crud));
return result;
}
template <Json_Type Json, class T>
requires Described_Type<Object_Model_Type<T>>
Json to_amis_crud_status_schema(std::string base_api, std::string status_api, const Json& status_descriptor, std::uint64_t status_interval = 2000, const Json* object_status_descriptor = nullptr, std::uint64_t object_status_interval = 2000) {
Json result = to_amis_crud_schema<Json, T>(std::move(base_api), object_status_descriptor, object_status_interval);
Json crud = std::move(Json_Adapter<Json>::at(result, "body"));
Json body = json_array<Json>();
json_append(body, make_amis_status_service<Json>(status_descriptor, std::move(status_api), status_interval));
json_append(body, std::move(crud));
json_set(result, "body", std::move(body));
json_set(result, "title", view.title);
json_set(result, "body", std::move(page_body));
return result;
}
}
+78 -16
View File
@@ -2,9 +2,12 @@
#include "adminive/http.hpp"
#include "adminive/status.hpp"
#include <algorithm>
#include <cctype>
#include <charconv>
#include <concepts>
#include <cstdint>
#include <functional>
#include <map>
#include <mutex>
#include <string>
#include <string_view>
@@ -17,9 +20,10 @@ struct Collection_Query {
std::size_t per_page{20};
std::string order_by;
std::string order_dir;
std::map<std::string, std::string> fields;
};
template <Described_Type T, Json_Type Json, Basic_Lock Lock = std::mutex>
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
requires Table_View_Described_Type<T> && std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
class Collection_Service {
public:
explicit Collection_Service(std::string path, std::vector<T> initial = {}) : path_(std::move(path)) {
@@ -53,7 +57,7 @@ public:
return static_cast<bool>(status_reader_);
}
bool user_reorderable() const {
return describe<T>().list_options().user_reorderable_;
return describe_table_view<T>().user_reorderable;
}
std::size_t size() const {
std::scoped_lock lock(mutex_);
@@ -67,18 +71,24 @@ public:
}
return items;
}
Json view_schema() const {
return to_view_json<Json, T>(describe_table_view<T>());
}
Json amis_schema() const {
const Json* descriptor = status_reader_ ? &status_descriptor_ : nullptr;
if(overview_status_api_.empty()) {
return to_amis_crud_schema<Json, T>(path_, descriptor, status_interval_);
}
return to_amis_crud_status_schema<Json, T>(path_, overview_status_api_, overview_status_descriptor_, overview_status_interval_, descriptor, status_interval_);
const Json* item_status = status_reader_ ? &status_descriptor_ : nullptr;
const Json* overview_status = overview_status_api_.empty() ? nullptr : &overview_status_descriptor_;
return to_amis_table_schema<Json, T>(path_, describe_table_view<T>(), item_status, status_interval_, overview_status, overview_status_api_, overview_status_interval_);
}
Http_Response<Json> descriptor_response() const noexcept {
return safe_response([&] {
return make_http_success<Json>(to_descriptor_json<Json, T>());
});
}
Http_Response<Json> view_response() const noexcept {
return safe_response([&] {
return make_http_success<Json>(view_schema());
});
}
Http_Response<Json> amis_response() const noexcept {
return safe_response([&] {
return make_http_success<Json>(amis_schema());
@@ -113,6 +123,7 @@ public:
for(const auto& entry : entries_) {
ordered_entries.push_back(&entry);
}
apply_filters(query, ordered_entries);
apply_sort(query, ordered_entries);
const std::size_t page = query.page == 0 ? 1 : query.page;
const std::size_t per_page = query.per_page == 0 ? 20 : query.per_page;
@@ -230,15 +241,67 @@ private:
const std::string right_value = dump_json(right);
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
}
static const Table_Column* find_table_column(const Table_View& view, std::string_view name) {
const auto iterator = std::find_if(view.columns.begin(), view.columns.end(), [&](const Table_Column& column) {
return column.field == name;
});
return iterator == view.columns.end() ? nullptr : &*iterator;
}
static bool is_sortable_field(const std::string& name) {
if(name == "id") {
return true;
}
bool sortable{};
std::apply([&](const auto&... field) {
((field.name() == name ? sortable = field.is_sortable() : false), ...);
}, describe<T>().fields());
return sortable;
const auto view = describe_table_view<T>();
const auto* column = find_table_column(view, name);
return column && column->sortable;
}
static std::string query_text(const Json& value) {
if(Json_Adapter<Json>::is_string(value)) {
return json_get<Json, std::string>(value);
}
if(Json_Adapter<Json>::is_boolean(value)) {
return json_get<Json, bool>(value) ? "true" : "false";
}
if(Json_Adapter<Json>::is_number(value)) {
char buffer[64];
const auto [end, error] = std::to_chars(buffer, buffer + sizeof(buffer), Json_Adapter<Json>::number(value), std::chars_format::general);
if(error == std::errc{}) {
return std::string(buffer, end);
}
}
return dump_json(value);
}
static std::string lowercase(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
return value;
}
static bool matches_query(const Json& value, std::string_view query, const Table_Column& column) {
const std::string text = query_text(value);
if(column.searchable) {
return lowercase(text).find(lowercase(std::string(query))) != std::string::npos;
}
return text == query;
}
static void apply_filters(const Collection_Query& query, std::vector<const Entry*>& entries) {
if(query.fields.empty()) {
return;
}
const auto view = describe_table_view<T>();
for(const auto& [name, value] : query.fields) {
const auto* column = find_table_column(view, name);
if(!column || (!column->searchable && !column->filterable)) {
throw std::invalid_argument("table query field is not searchable or filterable: " + name);
}
entries.erase(std::remove_if(entries.begin(), entries.end(), [&](const Entry* entry) {
const Json encoded = to_frontend_json<Json>(entry->value);
if(!Json_Adapter<Json>::contains(encoded, name)) {
return true;
}
return !matches_query(Json_Adapter<Json>::at(encoded, name), value, *column);
}), entries.end());
}
}
static std::vector<std::uint64_t> read_order_ids(const Json& value) {
std::vector<std::uint64_t> result;
@@ -262,10 +325,9 @@ private:
return result;
}
void apply_sort(const Collection_Query& query, std::vector<const Entry*>& entries) const {
const auto descriptor = describe<T>();
const auto& options = descriptor.list_options();
const std::string order_by = query.order_by.empty() ? options.default_order_by : query.order_by;
const std::string order_dir = query.order_by.empty() ? options.default_order_dir : query.order_dir.empty() ? "asc" : query.order_dir;
const auto view = describe_table_view<T>();
const std::string order_by = query.order_by.empty() ? view.default_order_by : query.order_by;
const std::string order_dir = query.order_by.empty() ? view.default_order_dir : query.order_dir.empty() ? "asc" : query.order_dir;
if(order_by.empty() || !is_sortable_field(order_by)) {
return;
}
@@ -0,0 +1,157 @@
#pragma once
#include <cstddef>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace adminive {
enum class Composition_Node_Kind {
slot,
heading,
vertical,
horizontal,
flow,
grid,
group,
card,
tabs,
tab
};
struct Composition_Node {
Composition_Node_Kind kind{Composition_Node_Kind::vertical};
std::string slot;
std::string title;
std::string description;
std::string visible_on;
std::string accent_color;
std::size_t columns{};
bool collapsible{};
bool collapsed{};
std::vector<Composition_Node> children;
Composition_Node titled(std::string value) const {
auto result = *this;
result.title = std::move(value);
return result;
}
Composition_Node described(std::string value) const {
auto result = *this;
result.description = std::move(value);
return result;
}
Composition_Node visible_when(std::string value) const {
auto result = *this;
result.visible_on = std::move(value);
return result;
}
Composition_Node accent(std::string value) const {
auto result = *this;
result.accent_color = std::move(value);
return result;
}
Composition_Node collapsible_when(bool value = true, bool initially_collapsed = false) const {
auto result = *this;
result.collapsible = value;
result.collapsed = value && initially_collapsed;
return result;
}
};
namespace compose {
inline Composition_Node slot(std::string name) {
Composition_Node result;
result.kind = Composition_Node_Kind::slot;
result.slot = std::move(name);
return result;
}
inline Composition_Node heading(std::string title) {
Composition_Node result;
result.kind = Composition_Node_Kind::heading;
result.title = std::move(title);
return result;
}
template <class... Children>
Composition_Node make_container(Composition_Node_Kind kind, Children&&... children) {
Composition_Node result;
result.kind = kind;
result.children.reserve(sizeof...(Children));
(result.children.push_back(std::forward<Children>(children)), ...);
return result;
}
template <class... Children>
Composition_Node vertical(Children&&... children) {
return make_container(Composition_Node_Kind::vertical, std::forward<Children>(children)...);
}
template <class... Children>
Composition_Node horizontal(Children&&... children) {
return make_container(Composition_Node_Kind::horizontal, std::forward<Children>(children)...);
}
template <class... Children>
Composition_Node flow(Children&&... children) {
return make_container(Composition_Node_Kind::flow, std::forward<Children>(children)...);
}
template <class... Children>
Composition_Node grid(std::size_t columns, Children&&... children) {
auto result = make_container(Composition_Node_Kind::grid, std::forward<Children>(children)...);
result.columns = columns;
return result;
}
template <class... Children>
Composition_Node group(std::string title, Children&&... children) {
auto result = make_container(Composition_Node_Kind::group, std::forward<Children>(children)...);
result.title = std::move(title);
return result;
}
template <class... Children>
Composition_Node card(std::string title, Children&&... children) {
auto result = make_container(Composition_Node_Kind::card, std::forward<Children>(children)...);
result.title = std::move(title);
return result;
}
template <class... Children>
Composition_Node tab(std::string title, Children&&... children) {
auto result = make_container(Composition_Node_Kind::tab, std::forward<Children>(children)...);
result.title = std::move(title);
return result;
}
template <class... Children>
Composition_Node tabs(Children&&... children) {
return make_container(Composition_Node_Kind::tabs, std::forward<Children>(children)...);
}
}
struct Composition_View {
std::string name;
std::string title;
Composition_Node body;
Composition_View titled(std::string value) const {
auto result = *this;
result.title = std::move(value);
return result;
}
};
inline Composition_View composition_view(std::string name, Composition_Node body) {
return Composition_View{std::move(name), {}, std::move(body)};
}
inline void validate_composition_node(const Composition_Node& node) {
if(node.kind == Composition_Node_Kind::slot && node.slot.empty()) {
throw std::invalid_argument("composition slot name must not be empty");
}
if(node.kind == Composition_Node_Kind::grid && node.columns == 0) {
throw std::invalid_argument("composition grid requires at least one column");
}
if(node.kind == Composition_Node_Kind::tabs) {
for(const auto& child : node.children) {
if(child.kind != Composition_Node_Kind::tab) {
throw std::invalid_argument("composition tabs accepts only tab children");
}
}
}
for(const auto& child : node.children) {
validate_composition_node(child);
}
}
inline void validate_composition_view(const Composition_View& view) {
if(view.name.empty()) {
throw std::invalid_argument("composition view name must not be empty");
}
validate_composition_node(view.body);
}
}
+43 -170
View File
@@ -1,6 +1,7 @@
#pragma once
#include "adminive/adapter.hpp"
#include "adminive/concepts.hpp"
#include "adminive/presentation.hpp"
#include <cctype>
#include <concepts>
#include <map>
@@ -93,22 +94,13 @@ inline std::string make_label(std::string_view name) {
}
struct Field_Metadata {
std::string name;
std::string label;
std::string list_label;
std::string description;
std::string widget;
std::string visible_on;
std::map<std::string, std::string> enum_labels;
Field_Presentation presentation;
bool editable{};
bool creatable{};
bool readable{true};
bool sensitive{};
bool include_default{true};
bool visible{true};
bool required{};
bool list_visible{true};
int order{-1};
bool sortable{};
};
template <class Accessor, bool Managed_Writable = false, bool Synchronized = true>
class Field_Descriptor {
@@ -121,11 +113,11 @@ public:
static constexpr bool synchronized = Synchronized;
explicit Field_Descriptor(std::string name) {
metadata_.name = std::move(name);
metadata_.label = make_label(metadata_.name);
metadata_.presentation.label = make_label(metadata_.name);
}
Field_Descriptor(std::string name, std::string label) {
metadata_.name = std::move(name);
metadata_.label = std::move(label);
metadata_.presentation.label = std::move(label);
}
explicit Field_Descriptor(Field_Metadata metadata) : metadata_(std::move(metadata)) {}
auto editable() const {
@@ -163,54 +155,50 @@ public:
result.metadata_.include_default = value;
return result;
}
Field_Descriptor visible(bool value = true) const {
auto result = *this;
result.metadata_.visible = value;
return result;
}
Field_Descriptor required(bool value = true) const {
auto result = *this;
result.metadata_.required = value;
return result;
}
Field_Descriptor list_visible(bool value = true) const {
auto result = *this;
result.metadata_.list_visible = value;
return result;
}
Field_Descriptor label(std::string value) const {
auto result = *this;
result.metadata_.label = std::move(value);
result.metadata_.presentation.label = std::move(value);
return result;
}
Field_Descriptor description(std::string value) const {
auto result = *this;
result.metadata_.description = std::move(value);
result.metadata_.presentation.description = std::move(value);
return result;
}
Field_Descriptor widget(std::string value) const {
Field_Descriptor control(Field_Control value) const {
auto result = *this;
result.metadata_.widget = std::move(value);
result.metadata_.presentation.control = value;
return result;
}
Field_Descriptor text_input() const {
return control(Field_Control::text);
}
Field_Descriptor multiline_text() const {
return control(Field_Control::multiline_text);
}
Field_Descriptor number_input() const {
return control(Field_Control::number);
}
Field_Descriptor boolean_input() const {
return control(Field_Control::boolean);
}
Field_Descriptor select_input() const {
return control(Field_Control::select);
}
Field_Descriptor date_input() const {
return control(Field_Control::date);
}
Field_Descriptor color_input() const {
return control(Field_Control::color);
}
Field_Descriptor visible_on(std::string value) const {
auto result = *this;
result.metadata_.visible_on = std::move(value);
return result;
}
Field_Descriptor list_label(std::string value) const {
auto result = *this;
result.metadata_.list_label = std::move(value);
return result;
}
Field_Descriptor order(int value) const {
auto result = *this;
result.metadata_.order = value;
return result;
}
Field_Descriptor sortable(bool value = true) const {
auto result = *this;
result.metadata_.sortable = value;
result.metadata_.presentation.visible_on = std::move(value);
return result;
}
template <auto Value>
@@ -221,12 +209,12 @@ public:
throw std::invalid_argument("enum adapter returned an empty value name");
}
auto result = *this;
result.metadata_.enum_labels.insert_or_assign(std::string(name), std::move(label));
result.metadata_.presentation.enum_labels.insert_or_assign(std::string(name), std::move(label));
return result;
}
Field_Descriptor enum_label(std::string value_name, std::string label) const {
auto result = *this;
result.metadata_.enum_labels.insert_or_assign(std::move(value_name), std::move(label));
result.metadata_.presentation.enum_labels.insert_or_assign(std::move(value_name), std::move(label));
return result;
}
template <class Object>
@@ -242,23 +230,23 @@ public:
const std::string& name() const noexcept {
return metadata_.name;
}
const std::string& label() const noexcept {
return metadata_.label;
const Field_Presentation& presentation() const noexcept {
return metadata_.presentation;
}
const std::string& list_label() const noexcept {
return metadata_.list_label.empty() ? metadata_.label : metadata_.list_label;
const std::string& label() const noexcept {
return metadata_.presentation.label;
}
const std::string& description() const noexcept {
return metadata_.description;
return metadata_.presentation.description;
}
const std::string& widget() const noexcept {
return metadata_.widget;
Field_Control control() const noexcept {
return metadata_.presentation.control;
}
const std::string& visible_on() const noexcept {
return metadata_.visible_on;
return metadata_.presentation.visible_on;
}
const std::map<std::string, std::string>& enum_labels() const noexcept {
return metadata_.enum_labels;
return metadata_.presentation.enum_labels;
}
bool is_editable() const noexcept {
return metadata_.editable;
@@ -275,21 +263,9 @@ public:
bool includes_default() const noexcept {
return metadata_.include_default;
}
bool is_visible() const noexcept {
return metadata_.visible;
}
bool is_required() const noexcept {
return metadata_.required;
}
int order() const noexcept {
return metadata_.order;
}
bool is_sortable() const noexcept {
return metadata_.sortable;
}
bool is_list_visible() const noexcept {
return metadata_.list_visible;
}
private:
template <bool New_Writable, bool New_Synchronized>
auto rebind() const {
@@ -338,98 +314,22 @@ struct No_Object_Validator {
template <class T>
void operator()(const T&) const noexcept {}
};
struct Object_List_Options {
std::string id_label{"ID"};
std::string create_label{"Create"};
std::string edit_label{"Edit"};
std::string delete_label{"Delete"};
std::string actions_label{"Actions"};
std::string confirm_label{"Confirm Changes"};
std::string delete_confirm{"Delete this record?"};
std::string view_status_label{"View status"};
std::string management_label;
std::string default_order_by;
std::string default_order_dir{"asc"};
bool user_reorderable_{};
bool column_reorderable_{true};
};
template <class T, class Validator, Field_Descriptor_Type... Fields>
class Object_Descriptor {
public:
using object_type = T;
using validator_type = Validator;
using fields_type = std::tuple<Fields...>;
Object_Descriptor(std::string name, std::string label, Validator validator, std::tuple<Fields...> fields, Object_List_Options list_options = {}) : name_(std::move(name)), label_(std::move(label)), validator_(std::move(validator)), fields_(std::move(fields)), list_options_(std::move(list_options)) {}
Object_Descriptor(std::string name, std::string label, Validator validator, std::tuple<Fields...> fields) : name_(std::move(name)), label_(std::move(label)), validator_(std::move(validator)), fields_(std::move(fields)) {}
template <class New_Validator>
auto validator(New_Validator value) const {
return Object_Descriptor<T, New_Validator, Fields...>(name_, label_, std::move(value), fields_, list_options_);
return Object_Descriptor<T, New_Validator, Fields...>(name_, label_, std::move(value), fields_);
}
Object_Descriptor label(std::string value) const {
auto result = *this;
result.label_ = std::move(value);
return result;
}
Object_Descriptor id_label(std::string value) const {
auto result = *this;
result.list_options_.id_label = std::move(value);
return result;
}
Object_Descriptor create_label(std::string value) const {
auto result = *this;
result.list_options_.create_label = std::move(value);
return result;
}
Object_Descriptor edit_label(std::string value) const {
auto result = *this;
result.list_options_.edit_label = std::move(value);
return result;
}
Object_Descriptor delete_label(std::string value) const {
auto result = *this;
result.list_options_.delete_label = std::move(value);
return result;
}
Object_Descriptor actions_label(std::string value) const {
auto result = *this;
result.list_options_.actions_label = std::move(value);
return result;
}
Object_Descriptor confirm_label(std::string value) const {
auto result = *this;
result.list_options_.confirm_label = std::move(value);
return result;
}
Object_Descriptor delete_confirm(std::string value) const {
auto result = *this;
result.list_options_.delete_confirm = std::move(value);
return result;
}
Object_Descriptor view_status_label(std::string value) const {
auto result = *this;
result.list_options_.view_status_label = std::move(value);
return result;
}
Object_Descriptor management_label(std::string value) const {
auto result = *this;
result.list_options_.management_label = std::move(value);
return result;
}
Object_Descriptor default_sort(std::string field_name, bool descending = false) const {
auto result = *this;
result.list_options_.default_order_by = std::move(field_name);
result.list_options_.default_order_dir = descending ? "desc" : "asc";
return result;
}
Object_Descriptor user_reorderable(bool value = true) const {
auto result = *this;
result.list_options_.user_reorderable_ = value;
return result;
}
Object_Descriptor column_reorderable(bool value = true) const {
auto result = *this;
result.list_options_.column_reorderable_ = value;
return result;
}
const std::string& name() const noexcept {
return name_;
}
@@ -442,15 +342,11 @@ public:
const std::tuple<Fields...>& fields() const noexcept {
return fields_;
}
const Object_List_Options& list_options() const noexcept {
return list_options_;
}
private:
std::string name_;
std::string label_;
Validator validator_;
std::tuple<Fields...> fields_;
Object_List_Options list_options_;
};
template <class Descriptor>
void validate_descriptor(const Descriptor& descriptor) {
@@ -468,29 +364,6 @@ void validate_descriptor(const Descriptor& descriptor) {
}
}(), ...);
}, descriptor.fields());
const auto& options = descriptor.list_options();
if(options.default_order_dir != "asc" && options.default_order_dir != "desc") {
throw std::invalid_argument("default sort direction must be 'asc' or 'desc'");
}
if(options.default_order_by.empty()) {
return;
}
bool found{};
bool sortable{};
std::apply([&](const auto&... field) {
([&] {
if(field.name() == options.default_order_by) {
found = true;
sortable = field.is_sortable();
}
}(), ...);
}, descriptor.fields());
if(!found) {
throw std::invalid_argument("default sort field does not exist: " + options.default_order_by);
}
if(!sortable) {
throw std::invalid_argument("default sort field is not sortable: " + options.default_order_by);
}
}
template <class T, Field_Descriptor_Type... Fields>
auto object(std::string name, Fields... fields) {
+10 -1
View File
@@ -114,10 +114,14 @@ public:
const std::string& path() const noexcept {
return path_;
}
Json view_schema() const {
return to_view_json<Json, Model>(describe_edit_view<Model>());
}
Json amis_schema() const {
Json result;
const auto view = describe_edit_view<Model>();
read_access_([&](const Object& value) {
result = to_amis_form_schema<Json>(value, path_ + "/data", describe<Model>().list_options().confirm_label);
result = to_amis_form_schema<Json>(value, view, path_ + "/data");
});
return result;
}
@@ -126,6 +130,11 @@ public:
return make_http_success<Json>(to_descriptor_json<Json, Object>());
});
}
Http_Response<Json> view_response() const noexcept {
return safe_response([&] {
return make_http_success<Json>(view_schema());
});
}
Http_Response<Json> data_response() const noexcept {
return safe_response([&] {
Http_Response<Json> response;
+11 -36
View File
@@ -395,7 +395,6 @@ requires Described_Type<Model>
Json model_descriptor_json(const Model* defaults = nullptr) {
const auto descriptor = describe<Model>();
Json fields = json_array<Json>();
int field_index{};
std::apply([&](const auto&... item) {
([&] {
using Field = std::remove_cvref_t<decltype(item)>;
@@ -403,70 +402,46 @@ Json model_descriptor_json(const Model* defaults = nullptr) {
using Value = Adapted_Value_Type<Member, Json>;
using Descriptor_Value = Optional_Unwrapped_Type<Value>;
Json field_json = json_object<Json>();
const int order = item.order() < 0 ? field_index : item.order();
++field_index;
json_set(field_json, "name", item.name());
json_set(field_json, "label", item.label());
json_set(field_json, "list_label", item.list_label());
json_set(field_json, "value_type", value_type_name<Json, Member>());
json_set(field_json, "nullable", Optional_Type<Value>);
json_set(field_json, "readable", item.is_readable());
json_set(field_json, "sensitive", item.is_sensitive());
json_set(field_json, "editable", item.is_editable());
using Field = std::remove_cvref_t<decltype(item)>;
json_set(field_json, "writable", Field::managed_writable);
json_set(field_json, "synchronized", Field::managed_writable && Field::synchronized);
json_set(field_json, "creatable", item.is_creatable());
json_set(field_json, "visible", item.is_visible());
json_set(field_json, "required", item.is_required());
json_set(field_json, "list_visible", item.is_list_visible());
json_set(field_json, "order", order);
json_set(field_json, "sortable", item.is_sortable());
if(defaults && item.is_readable() && !item.is_sensitive() && item.includes_default()) {
json_set(field_json, "default", encode_json_value<Json>(item.get(*defaults)));
}
Json presentation = json_object<Json>();
json_set(presentation, "label", item.label());
json_set(presentation, "control", std::string(field_control_name(item.control())));
if(!item.description().empty()) {
json_set(field_json, "description", item.description());
}
if(!item.widget().empty()) {
json_set(field_json, "widget", item.widget());
json_set(presentation, "description", item.description());
}
if(!item.visible_on().empty()) {
json_set(field_json, "visible_on", item.visible_on());
json_set(presentation, "visible_on", item.visible_on());
}
auto options = enum_options<Json, Member>(item.enum_labels());
if(Json_Adapter<Json>::size(options) != 0) {
json_set(presentation, "options", std::move(options));
}
json_set(field_json, "presentation", std::move(presentation));
if constexpr(Described_Type<Descriptor_Value> || Snapshot_Adapted_Object<Descriptor_Value>) {
json_set(field_json, "children", Json_Adapter<Json>::at(to_descriptor_json<Json, Descriptor_Value>(), "fields"));
}
append_constraints<Json, Member>(field_json);
auto options = enum_options<Json, Member>(item.enum_labels());
if(Json_Adapter<Json>::size(options) != 0) {
json_set(field_json, "options", std::move(options));
}
json_append(fields, std::move(field_json));
}(), ...);
}, descriptor.fields());
const auto& options = descriptor.list_options();
Json list = json_object<Json>();
json_set(list, "id_label", options.id_label);
json_set(list, "create_label", options.create_label);
json_set(list, "edit_label", options.edit_label);
json_set(list, "delete_label", options.delete_label);
json_set(list, "actions_label", options.actions_label);
json_set(list, "confirm_label", options.confirm_label);
json_set(list, "delete_confirm", options.delete_confirm);
json_set(list, "view_status_label", options.view_status_label);
json_set(list, "management_label", options.management_label.empty() ? descriptor.label() + " Management" : options.management_label);
json_set(list, "default_order_by", options.default_order_by);
json_set(list, "default_order_dir", options.default_order_dir);
json_set(list, "user_reorderable", options.user_reorderable_);
json_set(list, "column_reorderable", options.column_reorderable_);
Json result = json_object<Json>();
json_set(result, "protocol", "adminive.resource");
json_set(result, "protocol_version", 4);
json_set(result, "protocol_version", 5);
json_set(result, "name", descriptor.name());
json_set(result, "label", descriptor.label());
json_set(result, "value_type", "object");
json_set(result, "list", std::move(list));
json_set(result, "fields", std::move(fields));
return result;
}
@@ -0,0 +1,44 @@
#pragma once
#include <map>
#include <string>
#include <string_view>
namespace adminive {
enum class Field_Control {
automatic,
text,
multiline_text,
number,
boolean,
select,
date,
color
};
inline std::string_view field_control_name(Field_Control value) noexcept {
switch(value) {
case Field_Control::automatic:
return "automatic";
case Field_Control::text:
return "text";
case Field_Control::multiline_text:
return "multiline_text";
case Field_Control::number:
return "number";
case Field_Control::boolean:
return "boolean";
case Field_Control::select:
return "select";
case Field_Control::date:
return "date";
case Field_Control::color:
return "color";
}
return "automatic";
}
struct Field_Presentation {
std::string label;
std::string description;
Field_Control control{Field_Control::automatic};
std::string visible_on;
std::map<std::string, std::string> enum_labels;
};
}
+443
View File
@@ -0,0 +1,443 @@
#pragma once
#include "adminive/descriptor.hpp"
#include <algorithm>
#include <cstddef>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
namespace adminive {
enum class View_Node_Kind {
all_fields,
field,
vertical,
horizontal,
flow,
grid,
group,
card,
tabs,
tab
};
struct View_Node {
View_Node_Kind kind{View_Node_Kind::vertical};
std::string field;
std::string title;
std::string description;
std::string visible_on;
std::size_t columns{};
bool collapsible{};
bool collapsed{};
std::vector<View_Node> children;
View_Node titled(std::string value) const {
auto result = *this;
result.title = std::move(value);
return result;
}
View_Node described(std::string value) const {
auto result = *this;
result.description = std::move(value);
return result;
}
View_Node visible_when(std::string value) const {
auto result = *this;
result.visible_on = std::move(value);
return result;
}
View_Node collapsible_when(bool value = true, bool initially_collapsed = false) const {
auto result = *this;
result.collapsible = value;
result.collapsed = value && initially_collapsed;
return result;
}
};
inline View_Node all_fields() {
View_Node result;
result.kind = View_Node_Kind::all_fields;
return result;
}
inline View_Node view_field(std::string name) {
View_Node result;
result.kind = View_Node_Kind::field;
result.field = std::move(name);
return result;
}
template <auto Member>
std::string described_member_name() {
using Owner = typename Member_Pointer_Traits<Member>::owner_type;
const auto descriptor = describe<Owner>();
std::string result;
std::apply([&](const auto&... item) {
([&] {
using Field = std::remove_cvref_t<decltype(item)>;
using Accessor = typename Field::accessor_type;
if constexpr(requires { Accessor::member; }) {
if constexpr(std::same_as<std::remove_cv_t<decltype(Accessor::member)>, std::remove_cv_t<decltype(Member)>>) {
if constexpr(Accessor::member == Member) {
result = item.name();
}
}
}
}(), ...);
}, descriptor.fields());
if(result.empty()) {
throw std::invalid_argument("view member is not registered in the object descriptor");
}
return result;
}
template <auto Member>
View_Node use() {
return view_field(described_member_name<Member>());
}
template <Reflected_Type T, std::size_t Index>
View_Node use_index() {
static_assert(Index < static_cast<std::size_t>(Reflection_Adapter<T>::field_count));
const auto descriptor = describe<T>();
const auto& field = std::get<Index>(descriptor.fields());
return view_field(field.name());
}
template <class... Children>
View_Node make_view_container(View_Node_Kind kind, Children&&... children) {
View_Node result;
result.kind = kind;
result.children.reserve(sizeof...(Children));
(result.children.push_back(std::forward<Children>(children)), ...);
return result;
}
template <class... Children>
View_Node vertical(Children&&... children) {
return make_view_container(View_Node_Kind::vertical, std::forward<Children>(children)...);
}
template <class... Children>
View_Node horizontal(Children&&... children) {
return make_view_container(View_Node_Kind::horizontal, std::forward<Children>(children)...);
}
template <class... Children>
View_Node flow(Children&&... children) {
return make_view_container(View_Node_Kind::flow, std::forward<Children>(children)...);
}
template <class... Children>
View_Node grid(std::size_t columns, Children&&... children) {
auto result = make_view_container(View_Node_Kind::grid, std::forward<Children>(children)...);
result.columns = columns;
return result;
}
template <class... Children>
View_Node group(std::string title, Children&&... children) {
auto result = make_view_container(View_Node_Kind::group, std::forward<Children>(children)...);
result.title = std::move(title);
return result;
}
template <class... Children>
View_Node card(std::string title, Children&&... children) {
auto result = make_view_container(View_Node_Kind::card, std::forward<Children>(children)...);
result.title = std::move(title);
return result;
}
template <class... Children>
View_Node tab(std::string title, Children&&... children) {
auto result = make_view_container(View_Node_Kind::tab, std::forward<Children>(children)...);
result.title = std::move(title);
return result;
}
template <class... Children>
View_Node tabs(Children&&... children) {
return make_view_container(View_Node_Kind::tabs, std::forward<Children>(children)...);
}
enum class Form_Mode {
display,
edit,
create
};
struct Form_View {
std::string name;
std::string title;
std::string submit_label{"Apply"};
Form_Mode mode{Form_Mode::edit};
View_Node body{all_fields()};
bool affix_footer{true};
Form_View titled(std::string value) const {
auto result = *this;
result.title = std::move(value);
return result;
}
Form_View submit(std::string value) const {
auto result = *this;
result.submit_label = std::move(value);
return result;
}
Form_View affix(bool value = true) const {
auto result = *this;
result.affix_footer = value;
return result;
}
};
template <Described_Type T>
Form_View edit_form(View_Node body = all_fields()) {
const auto descriptor = describe<T>();
return Form_View{descriptor.name() + "_edit", descriptor.label(), "Apply", Form_Mode::edit, std::move(body), true};
}
template <Described_Type T>
Form_View create_form(View_Node body = all_fields()) {
const auto descriptor = describe<T>();
return Form_View{descriptor.name() + "_create", descriptor.label(), "Create", Form_Mode::create, std::move(body), true};
}
template <Described_Type T>
Form_View detail_view(View_Node body = all_fields()) {
const auto descriptor = describe<T>();
return Form_View{descriptor.name() + "_detail", descriptor.label(), {}, Form_Mode::display, std::move(body), false};
}
enum class Table_Fixed {
none,
left,
right
};
struct Table_Column {
std::string field;
std::string label;
bool sortable{};
bool searchable{};
bool filterable{};
Table_Fixed fixed{Table_Fixed::none};
Table_Column titled(std::string value) const {
auto result = *this;
result.label = std::move(value);
return result;
}
Table_Column sort(bool value = true) const {
auto result = *this;
result.sortable = value;
return result;
}
Table_Column search(bool value = true) const {
auto result = *this;
result.searchable = value;
return result;
}
Table_Column filter(bool value = true) const {
auto result = *this;
result.filterable = value;
return result;
}
Table_Column fix(Table_Fixed value) const {
auto result = *this;
result.fixed = value;
return result;
}
};
template <auto Member>
Table_Column column(std::string label = {}) {
return Table_Column{described_member_name<Member>(), std::move(label)};
}
template <Reflected_Type T, std::size_t Index>
Table_Column column_index(std::string label = {}) {
const auto descriptor = describe<T>();
const auto& field = std::get<Index>(descriptor.fields());
return Table_Column{field.name(), std::move(label)};
}
struct Table_View {
std::string name;
std::string title;
std::string id_label{"ID"};
std::string create_label{"Create"};
std::string edit_label{"Edit"};
std::string delete_label{"Delete"};
std::string actions_label{"Actions"};
std::string confirm_label{"Confirm Changes"};
std::string delete_confirm{"Delete this record?"};
std::string view_status_label{"View status"};
std::string default_order_by;
std::string default_order_dir{"asc"};
bool user_reorderable{};
bool column_reorderable{true};
std::vector<Table_Column> columns;
View_Node create_body{all_fields()};
View_Node edit_body{all_fields()};
Table_View titled(std::string value) const {
auto result = *this;
result.title = std::move(value);
return result;
}
Table_View id_title(std::string value) const {
auto result = *this;
result.id_label = std::move(value);
return result;
}
Table_View create_title(std::string value) const {
auto result = *this;
result.create_label = std::move(value);
return result;
}
Table_View edit_title(std::string value) const {
auto result = *this;
result.edit_label = std::move(value);
return result;
}
Table_View delete_title(std::string value) const {
auto result = *this;
result.delete_label = std::move(value);
return result;
}
Table_View actions_title(std::string value) const {
auto result = *this;
result.actions_label = std::move(value);
return result;
}
Table_View confirm_title(std::string value) const {
auto result = *this;
result.confirm_label = std::move(value);
return result;
}
Table_View delete_confirmation(std::string value) const {
auto result = *this;
result.delete_confirm = std::move(value);
return result;
}
Table_View status_title(std::string value) const {
auto result = *this;
result.view_status_label = std::move(value);
return result;
}
Table_View default_sort(std::string field_name, bool descending = false) const {
auto result = *this;
result.default_order_by = std::move(field_name);
result.default_order_dir = descending ? "desc" : "asc";
return result;
}
Table_View reorderable(bool value = true) const {
auto result = *this;
result.user_reorderable = value;
return result;
}
Table_View reorderable_columns(bool value = true) const {
auto result = *this;
result.column_reorderable = value;
return result;
}
Table_View create_layout(View_Node value) const {
auto result = *this;
result.create_body = std::move(value);
return result;
}
Table_View edit_layout(View_Node value) const {
auto result = *this;
result.edit_body = std::move(value);
return result;
}
};
template <Described_Type T, class... Columns>
Table_View table_view(Columns&&... columns) {
const auto descriptor = describe<T>();
Table_View result;
result.name = descriptor.name() + "_table";
result.title = descriptor.label() + " Management";
result.columns.reserve(sizeof...(Columns));
(result.columns.push_back(std::forward<Columns>(columns)), ...);
return result;
}
template <class T>
struct Type_View_Descriptor;
template <Described_Type T>
Form_View describe_edit_view() {
if constexpr(requires { { Type_View_Descriptor<T>::edit() } -> std::same_as<Form_View>; }) {
return Type_View_Descriptor<T>::edit();
} else {
return edit_form<T>();
}
}
template <Described_Type T>
Form_View describe_create_view() {
if constexpr(requires { { Type_View_Descriptor<T>::create() } -> std::same_as<Form_View>; }) {
return Type_View_Descriptor<T>::create();
} else {
return create_form<T>();
}
}
template <Described_Type T>
Form_View describe_detail_view() {
if constexpr(requires { { Type_View_Descriptor<T>::detail() } -> std::same_as<Form_View>; }) {
return Type_View_Descriptor<T>::detail();
} else {
return detail_view<T>();
}
}
template <class T>
concept Table_View_Described_Type = Described_Type<T> && requires {
{ Type_View_Descriptor<T>::table() } -> std::same_as<Table_View>;
};
template <Described_Type T>
Table_View describe_table_view() requires Table_View_Described_Type<T> {
return Type_View_Descriptor<T>::table();
}
template <class Descriptor>
bool descriptor_has_field(const Descriptor& descriptor, std::string_view name) {
bool found{};
std::apply([&](const auto&... field) {
((found = found || field.name() == name), ...);
}, descriptor.fields());
return found;
}
template <Described_Type T>
void validate_view_node(const View_Node& node) {
const auto descriptor = describe<T>();
if(node.kind == View_Node_Kind::field && !descriptor_has_field(descriptor, node.field)) {
throw std::invalid_argument("view field does not exist: " + node.field);
}
if(node.kind == View_Node_Kind::grid && node.columns == 0) {
throw std::invalid_argument("grid view requires at least one column");
}
if(node.kind == View_Node_Kind::tabs) {
for(const auto& child : node.children) {
if(child.kind != View_Node_Kind::tab) {
throw std::invalid_argument("tabs view accepts only tab children");
}
}
}
for(const auto& child : node.children) {
validate_view_node<T>(child);
}
}
template <Described_Type T>
void validate_form_view(const Form_View& view) {
if(view.name.empty()) {
throw std::invalid_argument("form view name must not be empty");
}
validate_view_node<T>(view.body);
}
template <Described_Type T>
void validate_table_view(const Table_View& view) {
if(view.name.empty()) {
throw std::invalid_argument("table view name must not be empty");
}
const auto descriptor = describe<T>();
std::vector<std::string> names;
names.reserve(view.columns.size());
for(const auto& column : view.columns) {
if(!descriptor_has_field(descriptor, column.field)) {
throw std::invalid_argument("table column field does not exist: " + column.field);
}
if(std::find(names.begin(), names.end(), column.field) != names.end()) {
throw std::invalid_argument("duplicate table column field: " + column.field);
}
names.push_back(column.field);
}
if(!view.default_order_by.empty()) {
const auto iterator = std::find_if(view.columns.begin(), view.columns.end(), [&](const Table_Column& column) {
return column.field == view.default_order_by;
});
if(iterator == view.columns.end()) {
throw std::invalid_argument("default sort field is not a table column: " + view.default_order_by);
}
if(!iterator->sortable) {
throw std::invalid_argument("default sort field is not sortable: " + view.default_order_by);
}
}
if(view.default_order_dir != "asc" && view.default_order_dir != "desc") {
throw std::invalid_argument("default sort direction must be 'asc' or 'desc'");
}
validate_view_node<T>(view.create_body);
validate_view_node<T>(view.edit_body);
}
}
@@ -0,0 +1,213 @@
#pragma once
#include "adminive/composition.hpp"
#include "adminive/json.hpp"
#include "adminive/view.hpp"
namespace adminive {
inline std::string_view composition_node_kind_name(Composition_Node_Kind value) noexcept {
switch(value) {
case Composition_Node_Kind::slot:
return "slot";
case Composition_Node_Kind::heading:
return "heading";
case Composition_Node_Kind::vertical:
return "vertical";
case Composition_Node_Kind::horizontal:
return "horizontal";
case Composition_Node_Kind::flow:
return "flow";
case Composition_Node_Kind::grid:
return "grid";
case Composition_Node_Kind::group:
return "group";
case Composition_Node_Kind::card:
return "card";
case Composition_Node_Kind::tabs:
return "tabs";
case Composition_Node_Kind::tab:
return "tab";
}
return "vertical";
}
template <Json_Type Json>
Json composition_node_to_json(const Composition_Node& node) {
Json result = json_object<Json>();
json_set(result, "kind", std::string(composition_node_kind_name(node.kind)));
if(!node.slot.empty()) {
json_set(result, "slot", node.slot);
}
if(!node.title.empty()) {
json_set(result, "title", node.title);
}
if(!node.description.empty()) {
json_set(result, "description", node.description);
}
if(!node.visible_on.empty()) {
json_set(result, "visible_on", node.visible_on);
}
if(!node.accent_color.empty()) {
json_set(result, "accent_color", node.accent_color);
}
if(node.columns != 0) {
json_set(result, "columns", node.columns);
}
if(node.collapsible) {
json_set(result, "collapsible", true);
json_set(result, "collapsed", node.collapsed);
}
if(!node.children.empty()) {
Json children = json_array<Json>();
for(const auto& child : node.children) {
json_append(children, composition_node_to_json<Json>(child));
}
json_set(result, "children", std::move(children));
}
return result;
}
template <Json_Type Json>
Json to_view_json(const Composition_View& view) {
validate_composition_view(view);
Json result = json_object<Json>();
json_set(result, "protocol", "adminive.view");
json_set(result, "protocol_version", 1);
json_set(result, "kind", "composition");
json_set(result, "name", view.name);
if(!view.title.empty()) {
json_set(result, "title", view.title);
}
json_set(result, "body", composition_node_to_json<Json>(view.body));
return result;
}
inline std::string_view view_node_kind_name(View_Node_Kind value) noexcept {
switch(value) {
case View_Node_Kind::all_fields:
return "all_fields";
case View_Node_Kind::field:
return "field";
case View_Node_Kind::vertical:
return "vertical";
case View_Node_Kind::horizontal:
return "horizontal";
case View_Node_Kind::flow:
return "flow";
case View_Node_Kind::grid:
return "grid";
case View_Node_Kind::group:
return "group";
case View_Node_Kind::card:
return "card";
case View_Node_Kind::tabs:
return "tabs";
case View_Node_Kind::tab:
return "tab";
}
return "vertical";
}
inline std::string_view form_mode_name(Form_Mode value) noexcept {
switch(value) {
case Form_Mode::display:
return "display";
case Form_Mode::edit:
return "edit";
case Form_Mode::create:
return "create";
}
return "display";
}
inline std::string_view table_fixed_name(Table_Fixed value) noexcept {
switch(value) {
case Table_Fixed::none:
return "none";
case Table_Fixed::left:
return "left";
case Table_Fixed::right:
return "right";
}
return "none";
}
template <Json_Type Json>
Json view_node_to_json(const View_Node& node) {
Json result = json_object<Json>();
json_set(result, "kind", std::string(view_node_kind_name(node.kind)));
if(!node.field.empty()) {
json_set(result, "field", node.field);
}
if(!node.title.empty()) {
json_set(result, "title", node.title);
}
if(!node.description.empty()) {
json_set(result, "description", node.description);
}
if(!node.visible_on.empty()) {
json_set(result, "visible_on", node.visible_on);
}
if(node.columns != 0) {
json_set(result, "columns", node.columns);
}
if(node.collapsible) {
json_set(result, "collapsible", true);
json_set(result, "collapsed", node.collapsed);
}
if(!node.children.empty()) {
Json children = json_array<Json>();
for(const auto& child : node.children) {
json_append(children, view_node_to_json<Json>(child));
}
json_set(result, "children", std::move(children));
}
return result;
}
template <Json_Type Json, Described_Type T>
Json to_view_json(const Form_View& view) {
validate_form_view<T>(view);
Json result = json_object<Json>();
json_set(result, "protocol", "adminive.view");
json_set(result, "protocol_version", 1);
json_set(result, "kind", "form");
json_set(result, "name", view.name);
json_set(result, "title", view.title);
json_set(result, "mode", std::string(form_mode_name(view.mode)));
json_set(result, "submit_label", view.submit_label);
json_set(result, "affix_footer", view.affix_footer);
json_set(result, "body", view_node_to_json<Json>(view.body));
return result;
}
template <Json_Type Json, Described_Type T>
Json to_view_json(const Table_View& view) {
validate_table_view<T>(view);
Json result = json_object<Json>();
json_set(result, "protocol", "adminive.view");
json_set(result, "protocol_version", 1);
json_set(result, "kind", "table");
json_set(result, "name", view.name);
json_set(result, "title", view.title);
json_set(result, "id_label", view.id_label);
json_set(result, "create_label", view.create_label);
json_set(result, "edit_label", view.edit_label);
json_set(result, "delete_label", view.delete_label);
json_set(result, "actions_label", view.actions_label);
json_set(result, "confirm_label", view.confirm_label);
json_set(result, "delete_confirm", view.delete_confirm);
json_set(result, "view_status_label", view.view_status_label);
json_set(result, "default_order_by", view.default_order_by);
json_set(result, "default_order_dir", view.default_order_dir);
json_set(result, "user_reorderable", view.user_reorderable);
json_set(result, "column_reorderable", view.column_reorderable);
Json columns = json_array<Json>();
for(const auto& column : view.columns) {
Json item = json_object<Json>();
json_set(item, "field", column.field);
if(!column.label.empty()) {
json_set(item, "label", column.label);
}
json_set(item, "sortable", column.sortable);
json_set(item, "searchable", column.searchable);
json_set(item, "filterable", column.filterable);
json_set(item, "fixed", std::string(table_fixed_name(column.fixed)));
json_append(columns, std::move(item));
}
json_set(result, "columns", std::move(columns));
json_set(result, "create_body", view_node_to_json<Json>(view.create_body));
json_set(result, "edit_body", view_node_to_json<Json>(view.edit_body));
return result;
}
}
+9 -1
View File
@@ -262,6 +262,13 @@ struct Type_Descriptor<adapter_test::Config> {
});
}
};
template <>
struct Type_View_Descriptor<adapter_test::Config> {
static Table_View table() {
using T = adapter_test::Config;
return table_view<T>(column_index<T, 0>("线程数").sort(), column_index<T, 1>("模式").sort().filter(), column_index<T, 2>("名称").search());
}
};
}
int main() {
using Json = adapter_test::Mini_Json;
@@ -270,7 +277,8 @@ int main() {
const auto& fields = adminive::Json_Adapter<Json>::at(descriptor, "fields");
assert((adminive::json_get<Json, std::string>(adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(fields, 0), "name")) == "worker_count"));
assert((adminive::json_get<Json, int>(adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(fields, 0), "default")) == 4));
const auto& options = adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(fields, 1), "options");
const auto& presentation = adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(fields, 1), "presentation");
const auto& options = adminive::Json_Adapter<Json>::at(presentation, "options");
assert((adminive::json_get<Json, std::string>(adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(options, 0), "label")) == "主用"));
Config config;
Json patch = adminive::json_object<Json>();
+4
View File
@@ -68,6 +68,9 @@ if(BUILD_TESTING)
add_executable(Adminive_Managed_Test "${CMAKE_CURRENT_LIST_DIR}/tests/managed_test.cpp")
target_link_libraries(Adminive_Managed_Test PRIVATE Adminive::Default)
add_test(NAME Adminive_Managed_Test COMMAND Adminive_Managed_Test)
add_executable(Adminive_View_Schema_Test "${CMAKE_CURRENT_LIST_DIR}/tests/view_schema_test.cpp")
target_link_libraries(Adminive_View_Schema_Test PRIVATE Adminive::Default)
add_test(NAME Adminive_View_Schema_Test COMMAND Adminive_View_Schema_Test)
add_executable(Adminive_Drogon_Adapter_Test "${CMAKE_CURRENT_LIST_DIR}/tests/drogon_adapter_test.cpp")
target_include_directories(Adminive_Drogon_Adapter_Test BEFORE PRIVATE "${CMAKE_CURRENT_LIST_DIR}/tests/fake_drogon")
target_link_libraries(Adminive_Drogon_Adapter_Test PRIVATE Adminive::Default)
@@ -79,6 +82,7 @@ if(BUILD_TESTING)
target_compile_options(Adminive_Safety_Test PRIVATE /permissive-)
target_compile_options(Adminive_Config_Store_Test PRIVATE /permissive-)
target_compile_options(Adminive_Managed_Test PRIVATE /permissive-)
target_compile_options(Adminive_View_Schema_Test PRIVATE /permissive-)
endif()
endif()
install(TARGETS Adminive_Service Adminive_Nlohmann Adminive_MagicEnum Adminive_BoostPfr Adminive_Httplib Adminive_Default EXPORT AdminiveServiceTargets)
@@ -87,6 +87,9 @@ public:
template <class Managed, std::size_t Root_Index, auto... Members>
requires (!std::is_const_v<Managed>) && std::same_as<typename Managed_Field<Managed, Root_Index, Members...>::value_type, std::remove_cvref_t<T>>
explicit Drogon_Resource(Managed_Field<Managed, Root_Index, Members...> value, std::string path, Transaction transaction = {}) : service_(std::make_shared<Service>(value, std::move(path), std::move(transaction))) {}
Json view_schema() const {
return service_->view_schema();
}
Json amis_schema() const {
return service_->amis_schema();
}
@@ -99,6 +102,11 @@ public:
return service->descriptor_response();
});
}, get_constraints);
app.registerHandler(service->path() + "/view", [service](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
complete_drogon_request<Json>(callback, [service] {
return service->view_response();
});
}, get_constraints);
app.registerHandler(service->path() + "/data", [service](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
complete_drogon_request<Json>(callback, [service] {
return service->data_response();
@@ -126,7 +134,7 @@ private:
std::shared_ptr<Service> service_;
};
template <Described_Type T, Json_Type Json, Basic_Lock Lock = std::mutex>
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
requires Table_View_Described_Type<T> && std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
class Drogon_Collection_Resource {
public:
using Service = Collection_Service<T, Json, Lock>;
@@ -145,6 +153,9 @@ public:
Json items_json() const {
return service_->items_json();
}
Json view_schema() const {
return service_->view_schema();
}
Json amis_schema() const {
return service_->amis_schema();
}
@@ -160,6 +171,11 @@ public:
return service->descriptor_response();
});
}, get_constraints);
app.registerHandler(service->path() + "/view", [service](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
complete_drogon_request<Json>(callback, [service] {
return service->view_response();
});
}, get_constraints);
app.registerHandler(service->path() + "/amis", [service](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
complete_drogon_request<Json>(callback, [service] {
return service->amis_response();
@@ -183,6 +199,16 @@ 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) {
continue;
}
const std::string value = request->getParameter(column.field);
if(!value.empty()) {
query.fields.insert_or_assign(column.field, value);
}
}
complete_drogon_request<Json>(callback, [service, query = std::move(query)]() mutable {
return service->list_response(std::move(query));
});
@@ -41,6 +41,9 @@ public:
template <class Managed, std::size_t Root_Index, auto... Members>
requires (!std::is_const_v<Managed>) && std::same_as<typename Managed_Field<Managed, Root_Index, Members...>::value_type, std::remove_cvref_t<T>>
explicit Http_Resource(Managed_Field<Managed, Root_Index, Members...> value, std::string path, Transaction transaction = {}) : service_(std::make_shared<Service>(value, std::move(path), std::move(transaction))) {}
Json view_schema() const {
return service_->view_schema();
}
Json amis_schema() const {
return service_->amis_schema();
}
@@ -49,6 +52,9 @@ public:
server.Get(service->path() + "/descriptor", [service](const httplib::Request&, httplib::Response& response) {
write_http_response(response, service->descriptor_response());
});
server.Get(service->path() + "/view", [service](const httplib::Request&, httplib::Response& response) {
write_http_response(response, service->view_response());
});
server.Get(service->path() + "/data", [service](const httplib::Request&, httplib::Response& response) {
write_http_response(response, service->data_response());
});
@@ -63,7 +69,7 @@ private:
std::shared_ptr<Service> service_;
};
template <Described_Type T, Json_Type Json, Basic_Lock Lock = std::mutex>
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
requires Table_View_Described_Type<T> && std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
class Http_Collection_Resource {
public:
using Service = Collection_Service<T, Json, Lock>;
@@ -82,6 +88,9 @@ public:
Json items_json() const {
return service_->items_json();
}
Json view_schema() const {
return service_->view_schema();
}
Json amis_schema() const {
return service_->amis_schema();
}
@@ -90,6 +99,9 @@ public:
server.Get(service->path() + "/descriptor", [service](const httplib::Request&, httplib::Response& response) {
write_http_response(response, service->descriptor_response());
});
server.Get(service->path() + "/view", [service](const httplib::Request&, httplib::Response& response) {
write_http_response(response, service->view_response());
});
server.Get(service->path() + "/amis", [service](const httplib::Request&, httplib::Response& response) {
write_http_response(response, service->amis_response());
});
@@ -109,6 +121,12 @@ public:
query.order_by = request.get_param_value("orderBy");
query.order_dir = request.has_param("orderDir") ? request.get_param_value("orderDir") : "asc";
}
const auto view = describe_table_view<T>();
for(const auto& column : view.columns) {
if((column.searchable || column.filterable) && request.has_param(column.field)) {
query.fields.insert_or_assign(column.field, request.get_param_value(column.field));
}
}
write_http_response(response, service->list_response(std::move(query)));
});
server.Get(item_pattern(service->path()), [service](const httplib::Request& request, httplib::Response& response) {
+7 -4
View File
@@ -18,8 +18,11 @@ std::vector<Network_Device_Row> make_network_rows() {
};
}
Json make_table_page(std::string_view title, std::string_view color, Json config_form, Json table_page) {
Json heading{{"type", "tpl"}, {"tpl", "<h3 style=\"margin:0;color:" + std::string(color) + "\">" + std::string(title) + "</h3>"}};
return Json{{"type", "container"}, {"body", Json::array({std::move(heading), std::move(config_form), std::move(table_page.at("body"))})}};
const auto view = composition_view("device_table", compose::vertical(compose::heading(std::string(title)).accent(std::string(color)), compose::slot("config"), compose::slot("table")));
std::map<std::string, Json> slots;
slots.insert_or_assign("config", std::move(config_form));
slots.insert_or_assign("table", std::move(table_page.at("body")));
return to_amis_composition_schema<Json>(view, slots);
}
}
Serial_Device_Table::Serial_Device_Table(Config_Store& store) : store_(store), config_resource_(store.serial_table(), "/admin/device_tables/serial/config", store.serial_table_transaction()), rows_("/admin/device_tables/serial/items", make_serial_rows()) {}
@@ -34,7 +37,7 @@ std::string_view Serial_Device_Table::type_label() const noexcept {
}
Json Serial_Device_Table::amis_schema() const {
const Serial_Table_Config config = store_.serial_table().snapshot();
return make_table_page(config.group_name, config.accent_color, to_amis_form_schema<Json>(config, "/admin/device_tables/serial/config/data", "确认修改"), rows_.amis_schema());
return make_table_page(config.group_name, config.accent_color, to_amis_form_schema<Json>(config, describe_edit_view<Serial_Table_Config>(), "/admin/device_tables/serial/config/data"), rows_.amis_schema());
}
void Serial_Device_Table::bind(httplib::Server& server) {
config_resource_.bind(server);
@@ -52,7 +55,7 @@ std::string_view Network_Device_Table::type_label() const noexcept {
}
Json Network_Device_Table::amis_schema() const {
const Network_Table_Config config = store_.network_table().snapshot();
return make_table_page(config.group_name, config.accent_color, to_amis_form_schema<Json>(config, "/admin/device_tables/network/config/data", "确认修改"), rows_.amis_schema());
return make_table_page(config.group_name, config.accent_color, to_amis_form_schema<Json>(config, describe_edit_view<Network_Table_Config>(), "/admin/device_tables/network/config/data"), rows_.amis_schema());
}
void Network_Device_Table::bind(httplib::Server& server) {
config_resource_.bind(server);
+59 -78
View File
@@ -19,138 +19,119 @@ template <>
struct Type_Descriptor<example::Endpoint_Config> {
static auto get() {
using T = example::Endpoint_Config;
return object<T>(
"endpoint_config",
ADMINIVE_FIELD_LABEL(T, enabled, "启用端点").editable().creatable().description("控制当前端点是否参与运行"),
ADMINIVE_FIELD_LABEL(T, host, "主机地址").editable().creatable().required().description("字符串输入示例"),
ADMINIVE_FIELD_LABEL(T, port, "端口").editable().creatable().required().description("整数输入示例")
).label("端点配置");
return object<T>("endpoint_config", ADMINIVE_FIELD_LABEL(T, enabled, "启用端点").editable().creatable().description("控制当前端点是否参与运行"), ADMINIVE_FIELD_LABEL(T, host, "主机地址").editable().creatable().required().description("字符串输入示例"), ADMINIVE_FIELD_LABEL(T, port, "端口").editable().creatable().required().description("整数输入示例")).label("端点配置");
}
};
template <>
struct Type_Descriptor<example::Appearance_Config> {
static auto get() {
using T = example::Appearance_Config;
return object<T>(
"appearance_config",
ADMINIVE_FIELD_LABEL(T, panel_title, "面板标题").editable().creatable().required(),
ADMINIVE_FIELD_LABEL(T, effective_date, "生效日期").editable().creatable().required().widget("input-date"),
ADMINIVE_FIELD_LABEL(T, accent_color, "主题颜色").editable().creatable().required().widget("input-color")
).label("界面配置");
return object<T>("appearance_config", ADMINIVE_FIELD_LABEL(T, panel_title, "面板标题").editable().creatable().required(), ADMINIVE_FIELD_LABEL(T, effective_date, "生效日期").editable().creatable().required().date_input(), ADMINIVE_FIELD_LABEL(T, accent_color, "主题颜色").editable().creatable().required().color_input()).label("界面配置");
}
};
template <>
struct Type_Descriptor<example::Radio_Service_Config> {
static auto get() {
using T = example::Radio_Service_Config;
return object<T>(
"radio_service_config",
ADMINIVE_FIELD_LABEL(T, profile_name, "配置名称").editable().required(),
ADMINIVE_FIELD_LABEL(T, mode, "运行模式").editable().required().enum_label<example::Radio_Mode::receive>("接收").enum_label<example::Radio_Mode::transmit>("发送").enum_label<example::Radio_Mode::duplex>("双工").enum_label<example::Radio_Mode::maintenance>("维护"),
ADMINIVE_FIELD_LABEL(T, worker_count, "工作线程数").editable().required(),
ADMINIVE_FIELD_LABEL(T, receive_gain, "接收增益").editable().required(),
ADMINIVE_FIELD_LABEL(T, primary_endpoint, "主端点").editable(),
ADMINIVE_FIELD_LABEL(T, backup_endpoint, "备用端点").editable().visible_on("${$self.mode == 'duplex'}"),
ADMINIVE_FIELD_LABEL(T, appearance, "界面显示").editable()
).label("无线电服务配置").confirm_label("确认修改");
return object<T>("radio_service_config", ADMINIVE_FIELD_LABEL(T, profile_name, "配置名称").editable().required(), ADMINIVE_FIELD_LABEL(T, mode, "运行模式").editable().required().enum_label<example::Radio_Mode::receive>("接收").enum_label<example::Radio_Mode::transmit>("发送").enum_label<example::Radio_Mode::duplex>("双工").enum_label<example::Radio_Mode::maintenance>("维护"), ADMINIVE_FIELD_LABEL(T, worker_count, "工作线程数").editable().required(), ADMINIVE_FIELD_LABEL(T, receive_gain, "接收增益").editable().required(), ADMINIVE_FIELD_LABEL(T, primary_endpoint, "主端点").editable(), ADMINIVE_FIELD_LABEL(T, backup_endpoint, "备用端点").editable().visible_on("${$self.mode == 'duplex'}"), ADMINIVE_FIELD_LABEL(T, appearance, "界面显示").editable()).label("无线电服务配置");
}
};
template <>
struct Type_View_Descriptor<example::Radio_Service_Config> {
static Form_View edit() {
using T = example::Radio_Service_Config;
return edit_form<T>(tabs(tab("基础", group("运行参数", use<&T::profile_name>(), use<&T::mode>(), horizontal(use<&T::worker_count>(), use<&T::receive_gain>()))), tab("端点", horizontal(card("主端点", use<&T::primary_endpoint>()), card("备用端点", use<&T::backup_endpoint>()))), tab("界面", card("显示设置", use<&T::appearance>())))).submit("确认修改");
}
};
template <>
struct Type_Descriptor<example::Alert_Config> {
static auto get() {
using T = example::Alert_Config;
return object<T>(
"alert_config",
ADMINIVE_FIELD_LABEL(T, enabled, "启用告警").editable(),
ADMINIVE_FIELD_LABEL(T, rule_name, "规则名称").editable().required(),
ADMINIVE_FIELD_LABEL(T, channel, "发送通道").editable().required().enum_label<example::Alert_Channel::dashboard>("控制台").enum_label<example::Alert_Channel::email>("邮件").enum_label<example::Alert_Channel::webhook>("Webhook"),
ADMINIVE_FIELD_LABEL(T, minimum_level, "最低日志级别").editable().required().enum_label<example::Log_Level::trace>("跟踪").enum_label<example::Log_Level::debug>("调试").enum_label<example::Log_Level::info>("信息").enum_label<example::Log_Level::warning>("警告").enum_label<example::Log_Level::error>("错误"),
ADMINIVE_FIELD_LABEL(T, repeat_minutes, "重复间隔(分钟)").editable().required(),
ADMINIVE_FIELD_LABEL(T, effective_date, "生效日期").editable().required().widget("input-date"),
ADMINIVE_FIELD_LABEL(T, highlight_color, "高亮颜色").editable().required().widget("input-color"),
ADMINIVE_FIELD_LABEL(T, webhook_endpoint, "Webhook 端点").editable().visible_on("${$self.enabled && $self.channel == 'webhook'}")
).label("告警配置").confirm_label("确认修改");
return object<T>("alert_config", ADMINIVE_FIELD_LABEL(T, enabled, "启用告警").editable(), ADMINIVE_FIELD_LABEL(T, rule_name, "规则名称").editable().required(), ADMINIVE_FIELD_LABEL(T, channel, "发送通道").editable().required().enum_label<example::Alert_Channel::dashboard>("控制台").enum_label<example::Alert_Channel::email>("邮件").enum_label<example::Alert_Channel::webhook>("Webhook"), ADMINIVE_FIELD_LABEL(T, minimum_level, "最低日志级别").editable().required().enum_label<example::Log_Level::trace>("跟踪").enum_label<example::Log_Level::debug>("调试").enum_label<example::Log_Level::info>("信息").enum_label<example::Log_Level::warning>("警告").enum_label<example::Log_Level::error>("错误"), ADMINIVE_FIELD_LABEL(T, repeat_minutes, "重复间隔(分钟)").editable().required(), ADMINIVE_FIELD_LABEL(T, effective_date, "生效日期").editable().required().date_input(), ADMINIVE_FIELD_LABEL(T, highlight_color, "高亮颜色").editable().required().color_input(), ADMINIVE_FIELD_LABEL(T, webhook_endpoint, "Webhook 端点").editable().visible_on("${$self.enabled && $self.channel == 'webhook'}")).label("告警配置");
}
};
template <>
struct Type_View_Descriptor<example::Alert_Config> {
static Form_View edit() {
using T = example::Alert_Config;
return edit_form<T>(vertical(group("告警规则", horizontal(use<&T::enabled>(), use<&T::rule_name>()), grid(3, use<&T::channel>(), use<&T::minimum_level>(), use<&T::repeat_minutes>())), flow(card("生效日期", use<&T::effective_date>()), card("高亮颜色", use<&T::highlight_color>())), group("Webhook", use<&T::webhook_endpoint>()).collapsible_when())).submit("确认修改");
}
};
template <>
struct Type_Descriptor<example::Serial_Table_Config> {
static auto get() {
using T = example::Serial_Table_Config;
return object<T>(
"serial_table_config",
ADMINIVE_FIELD_LABEL(T, group_name, "分组名称").editable().required(),
ADMINIVE_FIELD_LABEL(T, default_baud_rate, "默认波特率").editable().required(),
ADMINIVE_FIELD_LABEL(T, default_parity, "默认校验位").editable().required().enum_label<example::Serial_Parity::none>("无校验").enum_label<example::Serial_Parity::odd>("奇校验").enum_label<example::Serial_Parity::even>("偶校验"),
ADMINIVE_FIELD_LABEL(T, accent_color, "界面颜色").editable().required().widget("input-color")
).label("串口表格配置").confirm_label("确认修改");
return object<T>("serial_table_config", ADMINIVE_FIELD_LABEL(T, group_name, "分组名称").editable().required(), ADMINIVE_FIELD_LABEL(T, default_baud_rate, "默认波特率").editable().required(), ADMINIVE_FIELD_LABEL(T, default_parity, "默认校验位").editable().required().enum_label<example::Serial_Parity::none>("无校验").enum_label<example::Serial_Parity::odd>("奇校验").enum_label<example::Serial_Parity::even>("偶校验"), ADMINIVE_FIELD_LABEL(T, accent_color, "界面颜色").editable().required().color_input()).label("串口表格配置");
}
};
template <>
struct Type_View_Descriptor<example::Serial_Table_Config> {
static Form_View edit() {
using T = example::Serial_Table_Config;
return edit_form<T>(horizontal(group("默认参数", use<&T::group_name>(), use<&T::default_baud_rate>(), use<&T::default_parity>()), card("显示", use<&T::accent_color>()))).submit("确认修改");
}
};
template <>
struct Type_Descriptor<example::Network_Table_Config> {
static auto get() {
using T = example::Network_Table_Config;
return object<T>(
"network_table_config",
ADMINIVE_FIELD_LABEL(T, group_name, "分组名称").editable().required(),
ADMINIVE_FIELD_LABEL(T, default_protocol, "默认协议").editable().required().enum_label<example::Network_Protocol::tcp>("TCP").enum_label<example::Network_Protocol::udp>("UDP").enum_label<example::Network_Protocol::websocket>("WebSocket"),
ADMINIVE_FIELD_LABEL(T, timeout_ms, "超时时间(毫秒)").editable().required(),
ADMINIVE_FIELD_LABEL(T, tls_enabled, "默认启用 TLS").editable(),
ADMINIVE_FIELD_LABEL(T, accent_color, "界面颜色").editable().required().widget("input-color")
).label("网络表格配置").confirm_label("确认修改");
return object<T>("network_table_config", ADMINIVE_FIELD_LABEL(T, group_name, "分组名称").editable().required(), ADMINIVE_FIELD_LABEL(T, default_protocol, "默认协议").editable().required().enum_label<example::Network_Protocol::tcp>("TCP").enum_label<example::Network_Protocol::udp>("UDP").enum_label<example::Network_Protocol::websocket>("WebSocket"), ADMINIVE_FIELD_LABEL(T, timeout_ms, "超时时间(毫秒)").editable().required(), ADMINIVE_FIELD_LABEL(T, tls_enabled, "默认启用 TLS").editable(), ADMINIVE_FIELD_LABEL(T, accent_color, "界面颜色").editable().required().color_input()).label("网络表格配置");
}
};
template <>
struct Type_View_Descriptor<example::Network_Table_Config> {
static Form_View edit() {
using T = example::Network_Table_Config;
return edit_form<T>(grid(2, group("网络默认值", use<&T::group_name>(), use<&T::default_protocol>(), use<&T::timeout_ms>(), use<&T::tls_enabled>()), card("显示", use<&T::accent_color>()))).submit("确认修改");
}
};
template <>
struct Type_Descriptor<example::Application_Config> {
static auto get() {
using T = example::Application_Config;
return object<T>(
"application_config",
ADMINIVE_FIELD_LABEL(T, default_device_type, "默认设备类型").read_write(),
ADMINIVE_FIELD_LABEL(T, radio_service, "无线电服务").read_write(),
ADMINIVE_FIELD_LABEL(T, alerts, "告警").read_write(),
ADMINIVE_FIELD_LABEL(T, serial_table, "串口表格").read_write(),
ADMINIVE_FIELD_LABEL(T, network_table, "网络表格").read_write()
).label("Adminive 配置");
return object<T>("application_config", ADMINIVE_FIELD_LABEL(T, default_device_type, "默认设备类型").read_write(), ADMINIVE_FIELD_LABEL(T, radio_service, "无线电服务").read_write(), ADMINIVE_FIELD_LABEL(T, alerts, "告警").read_write(), ADMINIVE_FIELD_LABEL(T, serial_table, "串口表格").read_write(), ADMINIVE_FIELD_LABEL(T, network_table, "网络表格").read_write()).label("Adminive 配置");
}
};
template <>
struct Type_Descriptor<example::Radio_State> {
static auto get() {
using T = example::Radio_State;
return object<T>(
"radio_state",
ADMINIVE_FIELD_LABEL(T, mode, "运行模式").creatable().editable().required().list_label("模式").order(5).sortable().enum_label<example::Radio_Mode::receive>("接收").enum_label<example::Radio_Mode::transmit>("发送").enum_label<example::Radio_Mode::duplex>("双工").enum_label<example::Radio_Mode::maintenance>("维护"),
ADMINIVE_FIELD_LABEL(T, port, "监听端口").creatable().editable().required().list_label("监听端口").order(20).sortable(),
ADMINIVE_FIELD_LABEL(T, buffer_count, "缓冲区数量").creatable().editable().required().list_label("缓冲区").order(10).sortable(),
ADMINIVE_FIELD_LABEL(T, low_watermark, "低水位").creatable().editable().list_label("低水位").order(30).sortable(),
ADMINIVE_FIELD_LABEL(T, high_watermark, "高水位").creatable().editable().list_label("高水位").order(40).sortable()
).label("无线电状态").validator(T::Watermark_Validator{}).id_label("编号").create_label("新增").edit_label("编辑").delete_label("删除").actions_label("操作").confirm_label("确认修改").delete_confirm("确定删除这条无线电状态吗?").view_status_label("查看状态 ▾").management_label("无线电状态列表").default_sort("buffer_count");
return object<T>("radio_state", ADMINIVE_FIELD_LABEL(T, mode, "运行模式").creatable().editable().required().enum_label<example::Radio_Mode::receive>("接收").enum_label<example::Radio_Mode::transmit>("发送").enum_label<example::Radio_Mode::duplex>("双工").enum_label<example::Radio_Mode::maintenance>("维护"), ADMINIVE_FIELD_LABEL(T, port, "监听端口").creatable().editable().required(), ADMINIVE_FIELD_LABEL(T, buffer_count, "缓冲区数量").creatable().editable().required(), ADMINIVE_FIELD_LABEL(T, low_watermark, "低水位").creatable().editable(), ADMINIVE_FIELD_LABEL(T, high_watermark, "高水位").creatable().editable()).label("无线电状态").validator(T::Watermark_Validator{});
}
};
template <>
struct Type_View_Descriptor<example::Radio_State> {
static Table_View table() {
using T = example::Radio_State;
return table_view<T>(column<&T::mode>("模式").sort().filter(), column<&T::buffer_count>("缓冲区").sort(), column<&T::port>("监听端口").sort().search(), column<&T::low_watermark>("低水位").sort(), column<&T::high_watermark>("高水位").sort()).titled("无线电状态列表").id_title("编号").create_title("新增").edit_title("编辑").delete_title("删除").actions_title("操作").confirm_title("确认修改").delete_confirmation("确定删除这条无线电状态吗?").status_title("查看状态 ▾").default_sort("buffer_count");
}
};
template <>
struct Type_Descriptor<example::Serial_Device_Row> {
static auto get() {
using T = example::Serial_Device_Row;
return object<T>(
"serial_device",
ADMINIVE_FIELD_LABEL(T, port_name, "串口名称").creatable().editable().required().list_label("串口").order(10).sortable(),
ADMINIVE_FIELD_LABEL(T, baud_rate, "波特率").creatable().editable().required().list_label("波特率").order(20).sortable(),
ADMINIVE_FIELD_LABEL(T, parity, "校验位").creatable().editable().required().list_label("校验位").order(30).sortable().enum_label<example::Serial_Parity::none>("无校验").enum_label<example::Serial_Parity::odd>("奇校验").enum_label<example::Serial_Parity::even>("偶校验"),
ADMINIVE_FIELD_LABEL(T, enabled, "启用").creatable().editable().list_label("启用").order(40).sortable()
).label("串口设备").id_label("编号").create_label("新增串口").edit_label("编辑").delete_label("删除").actions_label("操作").confirm_label("确认修改").delete_confirm("确定删除这个串口吗?").management_label("串口设备列表").user_reorderable();
return object<T>("serial_device", ADMINIVE_FIELD_LABEL(T, port_name, "串口名称").creatable().editable().required(), ADMINIVE_FIELD_LABEL(T, baud_rate, "波特率").creatable().editable().required(), ADMINIVE_FIELD_LABEL(T, parity, "校验位").creatable().editable().required().enum_label<example::Serial_Parity::none>("无校验").enum_label<example::Serial_Parity::odd>("奇校验").enum_label<example::Serial_Parity::even>("偶校验"), ADMINIVE_FIELD_LABEL(T, enabled, "启用").creatable().editable()).label("串口设备");
}
};
template <>
struct Type_View_Descriptor<example::Serial_Device_Row> {
static Table_View table() {
using T = example::Serial_Device_Row;
return table_view<T>(column<&T::port_name>("串口").sort().search().fix(Table_Fixed::left), column<&T::baud_rate>("波特率").sort(), column<&T::parity>("校验位").sort().filter(), column<&T::enabled>("启用").sort().filter()).titled("串口设备列表").id_title("编号").create_title("新增串口").edit_title("编辑").delete_title("删除").actions_title("操作").confirm_title("确认修改").delete_confirmation("确定删除这个串口吗?").reorderable();
}
};
template <>
struct Type_Descriptor<example::Network_Device_Row> {
static auto get() {
using T = example::Network_Device_Row;
return object<T>(
"network_device",
ADMINIVE_FIELD_LABEL(T, endpoint, "网络地址").creatable().editable().required().list_label("地址").order(10).sortable(),
ADMINIVE_FIELD_LABEL(T, protocol, "协议").creatable().editable().required().list_label("协议").order(20).sortable().enum_label<example::Network_Protocol::tcp>("TCP").enum_label<example::Network_Protocol::udp>("UDP").enum_label<example::Network_Protocol::websocket>("WebSocket"),
ADMINIVE_FIELD_LABEL(T, timeout_ms, "超时时间").creatable().editable().required().list_label("超时(毫秒)").order(30).sortable(),
ADMINIVE_FIELD_LABEL(T, tls_enabled, "启用 TLS").creatable().editable().list_label("TLS").order(40).sortable()
).label("网络设备").id_label("编号").create_label("新增网络设备").edit_label("编辑").delete_label("删除").actions_label("操作").confirm_label("确认修改").delete_confirm("确定删除这个网络设备吗?").management_label("网络设备列表").user_reorderable();
return object<T>("network_device", ADMINIVE_FIELD_LABEL(T, endpoint, "网络地址").creatable().editable().required(), ADMINIVE_FIELD_LABEL(T, protocol, "协议").creatable().editable().required().enum_label<example::Network_Protocol::tcp>("TCP").enum_label<example::Network_Protocol::udp>("UDP").enum_label<example::Network_Protocol::websocket>("WebSocket"), ADMINIVE_FIELD_LABEL(T, timeout_ms, "超时时间").creatable().editable().required(), ADMINIVE_FIELD_LABEL(T, tls_enabled, "启用 TLS").creatable().editable()).label("网络设备");
}
};
template <>
struct Type_View_Descriptor<example::Network_Device_Row> {
static Table_View table() {
using T = example::Network_Device_Row;
return table_view<T>(column<&T::endpoint>("地址").sort().search().fix(Table_Fixed::left), column<&T::protocol>("协议").sort().filter(), column<&T::timeout_ms>("超时(毫秒)").sort(), column<&T::tls_enabled>("TLS").sort().filter()).titled("网络设备列表").id_title("编号").create_title("新增网络设备").edit_title("编辑").delete_title("删除").actions_title("操作").confirm_title("确认修改").delete_confirmation("确定删除这个网络设备吗?").reorderable();
}
};
}
+2 -2
View File
@@ -30,9 +30,9 @@ void describe_example() {
std::cout << adminive::to_descriptor_json<Json, Radio_State>().dump(2) << '\n';
std::cout << adminive::to_json<Json>(state).dump(2) << '\n';
std::cout << adminive::to_descriptor_json<Json, Radio_Service_Config>().dump(2) << '\n';
std::cout << adminive::to_amis_form_schema<Json>(service, "/admin/config/radio/data", "确认修改").dump(2) << '\n';
std::cout << adminive::to_amis_form_schema<Json>(service, adminive::describe_edit_view<Radio_Service_Config>(), "/admin/config/radio/data").dump(2) << '\n';
std::cout << adminive::to_descriptor_json<Json, Alert_Config>().dump(2) << '\n';
std::cout << adminive::to_amis_form_schema<Json>(alerts, "/admin/config/alerts/data", "确认修改").dump(2) << '\n';
std::cout << adminive::to_amis_form_schema<Json>(alerts, adminive::describe_edit_view<Alert_Config>(), "/admin/config/alerts/data").dump(2) << '\n';
}
}
int main(int argc, char** argv) {
@@ -134,12 +134,12 @@ struct Control_Adapter<advanced_test::Color_Code, advanced_test::Json> {
advanced_test::Json result;
result["type"] = "input-color";
result["name"] = make_field_name(context.prefix, field.at("name").get<std::string>());
result["label"] = field.at("label");
result["label"] = field.at("presentation").at("label");
result["clearable"] = true;
return result;
}
static advanced_test::Json make_column(const advanced_test::Json& field) {
return advanced_test::Json{{"type", "tpl"}, {"name", field.at("name")}, {"label", field.at("list_label")}, {"tpl", "<span style=\"color:${color}\">${color}</span>"}};
return advanced_test::Json{{"type", "tpl"}, {"name", field.at("name")}, {"label", field.at("presentation").at("label")}, {"tpl", "<span style=\"color:${color}\">${color}</span>"}};
}
};
template <>
@@ -171,7 +171,7 @@ template <>
struct Type_Descriptor<advanced_test::Control_Config> {
static auto get() {
using T = advanced_test::Control_Config;
return object<T>("control", "Control", ADMINIVE_FIELD(T, color).editable().list_label("颜色"));
return object<T>("control", "Control", ADMINIVE_FIELD(T, color).editable().label("颜色"));
}
};
template <>
@@ -255,7 +255,14 @@ template <>
struct Type_Descriptor<advanced_test::Invalid_Sort_Config> {
static auto get() {
using T = advanced_test::Invalid_Sort_Config;
return object<T>("invalid_sort", "Invalid Sort", ADMINIVE_FIELD(T, value)).default_sort("value");
return object<T>("invalid_sort", "Invalid Sort", ADMINIVE_FIELD(T, value));
}
};
template <>
struct Type_View_Descriptor<advanced_test::Invalid_Sort_Config> {
static Table_View table() {
using T = advanced_test::Invalid_Sort_Config;
return table_view<T>(column<&T::value>()).default_sort("value");
}
};
}
@@ -299,10 +306,10 @@ int main() {
assert(managed_inherited.field<0>().snapshot() == 12);
assert(managed_inherited.field<1>().snapshot() == "changed");
Control_Config controls;
const auto control_form = adminive::to_amis_form_schema<Json>(controls);
const auto control_form = adminive::to_amis_form_schema<Json>(controls, adminive::describe_edit_view<Control_Config>());
assert(control_form.at("body").at(0).at("type") == "input-color");
Device_Config devices;
const auto device_form = adminive::to_amis_form_schema<Json>(devices);
const auto device_form = adminive::to_amis_form_schema<Json>(devices, adminive::describe_edit_view<Device_Config>());
assert(device_form.at("body").at(0).at("type") == "fieldset");
assert(device_form.at("body").at(0).at("body").at(0).at("type") == "select");
assert(device_form.at("body").at(0).at("body").at(1).at("body").at(2).at("type") == "input-color");
@@ -333,7 +340,7 @@ int main() {
assert(duplicate_rejected);
bool invalid_sort_rejected{};
try {
static_cast<void>(adminive::to_descriptor_json<Json, Invalid_Sort_Config>());
adminive::validate_table_view<Invalid_Sort_Config>(adminive::describe_table_view<Invalid_Sort_Config>());
} catch(const std::invalid_argument&) {
invalid_sort_rejected = true;
}
+22 -2
View File
@@ -22,6 +22,13 @@ struct Type_Descriptor<drogon_test::Config> {
}
};
template <>
struct Type_View_Descriptor<drogon_test::Config> {
static Table_View table() {
using T = drogon_test::Config;
return table_view<T>(column<&T::name>("Name").sort().search());
}
};
template <>
struct Type_Descriptor<drogon_test::Status> {
static auto get() {
using T = drogon_test::Status;
@@ -63,9 +70,15 @@ int main() {
assert(app.filters("/config/data", drogon::Post).size() == 2);
assert(app.filters("/status/data", drogon::Get).size() == 2);
auto request = std::make_shared<drogon::HttpRequest>();
drogon::HttpResponsePtr response;
app.handle("/config/view", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) {
response = value;
});
assert(response->status == drogon::k200OK);
assert(Json::parse(response->body).at("data").at("kind") == "form");
request->body = R"({"name":"updated"})";
request->user = "wyc";
drogon::HttpResponsePtr response;
response.reset();
app.handle("/config/data", drogon::Post, request, [&response](const drogon::HttpResponsePtr& value) {
response = value;
});
@@ -102,7 +115,14 @@ int main() {
assert(response->status == drogon::k200OK);
adminive::Drogon_Collection_Resource<drogon_test::Config, Json> collection("/configs", {drogon_test::Config{"first"}});
collection.bind(app, options);
request->parameters = {{"page", "1"}, {"perPage", "10"}};
response.reset();
app.handle("/configs/view", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) {
response = value;
});
assert(response->status == drogon::k200OK);
assert(Json::parse(response->body).at("data").at("kind") == "table");
assert(Json::parse(response->body).at("data").at("columns").at(0).at("searchable") == true);
request->parameters = {{"page", "1"}, {"perPage", "10"}, {"name", "fir"}};
response.reset();
app.handle("/configs", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) {
response = value;
+1 -1
View File
@@ -115,7 +115,7 @@ int main() {
assert(Probe_Lock::exclusive_locks.load(std::memory_order_relaxed) == 0);
assert(Probe_Lock::shared_locks.load(std::memory_order_relaxed) == 0);
const auto descriptor = adminive::to_descriptor_json<Json, Section_Config>();
assert(descriptor.at("protocol_version") == 4);
assert(descriptor.at("protocol_version") == 5);
assert(!descriptor.at("fields").at(0).contains("lock_mode"));
assert(descriptor.at("fields").at(0).at("writable") == true);
assert(descriptor.at("fields").at(0).at("synchronized") == true);
+2 -2
View File
@@ -276,7 +276,7 @@ int main() {
update = adminive::apply_frontend_create<Json>(created, Json{{"child", Json{{"editable_value", 5}}}});
assert(!update.success);
assert(update.field_errors.contains("child.required_value"));
const auto root_form = adminive::to_amis_form_schema<Json>(root);
const auto root_form = adminive::to_amis_form_schema<Json>(root, adminive::describe_edit_view<Root_Config>());
assert(root_form.at("body").at(0).at("body").at(2).at("visibleOn") == "${child.editable_value > 0}");
Numeric_Config numeric;
update = adminive::apply_frontend_patch<Json>(numeric, Json{{"signed_value", 1.5}});
@@ -286,7 +286,7 @@ int main() {
update = adminive::apply_frontend_patch<Json>(numeric, Json{{"signed_value", std::numeric_limits<std::uint64_t>::max()}});
assert(!update.success);
Optional_Config optional;
const auto optional_form = adminive::to_amis_form_schema<Json>(optional);
const auto optional_form = adminive::to_amis_form_schema<Json>(optional, adminive::describe_edit_view<Optional_Config>());
assert(optional_form.at("body").at(0).at("clearable") == true);
assert(optional_form.at("body").at(1).at("clearable") == true);
update = adminive::apply_frontend_patch<Json>(optional, Json{{"count", nullptr}, {"note", nullptr}});
+120 -56
View File
@@ -1,33 +1,91 @@
#include "example_descriptors.hpp"
#include <cassert>
#include <string>
#include <string_view>
#include <vector>
namespace {
using Json = adminive::example::Json;
const Json* find_named_node(const Json& value, std::string_view name) {
if(value.is_object()) {
if(value.contains("name") && value.at("name").is_string() && value.at("name").get<std::string>() == name) {
return &value;
}
for(const auto& [key, child] : value.items()) {
static_cast<void>(key);
if(const Json* result = find_named_node(child, name)) {
return result;
}
}
} else if(value.is_array()) {
for(const auto& child : value) {
if(const Json* result = find_named_node(child, name)) {
return result;
}
}
}
return nullptr;
}
const Json* find_titled_node(const Json& value, std::string_view title) {
if(value.is_object()) {
if(value.contains("title") && value.at("title").is_string() && value.at("title").get<std::string>() == title) {
return &value;
}
for(const auto& [key, child] : value.items()) {
static_cast<void>(key);
if(const Json* result = find_titled_node(child, title)) {
return result;
}
}
} else if(value.is_array()) {
for(const auto& child : value) {
if(const Json* result = find_titled_node(child, title)) {
return result;
}
}
}
return nullptr;
}
}
int main() {
using adminive::example::Alert_Config;
using adminive::example::Radio_Item_Status;
using adminive::example::Radio_Mode;
using adminive::example::Radio_Service_Config;
using adminive::example::Radio_State;
using Json = adminive::example::Json;
Radio_State state;
const auto descriptor = adminive::to_descriptor_json<Json, Radio_State>();
assert(descriptor.at("protocol") == "adminive.resource");
assert(descriptor.at("protocol_version") == 5);
assert(descriptor.at("name") == "radio_state");
assert(descriptor.at("fields").size() == 5);
assert(descriptor.at("fields").at(0).at("name") == "mode");
assert(descriptor.at("fields").at(0).at("label") == "运行模式");
assert(descriptor.at("fields").at(0).at("list_label") == "模式");
assert(descriptor.at("fields").at(0).at("value_type") == "enum");
assert(descriptor.at("fields").at(0).at("options").size() == 4);
assert(descriptor.at("fields").at(0).at("options").at(0).at("value") == "receive");
assert(descriptor.at("fields").at(0).at("options").at(2).at("value") == "duplex");
assert(descriptor.at("fields").at(0).at("order") == 5);
assert(descriptor.at("fields").at(0).at("sortable") == true);
assert(descriptor.at("fields").at(1).at("name") == "port");
assert(descriptor.at("fields").at(1).at("list_label") == "监听端口");
assert(descriptor.at("fields").at(1).at("minimum") == 1);
assert(descriptor.at("fields").at(1).at("maximum") == 65535);
assert(descriptor.at("fields").at(2).at("name") == "buffer_count");
const auto& mode_field = descriptor.at("fields").at(0);
assert(mode_field.at("name") == "mode");
assert(mode_field.at("presentation").at("label") == "运行模式");
assert(mode_field.at("value_type") == "enum");
assert(mode_field.at("presentation").at("options").size() == 4);
assert(mode_field.at("presentation").at("options").at(0).at("value") == "receive");
assert(mode_field.at("presentation").at("options").at(2).at("value") == "duplex");
const auto& port_field = descriptor.at("fields").at(1);
assert(port_field.at("name") == "port");
assert(port_field.at("presentation").at("label") == "监听端口");
assert(port_field.at("minimum") == 1);
assert(port_field.at("maximum") == 65535);
assert(descriptor.at("fields").at(2).at("multiple_of") == 2);
assert(!descriptor.contains("list"));
const auto table_view = adminive::describe_table_view<Radio_State>();
assert(table_view.columns.size() == 5);
assert(table_view.columns.at(0).field == "mode");
assert(table_view.columns.at(0).label == "模式");
assert(table_view.columns.at(0).sortable);
assert(table_view.columns.at(0).filterable);
assert(table_view.columns.at(2).field == "port");
assert(table_view.columns.at(2).searchable);
assert(table_view.default_order_by == "buffer_count");
const auto table_view_json = adminive::to_view_json<Json, Radio_State>(table_view);
assert(table_view_json.at("protocol") == "adminive.view");
assert(table_view_json.at("kind") == "table");
assert(table_view_json.at("columns").at(0).at("field") == "mode");
assert(table_view_json.at("columns").at(0).at("filterable") == true);
const auto initial = adminive::to_json<Json>(state);
assert(initial.at("mode") == "receive");
assert(initial.at("port") == 9999);
@@ -57,29 +115,27 @@ int main() {
assert(!result.success);
assert(result.field_errors.contains("mode"));
assert(result.field_errors.contains("buffer_count"));
const auto form = adminive::to_amis_form_schema<Json>(state, "/admin/radio_state", "Confirm Changes");
const auto form_view = adminive::edit_form<Radio_State>().submit("Confirm Changes");
const auto form = adminive::to_amis_form_schema<Json>(state, form_view, "/admin/radio_state");
assert(form.at("type") == "form");
assert(form.at("body").size() == 5);
assert(form.at("body").at(0).at("type") == "select");
assert(form.at("body").at(0).at("options").size() == 4);
assert(form.at("body").at(1).at("type") == "input-number");
assert(form.at("body").at(2).at("step") == 2);
assert(form.at("actions").size() == 2);
assert(form.at("actions").at(0).at("type") == "reset");
assert(form.at("actions").at(1).at("type") == "submit");
assert(form.at("actions").at(1).at("label") == "Confirm Changes");
const auto crud = adminive::to_amis_crud_schema<Json, Radio_State>("/admin/radio_states");
const auto crud = adminive::to_amis_table_schema<Json, Radio_State>("/admin/radio_states", table_view);
assert(crud.at("type") == "page");
assert(crud.at("body").at("type") == "crud");
assert(crud.at("body").at("columns").size() == 7);
assert(crud.at("body").at("columns").at(1).at("name") == "mode");
assert(crud.at("body").at("columns").at(1).at("type") == "mapping");
assert(crud.at("body").at("columns").at(1).at("map").at("duplex") == "双工");
assert(crud.at("body").at("columns").at(2).at("name") == "buffer_count");
assert(crud.at("body").at("columns").at(3).at("name") == "port");
assert(!crud.at("body").contains("interval"));
const auto& create_form = crud.at("body").at("headerToolbar").at(2).at("dialog").at("body");
assert(create_form.at("actions").at(1).at("type") == "submit");
const auto& crud_body = crud.at("body").at(0);
assert(crud_body.at("type") == "crud");
assert(crud_body.at("columns").size() == 7);
assert(crud_body.at("columns").at(1).at("name") == "mode");
assert(crud_body.at("columns").at(1).at("type") == "mapping");
assert(crud_body.at("columns").at(1).at("map").at("duplex") == "双工");
assert(crud_body.at("columns").at(1).at("filterable") == true);
assert(crud_body.at("columns").at(3).at("name") == "port");
assert(crud_body.at("columns").at(3).at("searchable") == true);
const auto& create_form = crud_body.at("headerToolbar").at(2).at("dialog").at("body");
assert(create_form.at("actions").at(1).at("label") == "新增");
const auto status_descriptor = adminive::to_status_descriptor_json<Json, Radio_Item_Status>();
assert(status_descriptor.at("protocol") == "adminive.status");
@@ -94,7 +150,7 @@ int main() {
assert(first_status.at("refresh_sequence").at("value") == 1);
assert(second_status.at("refresh_sequence").at("value") == 2);
const auto overview_descriptor = adminive::to_status_descriptor_json<Json, adminive::example::Server_Status>();
const auto status_crud = adminive::to_amis_crud_status_schema<Json, Radio_State>("/admin/radio_states", "/admin/status", overview_descriptor, 2000, &status_descriptor);
const auto status_crud = adminive::to_amis_table_schema<Json, Radio_State>("/admin/radio_states", table_view, &status_descriptor, 2000, &overview_descriptor, "/admin/status", 2000);
assert(status_crud.at("body").is_array());
assert(status_crud.at("body").at(0).at("type") == "service");
assert(status_crud.at("body").at(1).at("columns").size() == 8);
@@ -117,38 +173,46 @@ int main() {
assert(service_descriptor.at("fields").at(3).at("value_type") == "number");
assert(service_descriptor.at("fields").at(4).at("value_type") == "object");
assert(service_descriptor.at("fields").at(4).at("children").size() == 3);
assert(service_descriptor.at("fields").at(5).at("visible_on") == "${$self.mode == 'duplex'}");
assert(service_descriptor.at("fields").at(6).at("children").at(1).at("widget") == "input-date");
assert(service_descriptor.at("fields").at(6).at("children").at(2).at("widget") == "input-color");
const auto service_form = adminive::to_amis_form_schema<Json>(service_config, "/admin/config/radio/data", "Confirm Changes");
assert(service_form.at("body").at(0).at("type") == "input-text");
assert(service_form.at("body").at(1).at("type") == "select");
assert(service_form.at("body").at(2).at("type") == "input-number");
assert(service_form.at("body").at(3).at("type") == "input-number");
assert(service_form.at("body").at(4).at("type") == "fieldset");
assert(service_form.at("body").at(4).at("body").at(1).at("name") == "primary_endpoint.host");
assert(service_form.at("body").at(5).at("visibleOn") == "${mode == 'duplex'}");
assert(service_form.at("body").at(6).at("body").at(1).at("type") == "input-date");
assert(service_form.at("body").at(6).at("body").at(1).at("valueFormat") == "YYYY-MM-DD");
assert(service_form.at("body").at(6).at("body").at(2).at("type") == "input-color");
assert(service_form.at("actions").at(1).at("type") == "submit");
assert(service_descriptor.at("fields").at(5).at("presentation").at("visible_on") == "${$self.mode == 'duplex'}");
assert(service_descriptor.at("fields").at(6).at("children").at(1).at("presentation").at("control") == "date");
assert(service_descriptor.at("fields").at(6).at("children").at(2).at("presentation").at("control") == "color");
const auto service_view = adminive::describe_edit_view<Radio_Service_Config>();
const auto service_view_json = adminive::to_view_json<Json, Radio_Service_Config>(service_view);
assert(service_view_json.at("body").at("kind") == "tabs");
assert(service_view_json.at("body").at("children").size() == 3);
const auto service_form = adminive::to_amis_form_schema<Json>(service_config, service_view, "/admin/config/radio/data");
assert(service_form.at("body").at(0).at("type") == "tabs");
const Json* profile_control = find_named_node(service_form, "profile_name");
const Json* mode_control = find_named_node(service_form, "mode");
const Json* worker_control = find_named_node(service_form, "worker_count");
const Json* date_control = find_named_node(service_form, "appearance.effective_date");
const Json* color_control = find_named_node(service_form, "appearance.accent_color");
assert(profile_control && profile_control->at("type") == "input-text");
assert(mode_control && mode_control->at("type") == "select");
assert(worker_control && worker_control->at("type") == "input-number");
assert(date_control && date_control->at("type") == "input-date");
assert(color_control && color_control->at("type") == "input-color");
assert(service_form.at("actions").at(1).at("label") == "确认修改");
result = adminive::apply_frontend_patch<Json>(service_config, Json{{"profile_name", "Duplex Profile"}, {"mode", "duplex"}, {"worker_count", 8}, {"receive_gain", 2.5}, {"primary_endpoint", Json{{"enabled", true}, {"host", "10.0.0.1"}, {"port", 9200}}}, {"backup_endpoint", Json{{"enabled", true}, {"host", "10.0.0.2"}, {"port", 9201}}}, {"appearance", Json{{"panel_title", "Duplex Radio"}, {"effective_date", "2026-08-07"}, {"accent_color", "#16a34a"}}}});
assert(result.success);
assert(service_config.mode == Radio_Mode::duplex);
assert(service_config.primary_endpoint.host == "10.0.0.1");
assert(service_config.appearance.accent_color == "#16a34a");
Alert_Config alert_config;
const auto alert_form = adminive::to_amis_form_schema<Json>(alert_config, "/admin/config/alerts/data", "Confirm Changes");
assert(alert_form.at("body").size() == 8);
assert(alert_form.at("body").at(1).at("type") == "input-text");
assert(alert_form.at("body").at(2).at("type") == "select");
assert(alert_form.at("body").at(3).at("type") == "select");
assert(alert_form.at("body").at(4).at("type") == "input-number");
assert(alert_form.at("body").at(5).at("type") == "input-date");
assert(alert_form.at("body").at(6).at("type") == "input-color");
assert(alert_form.at("body").at(7).at("type") == "fieldset");
assert(alert_form.at("body").at(7).at("visibleOn") == "${enabled && channel == 'webhook'}");
const auto alert_view = adminive::describe_edit_view<Alert_Config>();
const auto alert_view_json = adminive::to_view_json<Json, Alert_Config>(alert_view);
assert(alert_view_json.at("body").at("kind") == "vertical");
const auto alert_form = adminive::to_amis_form_schema<Json>(alert_config, alert_view, "/admin/config/alerts/data");
assert(find_named_node(alert_form, "rule_name"));
assert(find_named_node(alert_form, "effective_date")->at("type") == "input-date");
assert(find_named_node(alert_form, "highlight_color")->at("type") == "input-color");
const Json* webhook_group = find_titled_node(alert_form, "Webhook 端点");
assert(webhook_group);
assert(webhook_group->at("visibleOn") == "${enabled && channel == 'webhook'}");
adminive::example::Http_Resource<Radio_Service_Config> service_resource(service_config, "/admin/config/radio");
const auto resource_view = service_resource.view_schema();
assert(resource_view.at("protocol") == "adminive.view");
assert(resource_view.at("kind") == "form");
const auto resource_form = service_resource.amis_schema();
assert(resource_form.at("api").at("method") == "post");
assert(resource_form.at("api").at("url") == "/admin/config/radio/data");
+125
View File
@@ -0,0 +1,125 @@
#include "adminive/adminive.hpp"
#include "adminive/adapters/nlohmann_json.hpp"
#include <cassert>
#include <stdexcept>
#include <string>
namespace view_test {
using Json = nlohmann::json;
struct Config {
std::string name{"Alpha"};
int count{3};
bool enabled{true};
std::string color{"#336699"};
};
}
namespace adminive {
template <>
struct Type_Descriptor<view_test::Config> {
static auto get() {
using T = view_test::Config;
return object<T>("view_config", "View Config", ADMINIVE_FIELD(T, name).editable().creatable().label("Name").text_input(), ADMINIVE_FIELD(T, count).editable().creatable().label("Count").number_input(), ADMINIVE_FIELD(T, enabled).editable().creatable().label("Enabled").boolean_input(), ADMINIVE_FIELD(T, color).editable().creatable().label("Color").color_input());
}
};
template <>
struct Type_View_Descriptor<view_test::Config> {
static Form_View edit() {
using T = view_test::Config;
return edit_form<T>(vertical(group("Identity", horizontal(use<&T::name>(), use<&T::enabled>())), grid(2, use<&T::count>(), card("Appearance", use<&T::color>())), flow(card("Name Card", use<&T::name>()), card("Count Card", use<&T::count>())), tabs(tab("General", use<&T::name>(), use<&T::count>()), tab("Visual", use<&T::color>())))).submit("Save");
}
static Table_View table() {
using T = view_test::Config;
return table_view<T>(column<&T::name>("Name").search().fix(Table_Fixed::left), column<&T::count>("Count").sort(), column<&T::enabled>("Enabled").filter(), column<&T::color>("Color")).titled("Configs").default_sort("count", true).create_layout(grid(2, use<&T::name>(), use<&T::count>(), use<&T::enabled>(), use<&T::color>())).edit_layout(vertical(use<&T::name>(), horizontal(use<&T::count>(), use<&T::enabled>()), use<&T::color>()));
}
};
}
int main() {
using view_test::Config;
using view_test::Json;
const auto descriptor = adminive::to_descriptor_json<Json, Config>();
assert(descriptor.at("fields").at(0).at("presentation").at("label") == "Name");
assert(descriptor.at("fields").at(0).at("presentation").at("control") == "text");
assert(!descriptor.at("fields").at(0).contains("sortable"));
const auto form_view = adminive::describe_edit_view<Config>();
adminive::validate_form_view<Config>(form_view);
const auto form_json = adminive::to_view_json<Json, Config>(form_view);
assert(form_json.at("kind") == "form");
assert(form_json.at("body").at("kind") == "vertical");
assert(form_json.at("body").at("children").at(0).at("kind") == "group");
assert(form_json.at("body").at("children").at(1).at("kind") == "grid");
assert(form_json.at("body").at("children").at(2).at("kind") == "flow");
assert(form_json.at("body").at("children").at(3).at("kind") == "tabs");
Config config;
const auto form_amis = adminive::to_amis_form_schema<Json>(config, form_view, "/config/data");
assert(form_amis.at("type") == "form");
assert(form_amis.at("actions").at(1).at("label") == "Save");
assert(form_amis.at("body").at(0).at("type") == "fieldset");
assert(form_amis.at("body").at(1).at("type") == "grid");
assert(form_amis.at("body").at(2).at("type") == "flex");
assert(form_amis.at("body").at(3).at("type") == "tabs");
const auto table_view = adminive::describe_table_view<Config>();
adminive::validate_table_view<Config>(table_view);
const auto table_json = adminive::to_view_json<Json, Config>(table_view);
assert(table_json.at("kind") == "table");
assert(table_json.at("columns").at(0).at("searchable") == true);
assert(table_json.at("columns").at(0).at("fixed") == "left");
assert(table_json.at("columns").at(1).at("sortable") == true);
assert(table_json.at("columns").at(2).at("filterable") == true);
assert(table_json.at("default_order_by") == "count");
assert(table_json.at("default_order_dir") == "desc");
const auto composition = adminive::composition_view("config_page", adminive::compose::vertical(adminive::compose::heading("Config Page").accent("#123456"), adminive::compose::horizontal(adminive::compose::slot("form"), adminive::compose::slot("table"))));
const auto composition_json = adminive::to_view_json<Json>(composition);
assert(composition_json.at("kind") == "composition");
assert(composition_json.at("body").at("children").at(0).at("kind") == "heading");
const auto composition_amis = adminive::to_amis_composition_schema<Json>(composition, {{"form", form_amis}, {"table", Json{{"type", "tpl"}, {"tpl", "table"}}}});
assert(composition_amis.at("body").at(0).at("type") == "tpl");
assert(composition_amis.at("body").at(1).at("type") == "grid");
const auto table_amis = adminive::to_amis_table_schema<Json, Config>("/configs", table_view);
assert(table_amis.at("type") == "page");
assert(table_amis.at("body").at(0).at("columns").at(1).at("searchable") == true);
assert(table_amis.at("body").at(0).at("columns").at(2).at("sortable") == true);
assert(table_amis.at("body").at(0).at("columns").at(3).at("filterable") == true);
adminive::Collection_Service<Config, Json, adminive::No_Lock> collection("/configs", {Config{"Alpha", 3, true, "#111111"}, Config{"Beta", 7, false, "#222222"}, Config{"Alphabet", 5, true, "#333333"}});
adminive::Collection_Query query;
query.fields["name"] = "alpha";
auto response = collection.list_response(query);
assert(response.status == 200);
assert(response.body.at("data").at("total") == 2);
assert(response.body.at("data").at("items").at(0).at("count") == 5);
query = {};
query.fields["enabled"] = "true";
response = collection.list_response(query);
assert(response.status == 200);
assert(response.body.at("data").at("total") == 2);
query = {};
query.order_by = "count";
query.order_dir = "asc";
response = collection.list_response(query);
assert(response.status == 200);
assert(response.body.at("data").at("items").at(0).at("count") == 3);
query = {};
query.fields["color"] = "#111111";
response = collection.list_response(query);
assert(response.status == 400);
bool invalid_grid{};
try {
adminive::validate_form_view<Config>(adminive::edit_form<Config>(adminive::grid(0, adminive::use<&Config::name>())));
} catch(const std::invalid_argument&) {
invalid_grid = true;
}
assert(invalid_grid);
bool invalid_tabs{};
try {
adminive::validate_form_view<Config>(adminive::edit_form<Config>(adminive::tabs(adminive::use<&Config::name>())));
} catch(const std::invalid_argument&) {
invalid_tabs = true;
}
assert(invalid_tabs);
bool invalid_sort{};
try {
adminive::validate_table_view<Config>(adminive::table_view<Config>(adminive::column<&Config::name>()).default_sort("name"));
} catch(const std::invalid_argument&) {
invalid_sort = true;
}
assert(invalid_sort);
return 0;
}