Files
nmon/internal/api/problem.go
jleung f85c3ef781
Some checks failed
ci / lint (push) Has been cancelled
ci / test (ubuntu-24.04-arm) (push) Has been cancelled
ci / test (ubuntu-latest) (push) Has been cancelled
ci / postgres (push) Has been cancelled
ci / openapi (push) Has been cancelled
M3: Go API per UI contract, embedded UI, openapi compat check, nmon-devtools, live gate + screenshots; M4 WP1 reachability and dependency suppression
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-22 20:34:14 +00:00

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}},
}
}