diff --git a/Dockerfile.dev b/Dockerfile.dev index 8a60274..62702fa 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -14,7 +14,6 @@ WORKDIR /app COPY . . # Setup go mod -RUN [ ! -f ./go.mod ] && go mod init main RUN go mod tidy # Development requires fresh diff --git a/Dockerfile.scratch b/Dockerfile.scratch index 13d8cd9..67dea3c 100644 --- a/Dockerfile.scratch +++ b/Dockerfile.scratch @@ -14,7 +14,6 @@ WORKDIR /app COPY . . # Setup go mod -# RUN [ ! -f ./go.mod ] && go mod init main RUN go mod tidy RUN GOOS=linux CGO_ENABLED=0 go build -ldflags="-w -s" -o api . diff --git a/cmd/oas.go b/cmd/oas.go index 7a8dd25..ca4fb1d 100644 --- a/cmd/oas.go +++ b/cmd/oas.go @@ -13,10 +13,6 @@ type OasCmd struct { } func (v *OasCmd) Execute(args []string) error { - fmt.Println("oascmd") - fmt.Printf("%#v\n", v) - fmt.Printf("%#v\n", Application) - router := router.NewRouter(Application.Name, Application.Description, Application.Version) oasModel := exporter.ToOasModel(router.OpenAPI) diff --git a/endpoint/docs/docs.go b/endpoint/docs/docs.go index d5467df..d08ea5c 100644 --- a/endpoint/docs/docs.go +++ b/endpoint/docs/docs.go @@ -11,7 +11,7 @@ import ( "github.com/charmixer/golang-api-template/app" "github.com/charmixer/golang-api-template/endpoint" - "github.com/charmixer/golang-api-template/middleware" + // "github.com/charmixer/golang-api-template/middleware" "github.com/rs/zerolog/log" ) @@ -144,7 +144,7 @@ func NewGetDocsEndpoint() (endpoint.EndpointHandler) { }), endpoint.WithMiddleware( - middleware.WithRequestParser(&ep.Request), + ), ) diff --git a/endpoint/docs/openapi.go b/endpoint/docs/openapi.go index be6810e..b77826f 100644 --- a/endpoint/docs/openapi.go +++ b/endpoint/docs/openapi.go @@ -22,12 +22,8 @@ var ( } ) -type Test struct { - Testfield string `validate:"iscolor"` -} type GetOpenapiRequest struct { - Format string `json:"format" oas-query:"format" validate:"iscolor" oas-desc:"Format returned by the endpoint, eg. json"` - Test Test `oas-query:"fordasat" validate:"iscolor" oas-desc:"Format returned by the endpoint, eg. json"` + Format string `json:"format" oas-query:"format" oas-desc:"Format returned by the endpoint, eg. json"` } // https://golang.org/doc/effective_go#embedding @@ -84,9 +80,6 @@ func NewGetOpenapiEndpoint() (endpoint.EndpointHandler) { }), endpoint.WithMiddleware( - middleware.WithRequestParser(&ep.Request), - middleware.WithRequestValidation(&ep.Request/*, &ep.BadRequest*/), - middleware.WithResponseWriter(&ep.responseType, &ep.Response), ), ) diff --git a/endpoint/errors/errors.go b/endpoint/errors/errors.go new file mode 100644 index 0000000..53aafb6 --- /dev/null +++ b/endpoint/errors/errors.go @@ -0,0 +1,25 @@ +package errors + +/* +Codes we care about + +200 OK +400 BadRequest +401 Unauthorized - Authentication denied +403 Forbidden - Authorization denined +404 Not Found +500 Internal Server Error +503 Service Unavailable +*/ + +type FieldError struct { + Path string `json:"path"` + Err string `json:"err"` +} + +type HttpClientErrorResponse struct { + StatusCode int `json:"status_code"` + Method string `json:"method"` + Url string `json:"url"` + Errors []FieldError `json:"errors"` +} diff --git a/endpoint/health/health.go b/endpoint/health/health.go index a20c842..358aa39 100644 --- a/endpoint/health/health.go +++ b/endpoint/health/health.go @@ -64,9 +64,6 @@ func NewGetHealthEndpoint() (endpoint.EndpointHandler) { }), endpoint.WithMiddleware( - middleware.WithRequestParser(&ep.Request), - middleware.WithRequestValidation(&ep.Request), - middleware.WithResponseValidation(&ep.Response), middleware.WithJsonResponseWriter(&ep.Response), ), diff --git a/endpoint/metrics/metrics.go b/endpoint/metrics/metrics.go index 4d052b6..398d2d0 100644 --- a/endpoint/metrics/metrics.go +++ b/endpoint/metrics/metrics.go @@ -6,7 +6,7 @@ import ( "net/http" "github.com/charmixer/golang-api-template/endpoint" - "github.com/charmixer/golang-api-template/middleware" + // "github.com/charmixer/golang-api-template/middleware" "github.com/charmixer/oas/api" ) @@ -54,7 +54,7 @@ func NewGetMetricsEndpoint() (endpoint.EndpointHandler) { }), endpoint.WithMiddleware( - middleware.WithRequestParser(&ep.Request), + ), ) diff --git a/middleware/request.go b/middleware/request.go new file mode 100644 index 0000000..68a388b --- /dev/null +++ b/middleware/request.go @@ -0,0 +1,31 @@ +package middleware + +import ( + "net/http" + + "encoding/json" + + "go.opentelemetry.io/otel" +) + +// TODO move this to better place +func WithJsonRequestParser(request interface{}) MiddlewareHandler { + return func (next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + tr := otel.Tracer("request") + ctx, span := tr.Start(ctx, "middleware.request-parser") + defer span.End() + + // Try to decode the request body into the struct. If there is an error, + // respond to the client with the error message and a 400 status code. + err := json.NewDecoder(r.Body).Decode(&request) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + next.ServeHTTP(w, r) + }) + } +} diff --git a/middleware/response.go b/middleware/response.go index 6705340..4dc18b7 100644 --- a/middleware/response.go +++ b/middleware/response.go @@ -10,7 +10,16 @@ import ( "go.opentelemetry.io/otel" ) -func WithJsonResponseWriter(response interface{}) MiddlewareHandler { +func contains(s []int, e int) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +func WithJsonResponseWriter(response interface{}, status ...int) MiddlewareHandler { return func (next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -20,19 +29,22 @@ func WithJsonResponseWriter(response interface{}) MiddlewareHandler { next.ServeHTTP(w, r) - ctx, span = tr.Start(ctx, "write response") - defer span.End() + // Only write if written statuscode is in given list + if contains(status, w.(*responseWriter).Status) { + ctx, span = tr.Start(ctx, "write response") + defer span.End() - d, err := json.Marshal(response) - if err != nil { - panic(err) // TODO FIXME + d, err := json.Marshal(response) + if err != nil { + panic(err) // TODO FIXME + } + w.Write(d) } - w.Write(d) }) } } -func WithYamlResponseWriter(response interface{}) MiddlewareHandler { +func WithYamlResponseWriter(response interface{}, status ...int) MiddlewareHandler { return func (next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -42,19 +54,22 @@ func WithYamlResponseWriter(response interface{}) MiddlewareHandler { next.ServeHTTP(w, r) - ctx, span = tr.Start(ctx, "write response") - defer span.End() + // Only write if written statuscode is in given list + if contains(status, w.(*responseWriter).Status) { + ctx, span = tr.Start(ctx, "write response") + defer span.End() - d, err := yaml.Marshal(response) - if err != nil { - panic(err) // TODO FIXME + d, err := yaml.Marshal(response) + if err != nil { + panic(err) // TODO FIXME + } + w.Write(d) } - w.Write(d) }) } } -func WithResponseWriter(tp *string, response interface{}) MiddlewareHandler { +func WithResponseWriter(tp *string, response interface{}, status ...int) MiddlewareHandler { return func (next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -64,26 +79,29 @@ func WithResponseWriter(tp *string, response interface{}) MiddlewareHandler { next.ServeHTTP(w, r) - ctx, span = tr.Start(ctx, "write response") - defer span.End() - - switch (*tp) { - case "json": - d, err := json.Marshal(response) - if err != nil { - panic(err) // TODO FIXME - } - w.Write(d) - break; - case "yaml": - d, err := yaml.Marshal(response) - if err != nil { - panic(err) // TODO FIXME - } - w.Write(d) - break; - default: - panic(fmt.Sprintf("Unknown response type given, %s", tp)) + // Only write if written statuscode is in given list + if contains(status, w.(*responseWriter).Status) { + ctx, span = tr.Start(ctx, "write response") + defer span.End() + + switch (*tp) { + case "json": + d, err := json.Marshal(response) + if err != nil { + panic(err) // TODO FIXME + } + w.Write(d) + break; + case "yaml": + d, err := yaml.Marshal(response) + if err != nil { + panic(err) // TODO FIXME + } + w.Write(d) + break; + default: + panic(fmt.Sprintf("Unknown response type given, %s", *tp)) + } } }) diff --git a/middleware/validate.go b/middleware/validate.go index 8cbff5c..29f63d4 100644 --- a/middleware/validate.go +++ b/middleware/validate.go @@ -15,6 +15,8 @@ import ( "github.com/go-playground/locales/en" ut "github.com/go-playground/universal-translator" en_translations "github.com/go-playground/validator/v10/translations/en" + + ep_errors "github.com/charmixer/golang-api-template/endpoint/errors" ) var ( @@ -46,30 +48,6 @@ func init() { en_translations.RegisterDefaultTranslations(validate, trans) } -/* -Codes we care about - -200 OK, -400 BadRequest -401 Unauthorized - Authentication denied, -403 Forbidden - Authorization denined, -404 Not Found -500 Internal Server Error, -503 Service Unavailable -*/ - -type FieldError struct { - Path string `json:"path"` - Err string `json:"err"` -} - -type HttpClientErrorResponse struct { - StatusCode int `json:"status_code"` - Method string `json:"method"` - Url string `json:"url"` - Errors []FieldError `json:"errors"` -} - func WithRequestValidation(request interface{}) MiddlewareHandler{ return func (next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -86,7 +64,7 @@ func WithRequestValidation(request interface{}) MiddlewareHandler{ return } - response := HttpClientErrorResponse{ + response := ep_errors.HttpClientErrorResponse{ StatusCode: http.StatusBadRequest, Method: r.Method, Url: fmt.Sprintf("%s%s", r.Host, r.URL.RequestURI()), @@ -94,7 +72,7 @@ func WithRequestValidation(request interface{}) MiddlewareHandler{ } for _, verr := range err.(validator.ValidationErrors) { - e := FieldError{ + e := ep_errors.FieldError{ Path: verr.Field(), Err: verr.Translate(trans), } @@ -163,21 +141,3 @@ func WithResponseValidation(response interface{}) MiddlewareHandler { }) } } - - - - - -// TODO move this to better place -func WithRequestParser(request interface{}) MiddlewareHandler { - return func (next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - tr := otel.Tracer("request") - ctx, span := tr.Start(ctx, "middleware.request-parser") - defer span.End() - - next.ServeHTTP(w, r) - }) - } -} diff --git a/router/router.go b/router/router.go index 332c79e..4fa0de8 100644 --- a/router/router.go +++ b/router/router.go @@ -14,12 +14,9 @@ import ( "github.com/charmixer/oas/api" "github.com/julienschmidt/httprouter" -) -/*type Route interface { - http.Handler - Specification() api.Path -}*/ + "github.com/rs/zerolog/log" +) type Router struct { httprouter.Router @@ -28,7 +25,12 @@ type Router struct { } func (r *Router) NewRoute(method string, uri string, ep endpoint.EndpointHandler, handlers ...middleware.MiddlewareHandler) { - r.OpenAPI.NewEndpoint(method, uri, ep.Specification()) + log.Debug(). + Str("method", method). + Str("endpoint", uri). + Msg("Setting up endpoint") + + r.OpenAPI.NewEndpoint(method, uri, ep.Specification()) middlewareHandlers := append(handlers, ep.Middleware()...) r.Handler(method, uri, middleware.New(ep.(http.Handler), middlewareHandlers...))