Files
Error-Code-System/errcode/registry.go
T
weichenandCursor bd53e9ba79
ci / vet-and-test (push) Successful in 3s
ci / docker-checkout-hostgateway (push) Successful in 3s
Add a shared error-code catalog so failures can be traced by stable codes.
Services can resolve legacy aliases and expose the catalog over CLI and HTTP instead of ad-hoc error strings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 13:43:12 +00:00

277 lines
7.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package errcode
import (
"bytes"
"encoding/json"
"fmt"
"regexp"
"slices"
"strings"
"sync"
)
// 别名用于承接历史码(snake_case 或 UPPER_SNAKE),不是第二套规范码。
var aliasPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]{1,63}$`)
// Registry 是线程安全的错误码登记表。
type Registry struct {
mu sync.RWMutex
byCode map[Code]Definition
aliases map[string]Code
}
// NewRegistry 返回空登记表。种子码由 domains 包登记,测试使用独立实例。
func NewRegistry() *Registry {
return &Registry{
byCode: map[Code]Definition{},
aliases: map[string]Code{},
}
}
// Default 是进程内默认登记表。导入 domains 包后会填入种子码。
var Default = NewRegistry()
// Register 登记一条定义。码重复、别名冲突或字段不合法时返回错误,不部分写入。
func (r *Registry) Register(def Definition) error {
if err := def.Validate(); err != nil {
return err
}
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.byCode[def.Code]; ok {
return fmt.Errorf("errcode: duplicate code %s", def.Code)
}
if owner, ok := r.aliases[string(def.Code)]; ok {
return fmt.Errorf("errcode: code %s collides with alias of %s", def.Code, owner)
}
staged := make([]string, 0, len(def.Aliases))
seen := map[string]struct{}{}
for _, alias := range def.Aliases {
if alias == string(def.Code) {
continue
}
if !aliasPattern.MatchString(alias) {
return fmt.Errorf("errcode: %s: invalid alias %q", def.Code, alias)
}
if _, dup := seen[alias]; dup {
return fmt.Errorf("errcode: %s: duplicate alias %q", def.Code, alias)
}
if _, ok := r.byCode[Code(alias)]; ok {
return fmt.Errorf("errcode: alias %q collides with registered code", alias)
}
if owner, ok := r.aliases[alias]; ok {
return fmt.Errorf("errcode: alias %q already points at %s", alias, owner)
}
seen[alias] = struct{}{}
staged = append(staged, alias)
}
slices.Sort(staged)
def.Aliases = staged
r.byCode[def.Code] = def
for _, alias := range staged {
r.aliases[alias] = def.Code
}
return nil
}
// MustRegister 登记失败时 panic,仅用于启动期种子数据。
func (r *Registry) MustRegister(def Definition) {
if err := r.Register(def); err != nil {
panic(err)
}
}
// Alias 为已登记的码追加一个历史别名。
func (r *Registry) Alias(alias string, code Code) error {
r.mu.Lock()
defer r.mu.Unlock()
def, ok := r.byCode[code]
if !ok {
return fmt.Errorf("errcode: alias target %s is not registered", code)
}
if alias == string(code) {
return nil
}
if !aliasPattern.MatchString(alias) {
return fmt.Errorf("errcode: invalid alias %q", alias)
}
if _, ok := r.byCode[Code(alias)]; ok {
return fmt.Errorf("errcode: alias %q collides with registered code", alias)
}
if owner, ok := r.aliases[alias]; ok && owner != code {
return fmt.Errorf("errcode: alias %q already points at %s", alias, owner)
}
r.aliases[alias] = code
if !slices.Contains(def.Aliases, alias) {
def.Aliases = append(def.Aliases, alias)
slices.Sort(def.Aliases)
r.byCode[code] = def
}
return nil
}
// Lookup 按规范码或别名查找。未登记返回 false,不回落。
func (r *Registry) Lookup(s string) (Definition, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
if def, ok := r.byCode[Code(s)]; ok {
return cloneDef(def), true
}
if code, ok := r.aliases[s]; ok {
def, ok := r.byCode[code]
if !ok {
return Definition{}, false
}
return cloneDef(def), true
}
return Definition{}, false
}
// Resolve 查找规范码或别名。未知、空串一律回落 sys.unknown_code(fail-closed)。
// 若回落码本身尚未登记,返回合成定义,避免空表把调用方打崩。
func (r *Registry) Resolve(s string) Definition {
if def, ok := r.Lookup(s); ok {
return def
}
if s != string(SysUnknownCode) {
if def, ok := r.Lookup(string(SysUnknownCode)); ok {
return def
}
}
return Definition{
Code: SysUnknownCode,
HTTP: 500,
Severity: SeverityError,
Retry: RetryNever,
SideEffects: SideEffectUnknown,
Summary: "未登记的错误码",
Hint: "按失败处理,并把该码补登记到域注册表",
}
}
// Known 报告字符串是否为已登记规范码或别名。
func (r *Registry) Known(s string) bool {
_, ok := r.Lookup(s)
return ok
}
// List 返回已登记定义。domain 非空时只返回该域。顺序按码字典序。
func (r *Registry) List(domain string) []Definition {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]Definition, 0, len(r.byCode))
for _, def := range r.byCode {
if domain != "" && def.Code.Domain() != domain {
continue
}
out = append(out, cloneDef(def))
}
slices.SortFunc(out, func(a, b Definition) int {
return strings.Compare(string(a.Code), string(b.Code))
})
return out
}
// Search 在码、摘要、处置建议与别名中做大小写不敏感子串匹配。
func (r *Registry) Search(q string) []Definition {
q = strings.ToLower(strings.TrimSpace(q))
if q == "" {
return r.List("")
}
out := []Definition{}
for _, def := range r.List("") {
if definitionMatches(def, q) {
out = append(out, def)
}
}
return out
}
func definitionMatches(def Definition, q string) bool {
if strings.Contains(strings.ToLower(string(def.Code)), q) {
return true
}
if strings.Contains(strings.ToLower(def.Summary), q) {
return true
}
if strings.Contains(strings.ToLower(def.Hint), q) {
return true
}
for _, alias := range def.Aliases {
if strings.Contains(strings.ToLower(alias), q) {
return true
}
}
return false
}
// ExportJSON 导出全部定义(UTF-8、缩进),供跨语言消费与目录对照。
func (r *Registry) ExportJSON() ([]byte, error) {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(r.List("")); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// LoadJSON 从 JSON 数组或 {"codes":[...]} 登记定义。任一失败则回滚本批。
func (r *Registry) LoadJSON(data []byte) error {
defs, err := decodeDefinitions(data)
if err != nil {
return err
}
added := make([]Code, 0, len(defs))
for _, def := range defs {
if err := r.Register(def); err != nil {
r.unregister(added)
return err
}
added = append(added, def.Code)
}
return nil
}
func decodeDefinitions(data []byte) ([]Definition, error) {
data = bytes.TrimSpace(data)
if len(data) == 0 {
return nil, fmt.Errorf("errcode: empty catalog json")
}
if data[0] == '[' {
var defs []Definition
if err := json.Unmarshal(data, &defs); err != nil {
return nil, fmt.Errorf("errcode: catalog json: %w", err)
}
return defs, nil
}
var wrap struct {
Codes []Definition `json:"codes"`
}
if err := json.Unmarshal(data, &wrap); err != nil {
return nil, fmt.Errorf("errcode: catalog json: %w", err)
}
return wrap.Codes, nil
}
func (r *Registry) unregister(codes []Code) {
r.mu.Lock()
defer r.mu.Unlock()
drop := map[Code]struct{}{}
for _, code := range codes {
drop[code] = struct{}{}
delete(r.byCode, code)
}
for alias, code := range r.aliases {
if _, ok := drop[code]; ok {
delete(r.aliases, alias)
}
}
}
func cloneDef(def Definition) Definition {
def.Aliases = slices.Clone(def.Aliases)
return def
}