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>
277 lines
7.1 KiB
Go
277 lines
7.1 KiB
Go
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
|
||
}
|