纯库、零接线:不碰 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」打不红,正是因为全序与算法无关;这不是测试虚护。)
113 lines
3.0 KiB
Go
113 lines
3.0 KiB
Go
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
|
||
}
|
||
}
|