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 }