纯库、零接线:不碰 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」打不红,正是因为全序与算法无关;这不是测试虚护。)
79 lines
3.2 KiB
Go
79 lines
3.2 KiB
Go
package sdk
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"sort"
|
||
)
|
||
|
||
// 本文件定义面板入口的**条件显示**声明 `when`(design/09 §5.2.1 nav.item:
|
||
// 「`when` 不满足 ⇒ 条目不出现」,不是灰掉)。
|
||
|
||
const (
|
||
// whenKeyEnabled 是本期唯一支持的条件键:§5.2 的 `"when": { "enabled": true }`
|
||
// 是设计里出现过的唯一条件形态,§5.6 的验收判据也只有「未启用的插件 ⇒ 入口不出现」。
|
||
// 刻意不留第二个键 —— 闭集外的条件属于「先改设计再加」的范畴。
|
||
whenKeyEnabled = "enabled"
|
||
)
|
||
|
||
var (
|
||
// ErrInvalidWhen 报告 when 不是合法的条件对象(非对象、null、或 enabled 不是布尔)。
|
||
ErrInvalidWhen = errors.New("sdk: invalid when clause")
|
||
// ErrUnknownWhenKey 报告 when 里出现了闭集外的条件键。
|
||
//
|
||
// 刻意**报错而不是忽略**:静默忽略会让插件作者以为条件生效了,而入口照常出现 ——
|
||
// §5.2.1 的口径是「绝不静默丢弃」。这条只能在解码期拦住(结构体解码本身会丢弃未知
|
||
// 键),所以这里自己实现 UnmarshalJSON。
|
||
ErrUnknownWhenKey = errors.New("sdk: unknown when key")
|
||
)
|
||
|
||
// When 是面板入口的条件显示声明。零值语义是「没有条件」,见 paneldecl.Eval。
|
||
type When struct {
|
||
// Enabled 有三个状态,用指针而不是 bool 表达:
|
||
// - nil:没声明这个条件(含整份 when 缺省的 nil 指针)⇒ 不参与判定;
|
||
// - &true:要求插件当前已启用(§5.6.1「未启用 ⇒ 入口不出现」);
|
||
// - &false:要求插件当前**未**启用 —— 条件是「要求值相等」,写 false 不等于没写。
|
||
Enabled *bool `json:"enabled,omitempty"`
|
||
}
|
||
|
||
// UnmarshalJSON 严格解码 when:只接受对象,只接受闭集内的键,enabled 必须是布尔。
|
||
//
|
||
// 为什么不用默认的 struct 解码:默认解码会**静默丢弃**未知键(`{"enabled":true,"x":1}`
|
||
// 会解码成一个看似正常的 When),插件作者会以为自己写了条件却没有任何效果。这里把
|
||
// 未知键变成显式拒绝,由插件管理器按 §5.2.1「该面板不注册 + 显式报错」处理。
|
||
func (w *When) UnmarshalJSON(raw []byte) error {
|
||
trimmed := bytes.TrimSpace(raw)
|
||
if bytes.Equal(trimmed, []byte("null")) {
|
||
return fmt.Errorf("%w: null is not a condition set", ErrInvalidWhen)
|
||
}
|
||
var fields map[string]json.RawMessage
|
||
if err := json.Unmarshal(trimmed, &fields); err != nil {
|
||
return fmt.Errorf("%w: %w", ErrInvalidWhen, err)
|
||
}
|
||
// 按键名排序后再判定:多个未知键时错误信息必须确定(否则同一个坏声明在不同进程里
|
||
// 报不同的键,排障与测试都会被搞乱)。
|
||
keys := make([]string, 0, len(fields))
|
||
for k := range fields {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
for _, k := range keys {
|
||
if k != whenKeyEnabled {
|
||
return fmt.Errorf("%w: %q", ErrUnknownWhenKey, k)
|
||
}
|
||
}
|
||
rawEnabled, ok := fields[whenKeyEnabled]
|
||
if !ok {
|
||
w.Enabled = nil
|
||
return nil
|
||
}
|
||
var enabled bool
|
||
if err := json.Unmarshal(rawEnabled, &enabled); err != nil {
|
||
return fmt.Errorf("%w: enabled must be a boolean: %w", ErrInvalidWhen, err)
|
||
}
|
||
w.Enabled = &enabled
|
||
return nil
|
||
}
|