Files
go-omni/response/encoder.go

98 lines
3.1 KiB
Go

package response
import (
"encoding/json"
"net/http"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
)
// Result is the transport-independent input to an Encoder. Err is available for
// application error mapping; default encoders never expose it or its causes.
type Result struct {
Status int
Code, Message, RequestID string
Data any
Err error
BusinessCode any
Failed bool
// MessageOverride is optional localized presentation. Message remains the
// canonical catalog text for validation. Custom protocols opt in by calling
// PublicMessage; fixed acknowledgments remain under their encoder's control.
MessageOverride *string
// PreserveStatus keeps an explicit protocol status through catalog mapping
// and HTTP output (for example, readiness must remain 503 when unhealthy).
PreserveStatus bool
}
// IsError preserves failure semantics independently of the HTTP status.
func (r Result) IsError() bool { return r.Failed || r.Err != nil || r.Status >= 400 }
func (r Result) PublicMessage() string {
if r.MessageOverride != nil {
return *r.MessageOverride
}
return r.Message
}
// Payload is a complete response. Encoders must be safe for concurrent use.
// Status and headers are intentionally part of the contract: callback protocols
// may acknowledge an error using a different HTTP status or media type.
type Payload struct {
Status int
Header http.Header
Body []byte
}
type Encoder interface{ Encode(Result) (Payload, error) }
type EncoderFunc func(Result) (Payload, error)
func (f EncoderFunc) Encode(r Result) (Payload, error) { return f(r) }
func JSON(status int, value any) (Payload, error) {
data, err := json.Marshal(value)
if err != nil {
return Payload{}, err
}
return Payload{Status: status, Header: http.Header{"Content-Type": {"application/json; charset=utf-8"}}, Body: append(data, '\n')}, nil
}
func Text(status int, value string) Payload {
return Payload{Status: status, Header: http.Header{"Content-Type": {"text/plain; charset=utf-8"}}, Body: []byte(value)}
}
// Default is immutable; applications can use different encoders in one process.
var defaultEncoder = MustNew(Config{})
func Default() Encoder { return defaultEncoder }
// RawJSON leaves successful data unwrapped. Errors use the selected error
// policy; callback protocols with custom acknowledgments should use EncoderFunc.
type rawJSONEncoder struct{ errors Encoder }
func (e *rawJSONEncoder) ErrorRegistry() *apierrors.Registry {
if p, ok := e.errors.(ErrorRegistryProvider); ok {
return p.ErrorRegistry()
}
return nil
}
func (e *rawJSONEncoder) Encode(r Result) (Payload, error) {
if r.IsError() {
return e.errors.Encode(r)
}
return JSON(r.Status, r.Data)
}
func RawJSON(errors Encoder) Encoder {
if errors == nil {
errors = Default()
}
return &rawJSONEncoder{errors: errors}
}
func (e *rawJSONEncoder) ValidateErrors(catalog apierrors.Catalog) error {
if checker, ok := e.errors.(interface{ ValidateErrors(apierrors.Catalog) error }); ok {
return checker.ValidateErrors(catalog)
}
return nil
}