feat(plugins): 插件「管理面板声明」库——声明模型 + 闭集校验 + catalog(design/09 §5.2/§5.2.1)
纯库、零接线:不碰 gateway/frontend/Docs/.env*(本片刻意不接线,避免与在途切片争用文件面)。 - pkg/plugins/sdk:面板声明类型(JSON tag 与 design/09 §5.2 示例逐字同形); 字段类型与 section 类型都是**闭集**;When 三态 + **严格 UnmarshalJSON**(未知条件键报错, 不静默丢) - internal/capability/paneldecl:闭集校验(47 条拒绝用例,断言的是**分类哨兵 + JSON 路径**, 不是错误字符串)· catalog(并发安全、返回拷贝、按 (group, order, 登记序号) 的**全序**稳定排序)· when 求值(上下文由 Resolver 注入,本包不读环境变量/文件)· 角色过滤(viewer 只留 status/log/table 且摘掉 rowActions,纯写面板整体隐藏;未知角色 fail-closed) - 拒绝粒度:插件 id 非法 ⇒ 整插件拒绝;单个面板非法 ⇒ 只拒该面板 + 聚合显式报错(理由见报告) 【我的验收】隔离 worktree(19 文件)build/vet/linux/darwin 全过、69 包全绿; 我亲手打红变异:把同组同序的回退比较 反成 ⇒ TestCatalog_StableOrder FAIL。 (记录一条观察:比较器已靠 seq 构成全序,故 sort.SliceStable 的稳定性是冗余保护 —— 我第一次用「SliceStable→Slice」打不红,正是因为全序与算法无关;这不是测试虚护。)
This commit is contained in:
1 parent
6e28b38b08
commit
c9144c1096
19 files changed
+3053
No files matched your search
@@ -0,0 +1,112 @@
|
||||
package paneldecl
|
||||
|
||||
import "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
||||
|
||||
// 本文件是目录的**拷贝语义**:存进来时拷一次、发出去时再拷一次。
|
||||
//
|
||||
// 为什么要两次:§5.2.1 的不变量是「`List` 返回的必须是拷贝」——调用方(gateway / 插件管理器)
|
||||
// 拿着结果做过滤或补字段时,绝不能改到目录内部状态;反向同理,插件管理器注册后继续改自己
|
||||
// 那份声明,也不能影响目录里的快照。声明是嵌套结构(sections → fields → 指针字段),
|
||||
// 浅拷贝会把内部切片暴露出去,所以这里逐层深拷。
|
||||
|
||||
// clonePanel 深拷一份面板声明。
|
||||
func clonePanel(p sdk.Panel) sdk.Panel {
|
||||
out := p
|
||||
out.When = cloneWhen(p.When)
|
||||
if p.Sections != nil {
|
||||
out.Sections = make([]sdk.Section, len(p.Sections))
|
||||
for i, s := range p.Sections {
|
||||
out.Sections[i] = cloneSection(s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// cloneSection 深拷一个区块(切片与指针字段都要换新)。
|
||||
func cloneSection(s sdk.Section) sdk.Section {
|
||||
out := s
|
||||
if s.Fields != nil {
|
||||
out.Fields = make([]sdk.Field, len(s.Fields))
|
||||
for i, f := range s.Fields {
|
||||
out.Fields[i] = cloneField(f)
|
||||
}
|
||||
}
|
||||
if s.Columns != nil {
|
||||
out.Columns = make([]sdk.Column, len(s.Columns))
|
||||
copy(out.Columns, s.Columns)
|
||||
}
|
||||
if s.RowActions != nil {
|
||||
out.RowActions = make([]sdk.RowAction, len(s.RowActions))
|
||||
copy(out.RowActions, s.RowActions)
|
||||
}
|
||||
if s.Submit != nil {
|
||||
submit := *s.Submit
|
||||
out.Submit = &submit
|
||||
}
|
||||
if s.Source != nil {
|
||||
source := *s.Source
|
||||
out.Source = &source
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// cloneField 深拷一个字段(Default 是 any,可能装着 map/slice)。
|
||||
func cloneField(f sdk.Field) sdk.Field {
|
||||
out := f
|
||||
out.Default = cloneValue(f.Default)
|
||||
if f.Min != nil {
|
||||
min := *f.Min
|
||||
out.Min = &min
|
||||
}
|
||||
if f.Max != nil {
|
||||
max := *f.Max
|
||||
out.Max = &max
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// cloneWhen 深拷 when(Enabled 是指针,共享指针等于共享可变状态)。
|
||||
func cloneWhen(w *sdk.When) *sdk.When {
|
||||
if w == nil {
|
||||
return nil
|
||||
}
|
||||
out := &sdk.When{}
|
||||
if w.Enabled != nil {
|
||||
enabled := *w.Enabled
|
||||
out.Enabled = &enabled
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// cloneValue 递归拷贝默认值里的容器。标量(字符串/数字/布尔)本身不可变,直接返回。
|
||||
//
|
||||
// 未覆盖的类型(声明由 Go 代码直接构造时可能塞进任意类型)按原值返回并在文档里写明:
|
||||
// 本包只承诺「结构声明不可被调用方改坏」,不承诺替调用方深拷它自己塞进来的自定义类型。
|
||||
func cloneValue(v any) any {
|
||||
switch val := v.(type) {
|
||||
case map[string]any:
|
||||
out := make(map[string]any, len(val))
|
||||
for k, item := range val {
|
||||
out[k] = cloneValue(item)
|
||||
}
|
||||
return out
|
||||
case map[string]string:
|
||||
out := make(map[string]string, len(val))
|
||||
for k, item := range val {
|
||||
out[k] = item
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, len(val))
|
||||
for i, item := range val {
|
||||
out[i] = cloneValue(item)
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
out := make([]string, len(val))
|
||||
copy(out, val)
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user