Skip to content

Errors can be declared globally and route per route #150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ linters:
- ineffassign
- staticcheck
- unused
- usestdlibvars

linters-settings:
gofumpt:
Expand Down
16 changes: 8 additions & 8 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ type ErrorWithStatus interface {

// HTTPError is the error response used by the serialization part of the framework.
type HTTPError struct {
Err error `json:"-" xml:"-"` // Developer readable error message. Not shown to the user to avoid security leaks.
Type string `json:"type,omitempty" xml:"type,omitempty"` // URL of the error type. Can be used to lookup the error in a documentation
Title string `json:"title,omitempty" xml:"title,omitempty"` // Short title of the error
Status int `json:"status,omitempty" xml:"status,omitempty"` // HTTP status code. If using a different type than [HTTPError], for example [BadRequestError], this will be automatically overridden after Fuego error handling.
Detail string `json:"detail,omitempty" xml:"detail,omitempty"` // Human readable error message
Err error `json:"-" xml:"-"` // Developer readable error message. Not shown to the user to avoid security leaks.
Type string `json:"type,omitempty" xml:"type,omitempty" description:"URL of the error type. Can be used to lookup the error in a documentation"` // URL of the error type. Can be used to lookup the error in a documentation
Title string `json:"title,omitempty" xml:"title,omitempty" description:"Short title of the error"` // Short title of the error
Status int `json:"status,omitempty" xml:"status,omitempty" description:"HTTP status code" example:"403"` // HTTP status code. If using a different type than [HTTPError], for example [BadRequestError], this will be automatically overridden after Fuego error handling.
Detail string `json:"detail,omitempty" xml:"detail,omitempty" description:"Human readable error message"` // Human readable error message
Instance string `json:"instance,omitempty" xml:"instance,omitempty"`
Errors []ErrorItem `json:"errors,omitempty" xml:"errors,omitempty"`
}

type ErrorItem struct {
Name string `json:"name"`
Reason string `json:"reason"`
More map[string]any `json:"more,omitempty"`
Name string `json:"name" xml:"name" description:"For example, name of the parameter that caused the error"`
Reason string `json:"reason" xml:"reason" description:"Human readable error message"`
More map[string]any `json:"more,omitempty" xml:"more,omitempty" description:"Additional information about the error"`
}

func (e HTTPError) Error() string {
Expand Down
3 changes: 3 additions & 0 deletions examples/full-app-gourmet/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ func (rs Ressources) Setup(
fuego.WithAutoAuth(controller.LoginFunc),
fuego.WithTemplateFS(templates.FS),
fuego.WithTemplateGlobs("**/*.html", "**/**/*.html"),
fuego.WithGlobalResponseTypes(http.StatusForbidden, "Forbidden"),
}

options = append(serverOptions, options...)

// Create server with some options
app := fuego.NewServer(options...)

app.OpenApiSpec.Info.Title = "Gourmet API"

rs.API.Security = app.Security

// Register middlewares (functions that will be executed before AND after the controllers, in the order they are registered)
Expand Down
3 changes: 2 additions & 1 deletion examples/full-app-gourmet/views/views.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package views

import (
"net/http"
"os"

"github.com/go-fuego/fuego"
Expand All @@ -24,7 +25,7 @@ func (rs Ressource) Routes(s *fuego.Server) {

// Public Chunks
fuego.Get(s, "/recipes-list", rs.showRecipesList)
fuego.Get(s, "/search", rs.searchRecipes)
fuego.Get(s, "/search", rs.searchRecipes).AddError(http.StatusUnauthorized, "Authorization Error").AddError(500, "My Server Error")
fuego.Get(s, "/ingredients/preselect-unit", rs.unitPreselected).QueryParam("id", "")

// Admin Pages
Expand Down
170 changes: 170 additions & 0 deletions examples/petstore/lib/testdata/doc/openapi.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,56 @@
}
},
"schemas": {
"HTTPError": {
"description": "HTTPError schema",
"properties": {
"detail": {
"description": "Human readable error message",
"nullable": true,
"type": "string"
},
"errors": {
"items": {
"properties": {
"more": {
"additionalProperties": {},
"type": "object"
},
"name": {
"type": "string"
},
"reason": {
"type": "string"
}
},
"type": "object"
},
"nullable": true,
"type": "array"
},
"instance": {
"nullable": true,
"type": "string"
},
"status": {
"description": "HTTP status code",
"example": 403,
"nullable": true,
"type": "integer"
},
"title": {
"description": "Short title of the error",
"nullable": true,
"type": "string"
},
"type": {
"description": "URL of the error type. Can be used to lookup the error in a documentation",
"nullable": true,
"type": "string"
}
},
"type": "object"
},
"Pets": {
"description": "Pets schema",
"properties": {
Expand Down Expand Up @@ -138,6 +188,26 @@
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Bad Request _(validation or deserialization error)_"
},
"500": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Internal Server Error _(panics)_"
},
"default": {
"description": ""
}
Expand Down Expand Up @@ -169,6 +239,26 @@
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Bad Request _(validation or deserialization error)_"
},
"500": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Internal Server Error _(panics)_"
},
"default": {
"description": ""
}
Expand Down Expand Up @@ -210,6 +300,26 @@
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Bad Request _(validation or deserialization error)_"
},
"500": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Internal Server Error _(panics)_"
},
"default": {
"description": ""
}
Expand Down Expand Up @@ -250,6 +360,26 @@
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Bad Request _(validation or deserialization error)_"
},
"500": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Internal Server Error _(panics)_"
},
"default": {
"description": ""
}
Expand Down Expand Up @@ -288,6 +418,26 @@
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Bad Request _(validation or deserialization error)_"
},
"500": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Internal Server Error _(panics)_"
},
"default": {
"description": ""
}
Expand Down Expand Up @@ -329,6 +479,26 @@
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Bad Request _(validation or deserialization error)_"
},
"500": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Internal Server Error _(panics)_"
},
"default": {
"description": ""
}
Expand Down
4 changes: 4 additions & 0 deletions mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func Group(s *Server, path string) *Server {
if newServer.groupTag != "" {
s.OpenApiSpec.Tags = append(s.OpenApiSpec.Tags, &openapi3.Tag{Name: newServer.groupTag})
}
newServer.mainRouter = s

return newServer
}
Expand All @@ -48,6 +49,8 @@ type Route[ResponseBody any, RequestBody any] struct {
Path string // URL path. Will be prefixed by the base path of the server and the group path if any
Handler http.Handler // handler executed for this route
FullName string // namespace and name of the function to execute

mainRouter *Server // ref to the main router, used to register the route in the OpenAPI spec
}

