From 22f2ea5fa838a930553e09bae42f065e30f6ea69 Mon Sep 17 00:00:00 2001 From: "Gustavo \"Guz\" L de Mello" Date: Tue, 29 Jul 2025 19:18:27 -0300 Subject: [PATCH] feat(smalltrip,problems): Problem interface --- smalltrip/problem/400.go | 92 ++++++++++++++--------------- smalltrip/problem/500.go | 36 ++++++------ smalltrip/problem/problem.go | 111 +++++++++++++++++++++++------------ 3 files changed, 137 insertions(+), 102 deletions(-) diff --git a/smalltrip/problem/400.go b/smalltrip/problem/400.go index 105dc9d..9f660cb 100644 --- a/smalltrip/problem/400.go +++ b/smalltrip/problem/400.go @@ -11,11 +11,11 @@ func NewBadRequest(detail string, opts ...Option) BadRequest { return BadRequest{NewDetailed(http.StatusBadRequest, detail, opts...)} } -type BadRequest struct{ Problem } +type BadRequest struct{ RegisteredProblem } func NewUnauthorized(scheme AuthScheme, opts ...Option) Unauthorized { return Unauthorized{ - Problem: NewDetailed( + RegisteredProblem: NewDetailed( http.StatusUnauthorized, fmt.Sprintf("You must authenticate using the %q scheme", scheme.Title), opts..., @@ -25,7 +25,7 @@ func NewUnauthorized(scheme AuthScheme, opts ...Option) Unauthorized { } type Unauthorized struct { - Problem + RegisteredProblem Authentication AuthScheme `json:"authentication,omitempty" xml:"authentication,omitempty"` } @@ -33,30 +33,30 @@ func (p Unauthorized) ServeHTTP(w http.ResponseWriter, r *http.Request) { if p.Authentication.Title != "" { w.Header().Set("WWW-Authenticate", p.Authentication.Title) } - p.handler(p).ServeHTTP(w, r) + p.Handler(p).ServeHTTP(w, r) } func NewPaymentRequired(opts ...Option) PaymentRequired { return PaymentRequired{NewStatus(http.StatusPaymentRequired, opts...)} } -type PaymentRequired struct{ Problem } +type PaymentRequired struct{ RegisteredProblem } func NewForbidden(opts ...Option) Forbidden { return Forbidden{NewStatus(http.StatusForbidden, opts...)} } -type Forbidden struct{ Problem } +type Forbidden struct{ RegisteredProblem } func NewNotFound(opts ...Option) NotFound { return NotFound{NewStatus(http.StatusNotFound, opts...)} } -type NotFound struct{ Problem } +type NotFound struct{ RegisteredProblem } func NewMethodNotAllowed[T string | []string](allow T, opts ...Option) MethodNotAllowed { p := MethodNotAllowed{ - Problem: NewStatus(http.StatusMethodNotAllowed, opts...), + RegisteredProblem: NewStatus(http.StatusMethodNotAllowed, opts...), } if as, ok := any(allow).([]string); ok { p.Allowed = as @@ -67,7 +67,7 @@ func NewMethodNotAllowed[T string | []string](allow T, opts ...Option) MethodNot } type MethodNotAllowed struct { - Problem + RegisteredProblem Allowed []string `json:"allowed,omitempty" xml:"allowed,omitempty"` } @@ -75,12 +75,12 @@ func (p MethodNotAllowed) ServeHTTP(w http.ResponseWriter, r *http.Request) { if len(p.Allowed) > 0 { w.Header().Set("Allow", strings.Join(p.Allowed, ", ")) } - p.handler(p).ServeHTTP(w, r) + p.Handler(p).ServeHTTP(w, r) } func NewNotAcceptable[T string | []string](header NegotiationHeader, allow T, opts ...Option) NotAcceptable { p := NotAcceptable{ - Problem: NewDetailed(http.StatusMethodNotAllowed, fmt.Sprintf( + RegisteredProblem: NewDetailed(http.StatusMethodNotAllowed, fmt.Sprintf( "Cannot provide a response matching the list of acceptable values defined by %q header", header), opts..., ), @@ -94,12 +94,12 @@ func NewNotAcceptable[T string | []string](header NegotiationHeader, allow T, op } type NotAcceptable struct { - Problem + RegisteredProblem Allowed []string `json:"allowed,omitempty" xml:"allowed,omitempty"` } func (p NotAcceptable) ServeHTTP(w http.ResponseWriter, r *http.Request) { - p.handler(p).ServeHTTP(w, r) + p.Handler(p).ServeHTTP(w, r) } const ( @@ -112,7 +112,7 @@ type NegotiationHeader string func NewProxyAuthRequired(scheme AuthScheme, opts ...Option) ProxyAuthRequired { return ProxyAuthRequired{ - Problem: NewDetailed( + RegisteredProblem: NewDetailed( http.StatusProxyAuthRequired, fmt.Sprintf("You must authenticate on the proxy using the %q scheme", scheme.Title), opts..., @@ -122,7 +122,7 @@ func NewProxyAuthRequired(scheme AuthScheme, opts ...Option) ProxyAuthRequired { } type ProxyAuthRequired struct { - Problem + RegisteredProblem Authentication AuthScheme `json:"authentication,omitempty" xml:"authentication,omitempty"` } @@ -130,7 +130,7 @@ func (p ProxyAuthRequired) ServeHTTP(w http.ResponseWriter, r *http.Request) { if p.Authentication.Title != "" { w.Header().Set("Proxy-Authenticate", p.Authentication.Title) } - p.handler(p).ServeHTTP(w, r) + p.Handler(p).ServeHTTP(w, r) } var ( @@ -154,130 +154,130 @@ func NewRequestTimeout(opts ...Option) RequestTimeout { return RequestTimeout{NewStatus(http.StatusRequestTimeout, opts...)} } -type RequestTimeout struct{ Problem } +type RequestTimeout struct{ RegisteredProblem } func (p RequestTimeout) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Connection", "close") - p.handler(p).ServeHTTP(w, r) + p.Handler(p).ServeHTTP(w, r) } func NewConflict(opts ...Option) Conflict { return Conflict{NewStatus(http.StatusConflict, opts...)} } -type Conflict struct{ Problem } +type Conflict struct{ RegisteredProblem } func NewGone(opts ...Option) Gone { return Gone{NewStatus(http.StatusGone, opts...)} } -type Gone struct{ Problem } +type Gone struct{ RegisteredProblem } func NewLengthRequired(opts ...Option) LengthRequired { return LengthRequired{NewStatus(http.StatusLengthRequired, opts...)} } -type LengthRequired struct{ Problem } +type LengthRequired struct{ RegisteredProblem } func NewPreconditionFailed(opts ...Option) PreconditionFailed { return PreconditionFailed{NewStatus(http.StatusPreconditionFailed, opts...)} } -type PreconditionFailed struct{ Problem } +type PreconditionFailed struct{ RegisteredProblem } func NewContentTooLarge(opts ...Option) ContentTooLarge { return ContentTooLarge{NewStatus(http.StatusRequestEntityTooLarge, opts...)} } -type ContentTooLarge struct{ Problem } +type ContentTooLarge struct{ RegisteredProblem } func NewURITooLong(opts ...Option) URITooLong { return URITooLong{NewStatus(http.StatusRequestURITooLong, opts...)} } -type URITooLong struct{ Problem } +type URITooLong struct{ RegisteredProblem } func NewUnsupportedMediaType(opts ...Option) UnsupportedMediaType { return UnsupportedMediaType{NewStatus(http.StatusUnsupportedMediaType, opts...)} } -type UnsupportedMediaType struct{ Problem } +type UnsupportedMediaType struct{ RegisteredProblem } func NewRangeNotSatisfiable(unit string, contentRange int, opts ...Option) RangeNotSatisfiable { return RangeNotSatisfiable{ - Problem: NewStatus(http.StatusGone, opts...), - Range: fmt.Sprintf("%s */%d", unit, contentRange), + RegisteredProblem: NewStatus(http.StatusGone, opts...), + Range: fmt.Sprintf("%s */%d", unit, contentRange), } } type RangeNotSatisfiable struct { - Problem + RegisteredProblem Range string `json:"range" xml:"range"` } func (p RangeNotSatisfiable) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Range", p.Range) - p.handler(p).ServeHTTP(w, r) + p.Handler(p).ServeHTTP(w, r) } func NewExpectationFailed(opts ...Option) ExpectationFailed { return ExpectationFailed{NewStatus(http.StatusExpectationFailed, opts...)} } -type ExpectationFailed struct{ Problem } +type ExpectationFailed struct{ RegisteredProblem } func NewTeapot(opts ...Option) Teapot { return Teapot{NewStatus(http.StatusTeapot, opts...)} } -type Teapot struct{ Problem } +type Teapot struct{ RegisteredProblem } func NewMisdirectedRequest(opts ...Option) MisdirectedRequest { return MisdirectedRequest{NewStatus(http.StatusMisdirectedRequest, opts...)} } -type MisdirectedRequest struct{ Problem } +type MisdirectedRequest struct{ RegisteredProblem } func NewUnprocessableContent(opts ...Option) UnprocessableContent { return UnprocessableContent{NewStatus(http.StatusUnprocessableEntity, opts...)} } -type UnprocessableContent struct{ Problem } +type UnprocessableContent struct{ RegisteredProblem } // TODO?: Should the response of this be different and follow WebDAV's XML format? func NewLocked(opts ...Option) Locked { return Locked{NewStatus(http.StatusLocked, opts...)} } -type Locked struct{ Problem } +type Locked struct{ RegisteredProblem } func NewFailedDependency(opts ...Option) FailedDependency { return FailedDependency{NewStatus(http.StatusFailedDependency, opts...)} } -type FailedDependency struct{ Problem } +type FailedDependency struct{ RegisteredProblem } func NewTooEarly(opts ...Option) TooEarly { return TooEarly{NewStatus(http.StatusTooEarly, opts...)} } -type TooEarly struct{ Problem } +type TooEarly struct{ RegisteredProblem } func NewUpgradeRequired(protocol Protocol, opts ...Option) UpgradeRequired { return UpgradeRequired{ - Problem: NewStatus(http.StatusUpgradeRequired, opts...), - Protocol: protocol, + RegisteredProblem: NewStatus(http.StatusUpgradeRequired, opts...), + Protocol: protocol, } } type UpgradeRequired struct { - Problem + RegisteredProblem Protocol Protocol `json:"protocol" xml:"protocol"` } func (p UpgradeRequired) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Upgrade", string(p.Protocol)) - p.handler(p).ServeHTTP(w, r) + p.Handler(p).ServeHTTP(w, r) } const ( @@ -292,31 +292,31 @@ func NewPreconditionRequired(opts ...Option) PreconditionRequired { return PreconditionRequired{NewStatus(http.StatusPreconditionRequired, opts...)} } -type PreconditionRequired struct{ Problem } +type PreconditionRequired struct{ RegisteredProblem } func NewTooManyRequests[T time.Time | time.Duration](retryAfter T, opts ...Option) TooManyRequests[T] { p := NewStatus(http.StatusTooManyRequests, opts...) - return TooManyRequests[T]{Problem: p, RetryAfter: RetryAfter[T]{time: retryAfter}} + return TooManyRequests[T]{RegisteredProblem: p, RetryAfter: RetryAfter[T]{time: retryAfter}} } type TooManyRequests[T time.Time | time.Duration] struct { - Problem + RegisteredProblem RetryAfter RetryAfter[T] `json:"retryAfter" xml:"retry-after"` } func (p TooManyRequests[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Retry-After", p.RetryAfter.String()) - p.handler(p).ServeHTTP(w, r) + p.Handler(p).ServeHTTP(w, r) } func NewRequestHeaderFieldsTooLarge(opts ...Option) RequestHeaderFieldsTooLarge { return RequestHeaderFieldsTooLarge{NewStatus(http.StatusRequestHeaderFieldsTooLarge, opts...)} } -type RequestHeaderFieldsTooLarge struct{ Problem } +type RequestHeaderFieldsTooLarge struct{ RegisteredProblem } func NewUnavailableForLegalReasons(opts ...Option) UnavailableForLegalReasons { return UnavailableForLegalReasons{NewStatus(http.StatusUnavailableForLegalReasons, opts...)} } -type UnavailableForLegalReasons struct{ Problem } +type UnavailableForLegalReasons struct{ RegisteredProblem } diff --git a/smalltrip/problem/500.go b/smalltrip/problem/500.go index 6d964af..df6bd1b 100644 --- a/smalltrip/problem/500.go +++ b/smalltrip/problem/500.go @@ -9,14 +9,14 @@ import ( func NewInternalError(err error, opts ...Option) InternalServerError { return InternalServerError{ - Problem: NewDetailed(http.StatusInternalServerError, err.Error(), opts...), - Errors: newErrorTree(err).Errors, - error: err, + RegisteredProblem: NewDetailed(http.StatusInternalServerError, err.Error(), opts...), + Errors: newErrorTree(err).Errors, + error: err, } } type InternalServerError struct { - Problem + RegisteredProblem Errors []ErrorTree `json:"errors" xml:"errors"` error error `json:"-" xml:"-"` @@ -25,7 +25,7 @@ type InternalServerError struct { var _ error = InternalServerError{} func (p InternalServerError) ServeHTTP(w http.ResponseWriter, r *http.Request) { - p.handler(p).ServeHTTP(w, r) + p.Handler(p).ServeHTTP(w, r) } func (p InternalServerError) Error() string { @@ -60,32 +60,32 @@ func (i ErrorTree) Error() string { func NewNotImplemented[T time.Time | time.Duration](retryAfter T, opts ...Option) NotImplemented[T] { p := NewStatus(http.StatusNotImplemented, opts...) - return NotImplemented[T]{Problem: p, RetryAfter: RetryAfter[T]{time: retryAfter}} + return NotImplemented[T]{RegisteredProblem: p, RetryAfter: RetryAfter[T]{time: retryAfter}} } type NotImplemented[T time.Time | time.Duration] struct { - Problem + RegisteredProblem RetryAfter RetryAfter[T] `json:"retryAfter" xml:"retry-after"` } func (p NotImplemented[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Retry-After", p.RetryAfter.String()) - p.handler(p).ServeHTTP(w, r) + p.Handler(p).ServeHTTP(w, r) } func NewBadGateway(opts ...Option) BadGateway { return BadGateway{NewStatus(http.StatusBadGateway, opts...)} } -type BadGateway struct{ Problem } +type BadGateway struct{ RegisteredProblem } func NewServiceUnavailable[T time.Time | time.Duration](retryAfter T, opts ...Option) ServiceUnavailable[T] { p := NewStatus(http.StatusNotImplemented, opts...) - return ServiceUnavailable[T]{Problem: p, RetryAfter: RetryAfter[T]{time: retryAfter}} + return ServiceUnavailable[T]{RegisteredProblem: p, RetryAfter: RetryAfter[T]{time: retryAfter}} } type ServiceUnavailable[T time.Time | time.Duration] struct { - Problem + RegisteredProblem RetryAfter RetryAfter[T] `json:"retryAfter" xml:"retry-after"` } @@ -98,40 +98,40 @@ func NewGatewayTimeout(opts ...Option) GatewayTimeout { return GatewayTimeout{NewStatus(http.StatusGatewayTimeout, opts...)} } -type GatewayTimeout struct{ Problem } +type GatewayTimeout struct{ RegisteredProblem } func NewHTTPVersionNotSupported(opts ...Option) HTTPVersionNotSupported { return HTTPVersionNotSupported{NewStatus(http.StatusHTTPVersionNotSupported, opts...)} } -type HTTPVersionNotSupported struct{ Problem } +type HTTPVersionNotSupported struct{ RegisteredProblem } func NewVariantAlsoNegotiates(opts ...Option) VariantAlsoNegotiates { return VariantAlsoNegotiates{NewStatus(http.StatusVariantAlsoNegotiates, opts...)} } -type VariantAlsoNegotiates struct{ Problem } +type VariantAlsoNegotiates struct{ RegisteredProblem } func NewInsufficientStorage(opts ...Option) InsufficientStorage { return InsufficientStorage{NewStatus(http.StatusInsufficientStorage, opts...)} } -type InsufficientStorage struct{ Problem } +type InsufficientStorage struct{ RegisteredProblem } func NewLoopDetected(opts ...Option) LoopDetected { return LoopDetected{NewStatus(http.StatusLoopDetected, opts...)} } -type LoopDetected struct{ Problem } +type LoopDetected struct{ RegisteredProblem } func NewNotExtended(opts ...Option) NotExtended { return NotExtended{NewStatus(http.StatusNotExtended, opts...)} } -type NotExtended struct{ Problem } +type NotExtended struct{ RegisteredProblem } func NewNetworkAuthenticationRequired(opts ...Option) NetworkAuthenticationRequired { return NetworkAuthenticationRequired{NewStatus(http.StatusNetworkAuthenticationRequired, opts...)} } -type NetworkAuthenticationRequired struct{ Problem } +type NetworkAuthenticationRequired struct{ RegisteredProblem } diff --git a/smalltrip/problem/problem.go b/smalltrip/problem/problem.go index 881e86c..e8f078d 100644 --- a/smalltrip/problem/problem.go +++ b/smalltrip/problem/problem.go @@ -7,91 +7,126 @@ import ( "slices" ) -type Problem struct { - Type string `json:"type,omitempty" xml:"type,omitempty"` - Title string `json:"title,omitempty" xml:"title,omitempty"` - Status int `json:"status,omitempty" xml:"status,omitempty"` - Detail string `json:"detail,omitempty" xml:"detail,omitempty"` - Instance string `json:"instance,omitempty" xml:"instance,omitempty"` +type Problem interface { + Type() string + Title() string + Status() int + Detail() string + Instance() string + + Handler(self Problem) http.Handler + + http.Handler +} + +type RegisteredProblem struct { + TypeURI string `json:"type,omitempty" xml:"type,omitempty"` + TypeTitle string `json:"title,omitempty" xml:"title,omitempty"` + StatusCode int `json:"status,omitempty" xml:"status,omitempty"` + DetailMessage string `json:"detail,omitempty" xml:"detail,omitempty"` + InstanceURI string `json:"instance,omitempty" xml:"instance,omitempty"` XMLName xml.Name `json:"-" xml:"problem"` - handler func(any) http.Handler `json:"-" xml:"-"` + handler Handler `json:"-" xml:"-"` } -func New(opts ...Option) Problem { - p := Problem{ - Type: DefaultType, - handler: ProblemHandler, +func NewStatus(s int, opts ...Option) RegisteredProblem { + return New(slices.Concat([]Option{WithStatus(s)}, opts)...) +} + +func NewDetailed(s int, detail string, opts ...Option) RegisteredProblem { + return New(slices.Concat([]Option{WithStatus(s), WithDetail(detail)}, opts)...) +} + +func New(opts ...Option) RegisteredProblem { + p := RegisteredProblem{ + TypeURI: DefaultTypeURI, + handler: DefaultHandler, } + for _, opt := range opts { opt(&p) } return p } -const DefaultType = "about:blank" +var ( + DefaultTypeURI = "about:blank" + DefaultHandler = HandlerAll +) -func NewStatus(s int, opts ...Option) Problem { - return New(slices.Concat([]Option{WithStatus(s)}, opts)...) +func (p RegisteredProblem) Type() string { + return p.TypeURI } -func NewDetailed(s int, detail string, opts ...Option) Problem { - return New(slices.Concat([]Option{WithStatus(s), WithDetail(detail)}, opts)...) +func (p RegisteredProblem) Title() string { + return p.TypeTitle } -func (p Problem) StatusCode() int { - return p.Status +func (p RegisteredProblem) Status() int { + return p.StatusCode } -func (p Problem) ServeHTTP(w http.ResponseWriter, r *http.Request) { - p.handler(p).ServeHTTP(w, r) +func (p RegisteredProblem) Detail() string { + return p.DetailMessage +} + +func (p RegisteredProblem) Instance() string { + return p.InstanceURI +} + +func (p RegisteredProblem) Handler(self Problem) http.Handler { + return p.handler(self) +} + +func (p RegisteredProblem) ServeHTTP(w http.ResponseWriter, r *http.Request) { + p.Handler(p).ServeHTTP(w, r) } func WithType(t string) Option { - return func(p *Problem) { - p.Type = t + return func(p *RegisteredProblem) { + p.TypeURI = t } } func WithTitle(t string) Option { - return func(p *Problem) { - p.Title = t + return func(p *RegisteredProblem) { + p.TypeTitle = t } } func WithStatus(s int) Option { - return func(p *Problem) { - if p.Title == "" { - p.Title = http.StatusText(s) + return func(p *RegisteredProblem) { + if p.TypeTitle == "" { + p.TypeTitle = http.StatusText(s) } - - p.Status = s + p.StatusCode = s } } func WithDetail(d string) Option { - return func(p *Problem) { - p.Detail = d + return func(p *RegisteredProblem) { + p.DetailMessage = d } } func WithDetailf(f string, args ...any) Option { - return func(p *Problem) { - p.Detail = fmt.Sprintf(f, args...) + return func(p *RegisteredProblem) { + p.DetailMessage = fmt.Sprintf(f, args...) } } func WithError(err error) Option { - return func(p *Problem) { - p.Detail = err.Error() + return func(p *RegisteredProblem) { + p.DetailMessage = err.Error() } } func WithInstance(i string) Option { - return func(p *Problem) { - p.Instance = i + return func(p *RegisteredProblem) { + p.InstanceURI = i } } -type Option func(*Problem) +type Option func(*RegisteredProblem)