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>
155 lines
3.7 KiB
Go
155 lines
3.7 KiB
Go
package errcode
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// E 是携带稳定错误码的错误。可用 errors.Is / errors.As 识别,也可 JSON 跨进程传输。
|
|
// 跨进程反序列化后 Cause 只保留文本,不再持有原始 Unwrap 链。
|
|
type E struct {
|
|
Code Code `json:"code"`
|
|
Reason string `json:"reason,omitempty"`
|
|
Fields map[string]any `json:"fields,omitempty"`
|
|
cause error
|
|
}
|
|
|
|
// New 构造一个不包装原因的错误。kv 必须成对出现,键必须是非空字符串;
|
|
// 这是调用方编程错误,会 panic(与 log/slog 的字段约定相同)。
|
|
func New(code Code, reason string, kv ...any) *E {
|
|
return &E{Code: code, Reason: reason, Fields: fieldsFrom(kv)}
|
|
}
|
|
|
|
// Wrap 用错误码包装 err。err 为 nil 时返回 nil,以便直接 return。
|
|
func Wrap(err error, code Code, reason string, kv ...any) *E {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return &E{Code: code, Reason: reason, Fields: fieldsFrom(kv), cause: err}
|
|
}
|
|
|
|
// Error 实现 error。格式为 code: reason: cause,空段省略。
|
|
func (e *E) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
msg := string(e.Code)
|
|
if e.Reason != "" {
|
|
msg += ": " + e.Reason
|
|
}
|
|
if e.cause != nil {
|
|
msg += ": " + e.cause.Error()
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// Unwrap 返回被包装的原因。
|
|
func (e *E) Unwrap() error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.cause
|
|
}
|
|
|
|
// Is 在目标也是 *E 且错误码相同时匹配,供 errors.Is 使用。
|
|
func (e *E) Is(target error) bool {
|
|
t, ok := target.(*E)
|
|
if !ok || t == nil || e == nil {
|
|
return false
|
|
}
|
|
return e.Code != "" && e.Code == t.Code
|
|
}
|
|
|
|
// ErrorCode 返回规范码字符串。日志系统用鸭子接口读取,无需 import 本包。
|
|
func (e *E) ErrorCode() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
return string(e.Code)
|
|
}
|
|
|
|
// ErrorFields 返回字段副本(含 reason)。日志系统用鸭子接口读取。
|
|
func (e *E) ErrorFields() map[string]any {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
out := make(map[string]any, len(e.Fields)+1)
|
|
for k, v := range e.Fields {
|
|
out[k] = v
|
|
}
|
|
if e.Reason != "" {
|
|
out["reason"] = e.Reason
|
|
}
|
|
return out
|
|
}
|
|
|
|
// From 沿 Unwrap 链提取最外层 *E。不存在时返回 nil。
|
|
func From(err error) *E {
|
|
var e *E
|
|
if errors.As(err, &e) {
|
|
return e
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON 序列化码、原因、字段与原因文本。
|
|
func (e *E) MarshalJSON() ([]byte, error) {
|
|
if e == nil {
|
|
return []byte("null"), nil
|
|
}
|
|
wire := struct {
|
|
Code Code `json:"code"`
|
|
Reason string `json:"reason,omitempty"`
|
|
Fields map[string]any `json:"fields,omitempty"`
|
|
Cause string `json:"cause,omitempty"`
|
|
}{
|
|
Code: e.Code,
|
|
Reason: e.Reason,
|
|
Fields: e.Fields,
|
|
}
|
|
if e.cause != nil {
|
|
wire.Cause = e.cause.Error()
|
|
}
|
|
return json.Marshal(wire)
|
|
}
|
|
|
|
// UnmarshalJSON 恢复错误。原因文本包装为普通 error,Unwrap 不再指向原对象。
|
|
func (e *E) UnmarshalJSON(data []byte) error {
|
|
var wire struct {
|
|
Code Code `json:"code"`
|
|
Reason string `json:"reason,omitempty"`
|
|
Fields map[string]any `json:"fields,omitempty"`
|
|
Cause string `json:"cause,omitempty"`
|
|
}
|
|
if err := json.Unmarshal(data, &wire); err != nil {
|
|
return err
|
|
}
|
|
e.Code = wire.Code
|
|
e.Reason = wire.Reason
|
|
e.Fields = wire.Fields
|
|
e.cause = nil
|
|
if wire.Cause != "" {
|
|
e.cause = errors.New(wire.Cause)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fieldsFrom(kv []any) map[string]any {
|
|
if len(kv) == 0 {
|
|
return nil
|
|
}
|
|
if len(kv)%2 != 0 {
|
|
panic("errcode: odd number of key-value arguments")
|
|
}
|
|
fields := make(map[string]any, len(kv)/2)
|
|
for i := 0; i < len(kv); i += 2 {
|
|
key, ok := kv[i].(string)
|
|
if !ok || key == "" {
|
|
panic(fmt.Sprintf("errcode: field key must be a non-empty string, got %T", kv[i]))
|
|
}
|
|
fields[key] = kv[i+1]
|
|
}
|
|
return fields
|
|
}
|