// Capture all methods (GET, POST, PUT, PATCH, DELETE) and register a controller.
Expand Down Expand Up @@ -129,6 +132,7 @@ func Register[T, B any](s *Server, route Route[T, B], controller http.Handler, m
route.Operation.Summary = route.NameFromNamespace(camelToHuman)
route.Operation.Description = "controller: `" + route.FullName + "`\n\n---\n\n"
route.Operation.OperationID = route.Method + "_" + s.basePath + strings.ReplaceAll(strings.ReplaceAll(route.Path, "{", ":"), "}", "")
route.mainRouter = s

return route
}
Expand Down
11 changes: 7 additions & 4 deletions openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,15 @@ func RegisterOpenAPIOperation[T, B any](s *Server, method, path string) (*openap
}
}

// Response - globals
for _, openAPIGlobalResponse := range s.globalOpenAPIResponses {
addResponse(s, operation, openAPIGlobalResponse.Code, openAPIGlobalResponse.Description, openAPIGlobalResponse.ErrorType)
}

// Response - 200
responseSchema := schemaTagFromType(s, *new(T))
content := openapi3.NewContentWithSchemaRef(&responseSchema.SchemaRef, []string{"application/json", "application/xml"})
response := openapi3.NewResponse().
WithDescription("OK").
WithContent(content)

response := openapi3.NewResponse().WithDescription("OK").WithContent(content)
operation.AddResponse(200, response)

// Path parameters
Expand Down
30 changes: 30 additions & 0 deletions openapi_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,36 @@ func (r Route[ResponseBody, RequestBody]) AddTags(tags ...string) Route[Response
return r
}

// AddError adds an error to the route.
func (r Route[ResponseBody, RequestBody]) AddError(code int, description string, errorType ...any) Route[ResponseBody, RequestBody] {
addResponse(r.mainRouter, r.Operation, code, description, errorType...)
return r
}

func addResponse(s *Server, operation *openapi3.Operation, code int, description string, errorType ...any) {
var responseSchema schemaTag

if len(errorType) > 0 {
responseSchema = schemaTagFromType(s, errorType[0])
} else {
responseSchema = schemaTagFromType(s, HTTPError{})
}
content := openapi3.NewContentWithSchemaRef(&responseSchema.SchemaRef, []string{"application/json"})

response := openapi3.NewResponse().
WithDescription(description).
WithContent(content)

operation.AddResponse(code, response)
}

// openAPIError describes a response error in the OpenAPI spec.
type openAPIError struct {
Code int
Description string
ErrorType any
}

// RemoveTags removes tags from the route.
func (r Route[ResponseBody, RequestBody]) RemoveTags(tags ...string) Route[ResponseBody, RequestBody] {
for _, tag := range tags {
Expand Down
Loading