parent
1409e0a1be
commit
28877f1b9b
|
@ -47,10 +47,18 @@ components:
|
||||||
properties:
|
properties:
|
||||||
slug:
|
slug:
|
||||||
type: string
|
type: string
|
||||||
|
example: length-out-of-range
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
|
path:
|
||||||
|
type: array
|
||||||
|
description: References field where an error occurred.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
x-go-type-skip-optional-pointer: true
|
||||||
params:
|
params:
|
||||||
type: object
|
type: object
|
||||||
|
description: Additional data related to the error. Can be used for i18n.
|
||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
x-go-type-skip-optional-pointer: true
|
x-go-type-skip-optional-pointer: true
|
||||||
Version:
|
Version:
|
||||||
|
@ -132,8 +140,10 @@ components:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- error
|
- errors
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
error:
|
errors:
|
||||||
$ref: "#/components/schemas/Error"
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -8,6 +8,7 @@ require (
|
||||||
github.com/brianvoe/gofakeit/v6 v6.28.0
|
github.com/brianvoe/gofakeit/v6 v6.28.0
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1
|
github.com/cenkalti/backoff/v4 v4.2.1
|
||||||
github.com/elliotchance/phpserialize v1.3.3
|
github.com/elliotchance/phpserialize v1.3.3
|
||||||
|
github.com/ettle/strcase v0.2.0
|
||||||
github.com/getkin/kin-openapi v0.123.0
|
github.com/getkin/kin-openapi v0.123.0
|
||||||
github.com/go-chi/chi/v5 v5.0.11
|
github.com/go-chi/chi/v5 v5.0.11
|
||||||
github.com/go-chi/render v1.0.3
|
github.com/go-chi/render v1.0.3
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -48,6 +48,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/elliotchance/phpserialize v1.3.3 h1:hV4QVmGdCiYgoBbw+ADt6fNgyZ2mYX0OgpnON1adTCM=
|
github.com/elliotchance/phpserialize v1.3.3 h1:hV4QVmGdCiYgoBbw+ADt6fNgyZ2mYX0OgpnON1adTCM=
|
||||||
github.com/elliotchance/phpserialize v1.3.3/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs=
|
github.com/elliotchance/phpserialize v1.3.3/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs=
|
||||||
|
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
|
||||||
|
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
|
||||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||||
|
|
|
@ -15,14 +15,7 @@ type ValidationError struct {
|
||||||
var _ ErrorWithParams = ValidationError{}
|
var _ ErrorWithParams = ValidationError{}
|
||||||
|
|
||||||
func (e ValidationError) Error() string {
|
func (e ValidationError) Error() string {
|
||||||
prefix := e.Model
|
prefix := e.Path()
|
||||||
|
|
||||||
if e.Field != "" {
|
|
||||||
if len(prefix) > 0 {
|
|
||||||
prefix += "."
|
|
||||||
}
|
|
||||||
prefix += e.Field
|
|
||||||
}
|
|
||||||
|
|
||||||
if prefix != "" {
|
if prefix != "" {
|
||||||
prefix += ": "
|
prefix += ": "
|
||||||
|
@ -31,6 +24,19 @@ func (e ValidationError) Error() string {
|
||||||
return prefix + e.Err.Error()
|
return prefix + e.Err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e ValidationError) Path() string {
|
||||||
|
path := e.Model
|
||||||
|
|
||||||
|
if e.Field != "" {
|
||||||
|
if len(path) > 0 {
|
||||||
|
path += "."
|
||||||
|
}
|
||||||
|
path += e.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
func (e ValidationError) Code() ErrorCode {
|
func (e ValidationError) Code() ErrorCode {
|
||||||
var domainErr Error
|
var domainErr Error
|
||||||
if errors.As(e.Err, &domainErr) {
|
if errors.As(e.Err, &domainErr) {
|
||||||
|
@ -69,14 +75,7 @@ type SliceElementValidationError struct {
|
||||||
var _ ErrorWithParams = SliceElementValidationError{}
|
var _ ErrorWithParams = SliceElementValidationError{}
|
||||||
|
|
||||||
func (e SliceElementValidationError) Error() string {
|
func (e SliceElementValidationError) Error() string {
|
||||||
prefix := e.Model
|
prefix := e.Path()
|
||||||
|
|
||||||
if e.Field != "" {
|
|
||||||
if len(prefix) > 0 {
|
|
||||||
prefix += "."
|
|
||||||
}
|
|
||||||
prefix += e.Field + "[" + strconv.Itoa(e.Index) + "]"
|
|
||||||
}
|
|
||||||
|
|
||||||
if prefix != "" {
|
if prefix != "" {
|
||||||
prefix += ": "
|
prefix += ": "
|
||||||
|
@ -85,6 +84,19 @@ func (e SliceElementValidationError) Error() string {
|
||||||
return prefix + e.Err.Error()
|
return prefix + e.Err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e SliceElementValidationError) Path() string {
|
||||||
|
path := e.Model
|
||||||
|
|
||||||
|
if e.Field != "" {
|
||||||
|
if len(path) > 0 {
|
||||||
|
path += "."
|
||||||
|
}
|
||||||
|
path += e.Field + "[" + strconv.Itoa(e.Index) + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
func (e SliceElementValidationError) Code() ErrorCode {
|
func (e SliceElementValidationError) Code() ErrorCode {
|
||||||
var domainErr Error
|
var domainErr Error
|
||||||
if errors.As(e.Err, &domainErr) {
|
if errors.As(e.Err, &domainErr) {
|
||||||
|
@ -229,6 +241,12 @@ var ErrNil error = simpleError{
|
||||||
slug: "nil",
|
slug: "nil",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrInvalidCursor error = simpleError{
|
||||||
|
msg: "invalid cursor",
|
||||||
|
code: ErrorCodeIncorrectInput,
|
||||||
|
slug: "invalid-cursor",
|
||||||
|
}
|
||||||
|
|
||||||
func validateSliceLen[S ~[]E, E any](s S, min, max int) error {
|
func validateSliceLen[S ~[]E, E any](s S, min, max int) error {
|
||||||
if l := len(s); l > max || l < min {
|
if l := len(s); l > max || l < min {
|
||||||
return LenOutOfRangeError{
|
return LenOutOfRangeError{
|
||||||
|
|
|
@ -109,13 +109,19 @@ type VersionCursor struct {
|
||||||
code NullString
|
code NullString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const versionCursorModelName = "VersionCursor"
|
||||||
|
|
||||||
func NewVersionCursor(code NullString) (VersionCursor, error) {
|
func NewVersionCursor(code NullString) (VersionCursor, error) {
|
||||||
if !code.Valid {
|
if !code.Valid {
|
||||||
return VersionCursor{}, nil
|
return VersionCursor{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateVersionCode(code.Value); err != nil {
|
if err := validateVersionCode(code.Value); err != nil {
|
||||||
return VersionCursor{}, err
|
return VersionCursor{}, ValidationError{
|
||||||
|
Model: versionCursorModelName,
|
||||||
|
Field: "code",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return VersionCursor{
|
return VersionCursor{
|
||||||
|
@ -125,7 +131,7 @@ func NewVersionCursor(code NullString) (VersionCursor, error) {
|
||||||
|
|
||||||
const (
|
const (
|
||||||
encodedVersionCursorMinLength = 1
|
encodedVersionCursorMinLength = 1
|
||||||
encodedVersionCursorMaxLength = 5000
|
encodedVersionCursorMaxLength = 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
func decodeVersionCursor(encoded string) (VersionCursor, error) {
|
func decodeVersionCursor(encoded string) (VersionCursor, error) {
|
||||||
|
@ -135,13 +141,18 @@ func decodeVersionCursor(encoded string) (VersionCursor, error) {
|
||||||
|
|
||||||
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return VersionCursor{}, err
|
return VersionCursor{}, ErrInvalidCursor
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewVersionCursor(NullString{
|
vc, err := NewVersionCursor(NullString{
|
||||||
Value: string(decoded),
|
Value: string(decoded),
|
||||||
Valid: true,
|
Valid: true,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return VersionCursor{}, ErrInvalidCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
return vc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vc VersionCursor) Code() NullString {
|
func (vc VersionCursor) Code() NullString {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package domain_test
|
package domain_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -53,7 +52,11 @@ func TestNewVersionCursor(t *testing.T) {
|
||||||
Valid: true,
|
Valid: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedErr: versionCodeTest.expectedErr,
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "VersionCursor",
|
||||||
|
Field: "code",
|
||||||
|
Err: versionCodeTest.expectedErr,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,23 +185,23 @@ func TestListVersionsParams_SetEncodedCursor(t *testing.T) {
|
||||||
Field: "cursor",
|
Field: "cursor",
|
||||||
Err: domain.LenOutOfRangeError{
|
Err: domain.LenOutOfRangeError{
|
||||||
Min: 1,
|
Min: 1,
|
||||||
Max: 5000,
|
Max: 1000,
|
||||||
Current: 0,
|
Current: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ERR: len(cursor) > 5000",
|
name: "ERR: len(cursor) > 1000",
|
||||||
args: args{
|
args: args{
|
||||||
cursor: gofakeit.LetterN(5001),
|
cursor: gofakeit.LetterN(1001),
|
||||||
},
|
},
|
||||||
expectedErr: domain.ValidationError{
|
expectedErr: domain.ValidationError{
|
||||||
Model: "ListVersionsParams",
|
Model: "ListVersionsParams",
|
||||||
Field: "cursor",
|
Field: "cursor",
|
||||||
Err: domain.LenOutOfRangeError{
|
Err: domain.LenOutOfRangeError{
|
||||||
Min: 1,
|
Min: 1,
|
||||||
Max: 5000,
|
Max: 1000,
|
||||||
Current: 5001,
|
Current: 1001,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -210,7 +213,7 @@ func TestListVersionsParams_SetEncodedCursor(t *testing.T) {
|
||||||
expectedErr: domain.ValidationError{
|
expectedErr: domain.ValidationError{
|
||||||
Model: "ListVersionsParams",
|
Model: "ListVersionsParams",
|
||||||
Field: "cursor",
|
Field: "cursor",
|
||||||
Err: base64.CorruptInputError(4),
|
Err: domain.ErrInvalidCursor,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,12 @@ import (
|
||||||
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/swgui"
|
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/swgui"
|
||||||
"github.com/getkin/kin-openapi/openapi3"
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/render"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiHTTPHandler struct {
|
type apiHTTPHandler struct {
|
||||||
versionSvc *app.VersionService
|
versionSvc *app.VersionService
|
||||||
getOpenAPISchema func() (*openapi3.T, error)
|
openAPISchema func() (*openapi3.T, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithOpenAPIConfig(oapiCfg OpenAPIConfig) APIHTTPHandlerOption {
|
func WithOpenAPIConfig(oapiCfg OpenAPIConfig) APIHTTPHandlerOption {
|
||||||
|
@ -28,7 +28,7 @@ func NewAPIHTTPHandler(versionSvc *app.VersionService, opts ...APIHTTPHandlerOpt
|
||||||
|
|
||||||
h := &apiHTTPHandler{
|
h := &apiHTTPHandler{
|
||||||
versionSvc: versionSvc,
|
versionSvc: versionSvc,
|
||||||
getOpenAPISchema: sync.OnceValues(func() (*openapi3.T, error) {
|
openAPISchema: sync.OnceValues(func() (*openapi3.T, error) {
|
||||||
return getOpenAPISchema(cfg.openAPI)
|
return getOpenAPISchema(cfg.openAPI)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,9 @@ func NewAPIHTTPHandler(versionSvc *app.VersionService, opts ...APIHTTPHandlerOpt
|
||||||
if cfg.openAPI.Enabled {
|
if cfg.openAPI.Enabled {
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
if cfg.openAPI.SwaggerEnabled {
|
if cfg.openAPI.SwaggerEnabled {
|
||||||
|
r.HandleFunc("/v2/swagger", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, r.RequestURI+"/", http.StatusMovedPermanently)
|
||||||
|
})
|
||||||
r.Handle("/v2/swagger/*", swgui.Handler(
|
r.Handle("/v2/swagger/*", swgui.Handler(
|
||||||
cfg.openAPI.BasePath+"/v2/swagger",
|
cfg.openAPI.BasePath+"/v2/swagger",
|
||||||
cfg.openAPI.BasePath+"/v2/openapi3.json",
|
cfg.openAPI.BasePath+"/v2/openapi3.json",
|
||||||
|
@ -47,10 +50,40 @@ func NewAPIHTTPHandler(versionSvc *app.VersionService, opts ...APIHTTPHandlerOpt
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return apimodel.HandlerWithOptions(h, apimodel.ChiServerOptions{BaseRouter: r})
|
// these handlers must be set after the swagger/openapi routes
|
||||||
|
// because we don't want to override the default handlers for these routes
|
||||||
|
r.NotFound(h.handleNotFound)
|
||||||
|
r.MethodNotAllowed(h.handleMethodNotAllowed)
|
||||||
|
|
||||||
|
return apimodel.HandlerWithOptions(h, apimodel.ChiServerOptions{
|
||||||
|
BaseRouter: r,
|
||||||
|
Middlewares: []apimodel.MiddlewareFunc{middleware.NoCache},
|
||||||
|
ErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
apiErrorRenderer{errors: []error{err}}.render(w, r)
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *apiHTTPHandler) renderJSON(w http.ResponseWriter, r *http.Request, status int, body any) {
|
func (h *apiHTTPHandler) handleNotFound(w http.ResponseWriter, r *http.Request) {
|
||||||
render.Status(r, status)
|
apiErrorRenderer{
|
||||||
render.JSON(w, r, body)
|
errors: []error{
|
||||||
|
apiError{
|
||||||
|
status: http.StatusNotFound,
|
||||||
|
slug: "route-not-found",
|
||||||
|
message: "route not found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}.render(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *apiHTTPHandler) handleMethodNotAllowed(w http.ResponseWriter, r *http.Request) {
|
||||||
|
apiErrorRenderer{
|
||||||
|
errors: []error{
|
||||||
|
apiError{
|
||||||
|
status: http.StatusMethodNotAllowed,
|
||||||
|
slug: "method-not-allowed",
|
||||||
|
message: "method not allowed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}.render(w, r)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
package port
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
|
||||||
|
"github.com/ettle/strcase"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiError struct {
|
||||||
|
status int
|
||||||
|
slug string
|
||||||
|
path []string
|
||||||
|
params map[string]any
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e apiError) Error() string {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e apiError) toResponse() apimodel.ErrorResponse {
|
||||||
|
return apimodel.ErrorResponse{
|
||||||
|
Errors: []apimodel.Error{
|
||||||
|
{
|
||||||
|
Message: e.message,
|
||||||
|
Params: e.params,
|
||||||
|
Path: e.path,
|
||||||
|
Slug: e.slug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiErrors []apiError
|
||||||
|
|
||||||
|
func (es apiErrors) status() int {
|
||||||
|
if len(es) == 0 {
|
||||||
|
return http.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
status := es[0].status
|
||||||
|
|
||||||
|
for _, e := range es {
|
||||||
|
if e.status > status {
|
||||||
|
status = e.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es apiErrors) toResponse() apimodel.ErrorResponse {
|
||||||
|
resp := apimodel.ErrorResponse{
|
||||||
|
Errors: make([]apimodel.Error, 0, len(es)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range es {
|
||||||
|
resp.Errors = append(resp.Errors, e.toResponse().Errors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorPathElement struct {
|
||||||
|
model string
|
||||||
|
field string
|
||||||
|
index int // index may be <0 and this means that it is unset
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiErrorRenderer struct {
|
||||||
|
errors []error
|
||||||
|
// formatErrorPath allows to override the default path formatter
|
||||||
|
// for domain.ValidationError and domain.SliceElementValidationError.
|
||||||
|
// If formatErrorPath returns an empty slice, an internal server error is rendered.
|
||||||
|
formatErrorPath func(elems []errorPathElement) []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var errInternalServerError = apiError{
|
||||||
|
status: http.StatusInternalServerError,
|
||||||
|
slug: "internal-server-error",
|
||||||
|
message: "internal server error",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (re apiErrorRenderer) render(w http.ResponseWriter, r *http.Request) {
|
||||||
|
errs := make(apiErrors, 0, len(re.errors))
|
||||||
|
|
||||||
|
for _, err := range re.errors {
|
||||||
|
var apiErr apiError
|
||||||
|
var domainErr domain.Error
|
||||||
|
var paramFormatErr *apimodel.InvalidParamFormatError
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &apiErr):
|
||||||
|
case errors.As(err, ¶mFormatErr):
|
||||||
|
apiErr = apiError{
|
||||||
|
status: http.StatusBadRequest,
|
||||||
|
slug: "invalid-param-format",
|
||||||
|
path: []string{"$query", paramFormatErr.ParamName},
|
||||||
|
message: paramFormatErr.Err.Error(),
|
||||||
|
}
|
||||||
|
case errors.As(err, &domainErr):
|
||||||
|
apiErr = re.domainErrorToAPIError(domainErr)
|
||||||
|
default:
|
||||||
|
apiErr = errInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
errs = append(errs, apiErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderJSON(w, r, errs.status(), errs.toResponse())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (re apiErrorRenderer) domainErrorToAPIError(domainErr domain.Error) apiError {
|
||||||
|
message := domainErr.Error()
|
||||||
|
var pathElems []errorPathElement
|
||||||
|
|
||||||
|
var err error = domainErr
|
||||||
|
for err != nil {
|
||||||
|
var validationErr domain.ValidationError
|
||||||
|
var sliceElementValidationErr domain.SliceElementValidationError
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &validationErr):
|
||||||
|
pathElems = append(pathElems, errorPathElement{
|
||||||
|
model: validationErr.Model,
|
||||||
|
field: validationErr.Field,
|
||||||
|
index: -1,
|
||||||
|
})
|
||||||
|
err = validationErr.Unwrap()
|
||||||
|
message = err.Error()
|
||||||
|
case errors.As(err, &sliceElementValidationErr):
|
||||||
|
pathElems = append(pathElems, errorPathElement{
|
||||||
|
model: sliceElementValidationErr.Model,
|
||||||
|
field: sliceElementValidationErr.Field,
|
||||||
|
index: sliceElementValidationErr.Index,
|
||||||
|
})
|
||||||
|
err = sliceElementValidationErr.Unwrap()
|
||||||
|
message = err.Error()
|
||||||
|
default:
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var path []string
|
||||||
|
|
||||||
|
if len(pathElems) > 0 {
|
||||||
|
if re.formatErrorPath == nil {
|
||||||
|
return errInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
path = re.formatErrorPath(pathElems)
|
||||||
|
|
||||||
|
if len(path) == 0 {
|
||||||
|
return errInternalServerError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var params map[string]any
|
||||||
|
if withParams, ok := domainErr.(domain.ErrorWithParams); ok {
|
||||||
|
params = withParams.Params()
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := make(map[string]any, len(params))
|
||||||
|
for k, v := range params {
|
||||||
|
cloned[strcase.ToCamel(k)] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiError{
|
||||||
|
status: errorCodeToStatusCode(domainErr.Code()),
|
||||||
|
slug: domainErr.Slug(),
|
||||||
|
path: path,
|
||||||
|
params: cloned,
|
||||||
|
message: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorCodeToStatusCode(code domain.ErrorCode) int {
|
||||||
|
switch code {
|
||||||
|
case domain.ErrorCodeIncorrectInput:
|
||||||
|
return http.StatusBadRequest
|
||||||
|
case domain.ErrorCodeNotFound:
|
||||||
|
return http.StatusNotFound
|
||||||
|
case domain.ErrorCodeUnknown:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,13 +9,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *apiHTTPHandler) sendOpenAPIJSON(w http.ResponseWriter, r *http.Request) {
|
func (h *apiHTTPHandler) sendOpenAPIJSON(w http.ResponseWriter, r *http.Request) {
|
||||||
schema, err := h.getOpenAPISchema()
|
schema, err := h.openAPISchema()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
renderPlainText(w, r, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.renderJSON(w, r, http.StatusOK, schema)
|
renderJSON(w, r, http.StatusOK, schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOpenAPISchema(cfg OpenAPIConfig) (*openapi3.T, error) {
|
func getOpenAPISchema(cfg OpenAPIConfig) (*openapi3.T, error) {
|
||||||
|
|
|
@ -11,18 +11,39 @@ func (h *apiHTTPHandler) ListVersions(w http.ResponseWriter, r *http.Request, pa
|
||||||
domainParams := domain.NewListVersionsParams()
|
domainParams := domain.NewListVersionsParams()
|
||||||
|
|
||||||
if params.Limit != nil {
|
if params.Limit != nil {
|
||||||
_ = domainParams.SetLimit(*params.Limit)
|
if err := domainParams.SetLimit(*params.Limit); err != nil {
|
||||||
|
apiErrorRenderer{errors: []error{err}, formatErrorPath: formatListVersionsParamsErrorPath}.render(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.Cursor != nil {
|
if params.Cursor != nil {
|
||||||
_ = domainParams.SetEncodedCursor(*params.Cursor)
|
if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil {
|
||||||
|
apiErrorRenderer{errors: []error{err}, formatErrorPath: formatListVersionsParamsErrorPath}.render(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := h.versionSvc.List(r.Context(), domainParams)
|
res, err := h.versionSvc.List(r.Context(), domainParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
apiErrorRenderer{errors: []error{err}}.render(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.renderJSON(w, r, http.StatusOK, apimodel.NewListVersionsResponse(res))
|
renderJSON(w, r, http.StatusOK, apimodel.NewListVersionsResponse(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatListVersionsParamsErrorPath(elems []errorPathElement) []string {
|
||||||
|
if elems[0].model != "ListVersionsParams" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch elems[0].field {
|
||||||
|
case "cursor":
|
||||||
|
return []string{"$query", "cursor"}
|
||||||
|
case "limit":
|
||||||
|
return []string{"$query", "limit"}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package port
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
func renderJSON(w http.ResponseWriter, r *http.Request, status int, v any) {
|
||||||
|
render.Status(r, status)
|
||||||
|
render.JSON(w, r, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderPlainText(w http.ResponseWriter, r *http.Request, status int, v string) {
|
||||||
|
render.Status(r, status)
|
||||||
|
render.PlainText(w, r, v)
|
||||||
|
}
|
Loading…
Reference in New Issue