Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
86 lines
2.9 KiB
Go
86 lines
2.9 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"reflect"
|
|
"sync"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
)
|
|
|
|
// ProblemDetail is one entry of a problem's errors list (a failed validation, say).
|
|
type ProblemDetail struct {
|
|
Message string `json:"message" doc:"Error message text."`
|
|
Location string `json:"location,omitempty" doc:"Where the error occurred, e.g. query.per_page or body.parents."`
|
|
Value any `json:"value,omitempty" doc:"The offending value."`
|
|
}
|
|
|
|
// Problem is the RFC 9457 problem details body of every error response. It
|
|
// replaces huma's ErrorModel only to make title and status required, which
|
|
// the generated TypeScript types rely on.
|
|
type Problem struct {
|
|
Type string `json:"type,omitempty" doc:"URI reference identifying the problem type; about:blank when absent."`
|
|
Title string `json:"title" example:"Not Found"`
|
|
Status int `json:"status" example:"404" doc:"HTTP status code."`
|
|
Detail string `json:"detail,omitempty" doc:"Explanation specific to this occurrence."`
|
|
Instance string `json:"instance,omitempty"`
|
|
Errors []ProblemDetail `json:"errors,omitempty" doc:"Individual errors, e.g. one per invalid parameter."`
|
|
}
|
|
|
|
// Error returns the detail, falling back to the title.
|
|
func (p *Problem) Error() string {
|
|
if p.Detail != "" {
|
|
return p.Detail
|
|
}
|
|
return p.Title
|
|
}
|
|
|
|
// GetStatus is the HTTP status of the response.
|
|
func (p *Problem) GetStatus() int { return p.Status }
|
|
|
|
// ContentType makes huma answer application/problem+json.
|
|
func (p *Problem) ContentType(ct string) string {
|
|
if ct == "application/json" {
|
|
return "application/problem+json"
|
|
}
|
|
return ct
|
|
}
|
|
|
|
// newProblem is huma.NewError for this API.
|
|
func newProblem(status int, msg string, errs ...error) huma.StatusError {
|
|
p := &Problem{Title: http.StatusText(status), Status: status, Detail: msg}
|
|
for _, e := range errs {
|
|
if d, ok := e.(huma.ErrorDetailer); ok {
|
|
x := d.ErrorDetail()
|
|
p.Errors = append(p.Errors, ProblemDetail{Message: x.Message, Location: x.Location, Value: x.Value})
|
|
continue
|
|
}
|
|
p.Errors = append(p.Errors, ProblemDetail{Message: e.Error()})
|
|
}
|
|
return p
|
|
}
|
|
|
|
var humaOnce sync.Once
|
|
|
|
// configureHuma sets huma's package-level switches once: arrays are never
|
|
// nullable, and errors are Problems.
|
|
func configureHuma() {
|
|
humaOnce.Do(func() {
|
|
huma.DefaultArrayNullable = false
|
|
huma.NewError = newProblem
|
|
})
|
|
}
|
|
|
|
// addDefaultProblem documents the RFC 9457 default response on every
|
|
// operation, the way the UI contract does, whatever status codes it also lists.
|
|
func addDefaultProblem(oapi *huma.OpenAPI, op *huma.Operation) {
|
|
if _, ok := op.Responses["default"]; ok {
|
|
return
|
|
}
|
|
schema := oapi.Components.Schemas.Schema(reflect.TypeFor[Problem](), true, "")
|
|
op.Responses["default"] = &huma.Response{
|
|
Description: "RFC 9457 problem details",
|
|
Content: map[string]*huma.MediaType{"application/problem+json": {Schema: schema}},
|
|
}
|
|
}
|