package paneldecl import ( "errors" "fmt" "strings" ) // 本文件是校验错误的**可分类**表示。 // // 为什么不用一句 `invalid`:§5.2.1 要求「非法声明 ⇒ 该面板不注册 + 显式报错」,而错误 // 要同时喂给三种消费者 —— 插件管理器(决定拒哪个面板)、启动期日志(排障)、以及未来 // 的安装响应(把错误码与文案分离,照 Home Assistant 的「错误码与文案分离」,前端可本地化)。 // 所以错误必须是结构化的:哨兵做分类,Path 做定位,Detail 做人读说明。 // 校验失败分类哨兵。调用方用 errors.Is 判类,不靠字符串匹配。 var ( // ErrInvalidPluginID 报告插件 id 不合法(无法作为目录命名空间 / 路由段)。 ErrInvalidPluginID = errors.New("paneldecl: invalid plugin id") // ErrInvalidPanelID 报告面板 id 不合法。 ErrInvalidPanelID = errors.New("paneldecl: invalid panel id") // ErrInvalidSectionID 报告区块 id 不合法。 ErrInvalidSectionID = errors.New("paneldecl: invalid section id") // ErrInvalidRowActionID 报告行内动作 id 不合法。 ErrInvalidRowActionID = errors.New("paneldecl: invalid row action id") // ErrInvalidFieldKey 报告字段 key 不合法(字段 key 是配置键,字符集比 id 宽,见 shape.go)。 ErrInvalidFieldKey = errors.New("paneldecl: invalid field key") // ErrMissingField 报告必填项缺失(id / title / section 的 type / field 的 key、type 等)。 ErrMissingField = errors.New("paneldecl: required field missing") // ErrInvalidText 报告文案非法:空白、含控制字符、或超出长度上限。 ErrInvalidText = errors.New("paneldecl: invalid text") // ErrUnknownGroup 报告分组不在闭集内(本期只有 plugins)。 ErrUnknownGroup = errors.New("paneldecl: unknown panel group") // ErrUnknownSectionType 报告 section 类型不在闭集内。 ErrUnknownSectionType = errors.New("paneldecl: unknown section type") // ErrUnknownFieldType 报告字段类型不在闭集内。 ErrUnknownFieldType = errors.New("paneldecl: unknown field type") // ErrSectionShape 报告区块形状错误:该类型不该出现的键出现了(如 table 上写 fields)。 ErrSectionShape = errors.New("paneldecl: invalid section shape") // ErrReadOnlySection 报告只读区块上出现了写面(status/log 上的 submit / rowActions)。 ErrReadOnlySection = errors.New("paneldecl: read-only section declares a write surface") // ErrInvalidOptionsFrom 报告 optionsFrom 非法:只允许 `plugin:*`,且只允许出现在下拉字段上。 ErrInvalidOptionsFrom = errors.New("paneldecl: invalid optionsFrom") // ErrInvalidAction 报告动作名非法(允许点分名,禁止 / : 等 URL 成分)。 ErrInvalidAction = errors.New("paneldecl: invalid action name") // ErrInvalidOrder 报告 order 为负。 ErrInvalidOrder = errors.New("paneldecl: invalid order") // ErrInvalidBounds 报告 min/max 非法(配在非 number 字段上、NaN、或 min > max)。 ErrInvalidBounds = errors.New("paneldecl: invalid numeric bounds") // ErrInvalidDefault 报告默认值与字段类型不匹配。 ErrInvalidDefault = errors.New("paneldecl: default value does not match the field type") // ErrSecretDefault 报告 secret 字段声明了默认值(§5.4.2 读侧无明文)。 ErrSecretDefault = errors.New("paneldecl: secret field must not declare a default") // ErrInvalidLimit 报告 log 区块的 limit 越界。 ErrInvalidLimit = errors.New("paneldecl: invalid log limit") // ErrInvalidIcon 报告图标名非法。 ErrInvalidIcon = errors.New("paneldecl: invalid icon name") // ErrDuplicateID 报告同一作用域内 id / key 重复。 ErrDuplicateID = errors.New("paneldecl: duplicate id") // ErrUnknownRole 报告角色不在 owner/admin/viewer 之内(查询目录时使用)。 ErrUnknownRole = errors.New("paneldecl: unknown role") // ErrNilResolver 报告构造目录时没有注入 when 求值上下文来源。 ErrNilResolver = errors.New("paneldecl: nil panel context resolver") // ErrNilCatalog 报告在 nil *Catalog 上调用了 Register。 ErrNilCatalog = errors.New("paneldecl: nil catalog") ) // maxReportedViolations 限制聚合错误在 Error() 里铺开的条数:一个坏清单可能有几百条 // 违规,全铺进一行日志会把有用的信息挤掉。结构化的完整列表仍在 Violations 里,不丢。 const maxReportedViolations = 5 // Violation 是一条**可分类**的校验失败。 type Violation struct { // Kind 是分类哨兵(本文件的 Err*),errors.Is 沿着它判定。 Kind error // Path 是出问题的位置,索引式 JSON 路径,如 // `adminPanels[0].sections[1].fields[2].type` —— 索引而不是 id,因为出错的可能正是 id。 Path string // Detail 是人读说明,只讲这一条问题本身,不含他人信息。 Detail string } // Error 实现 error。格式固定为 `<分类>: <路径>: <说明>`,便于日志切片与测试断言。 func (v Violation) Error() string { return fmt.Sprintf("%s: %s: %s", v.Kind, v.Path, v.Detail) } // Unwrap 让 errors.Is(err, ErrXxx) 能穿过这条违规。 func (v Violation) Unwrap() error { return v.Kind } // Errors 是逐条违规的聚合。它的存在是为了「一次把所有问题讲清楚」—— 逐个报错会让插件 // 作者改一轮、跑一轮。 type Errors struct { // Violations 按发现顺序排列(声明顺序,因此可复现)。 Violations []Violation } // Error 实现 error,最多铺开 maxReportedViolations 条,其余只报条数。 func (e *Errors) Error() string { if e == nil || len(e.Violations) == 0 { return "paneldecl: no violations" } parts := make([]string, 0, maxReportedViolations) for i, v := range e.Violations { if i == maxReportedViolations { parts = append(parts, fmt.Sprintf("...(+%d more)", len(e.Violations)-maxReportedViolations)) break } parts = append(parts, v.Error()) } return fmt.Sprintf("paneldecl: %d violation(s): %s", len(e.Violations), strings.Join(parts, "; ")) } // Unwrap 返回每一条违规,让 errors.Is / errors.As 能穿透聚合看分类。 func (e *Errors) Unwrap() []error { if e == nil { return nil } out := make([]error, 0, len(e.Violations)) for _, v := range e.Violations { out = append(out, v) } return out } // violation 构造一条违规。 func violation(kind error, path, detail string) Violation { return Violation{Kind: kind, Path: path, Detail: detail} } // violationsToError 把逐条违规收成一个 error:空切片返回 nil(Go 的 nil-error 约定, // 免得调用方写 `if len(vs) > 0 { return &Errors{vs} }`)。 func violationsToError(vs []Violation) error { if len(vs) == 0 { return nil } return &Errors{Violations: vs} }