feat: add project-owned error catalogs and HTTP 200 failure contracts
This commit is contained in:
84
response/errors.go
Normal file
84
response/errors.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
)
|
||||
|
||||
// ErrorRegistryProvider attaches an immutable error catalog to a route policy.
|
||||
// Contexts and buffered clones use it before encoding or running middleware.
|
||||
type ErrorRegistryProvider interface{ ErrorRegistry() *apierrors.Registry }
|
||||
type errorEncoder struct {
|
||||
next Encoder
|
||||
registry *apierrors.Registry
|
||||
}
|
||||
|
||||
func (e *errorEncoder) ErrorRegistry() *apierrors.Registry { return e.registry }
|
||||
func (e *errorEncoder) Encode(r Result) (Payload, error) {
|
||||
if r.Err != nil {
|
||||
err := e.registry.Resolve(r.Err)
|
||||
r.Status, r.Code, r.Message, r.BusinessCode, r.Failed = err.Status, err.Code, err.Message, err.BusinessCode, true
|
||||
}
|
||||
return e.next.Encode(r)
|
||||
}
|
||||
|
||||
// WithErrors binds a catalog to an encoder without imposing a response shape.
|
||||
// Built-in templates are checked for incompatible code types and conflicting
|
||||
// legacy mappings. Custom encoders remain responsible for their wire contract.
|
||||
func WithErrors(catalog apierrors.Catalog, next Encoder) (Encoder, error) {
|
||||
registry, err := apierrors.NewRegistry(catalog)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if next == nil {
|
||||
next = Default()
|
||||
}
|
||||
if provider, ok := next.(ErrorRegistryProvider); ok && provider.ErrorRegistry() != nil {
|
||||
if len(catalog.Definitions) == 0 {
|
||||
return next, nil
|
||||
}
|
||||
if reflect.DeepEqual(registry.Catalog(), provider.ErrorRegistry().Catalog()) {
|
||||
return next, nil
|
||||
}
|
||||
return nil, fmt.Errorf("response: cannot stack different error catalogs; choose one policy per scope")
|
||||
}
|
||||
if checker, ok := next.(interface{ ValidateErrors(apierrors.Catalog) error }); ok {
|
||||
if err := checker.ValidateErrors(registry.Catalog()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &errorEncoder{next: next, registry: registry}, nil
|
||||
}
|
||||
func MustWithErrors(catalog apierrors.Catalog, next Encoder) Encoder {
|
||||
e, err := WithErrors(catalog, next)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return e
|
||||
}
|
||||
func (f *Formatter) ValidateErrors(catalog apierrors.Catalog) error {
|
||||
for key, d := range catalog.Definitions {
|
||||
if err := f.validateError(Result{Code: key, Message: d.Message, BusinessCode: d.Code, Failed: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (f *Formatter) validateError(r Result) error {
|
||||
if !r.IsError() || r.BusinessCode == nil {
|
||||
return nil
|
||||
}
|
||||
normalized, err := normalizeCode(r.BusinessCode, f.config.CodeType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("response: catalog %s code does not match codeType %s: %w", r.Code, f.config.CodeType, err)
|
||||
}
|
||||
if code, ok := f.config.Codes[r.Code]; ok && !reflect.DeepEqual(code, normalized) {
|
||||
return fmt.Errorf("response: conflicting catalog/code mapping for %s", r.Code)
|
||||
}
|
||||
if msg, ok := f.config.Messages[r.Code]; ok && msg != r.Message {
|
||||
return fmt.Errorf("response: conflicting catalog/message mapping for %s", r.Code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user