From 7c00e0c8bf148ba1abcbd7a9ed390bdaab27832b Mon Sep 17 00:00:00 2001 From: William Petit Date: Fri, 28 Dec 2018 11:45:30 +0100 Subject: [PATCH] Add 'api' package to ease basic JSON API development --- api/api.go | 47 ++++++++++++++++++++++++++++++++ api/bind.go | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 api/api.go create mode 100644 api/bind.go diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..7f8f5cd --- /dev/null +++ b/api/api.go @@ -0,0 +1,47 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" +) + +// Response is a JSON encoded HTTP response body +type Response struct { + Error *Error `json:",omitempty"` + Data interface{} `json:",omitempty"` +} + +type ErrorCode string + +// Error is a JSON encoded error +type Error struct { + Code ErrorCode + Data interface{} `json:",omitempty"` +} + +func (e *Error) Error() string { + return fmt.Sprintf("%s: %v", e.Code, e.Data) +} + +func DataResponse(w http.ResponseWriter, status int, data interface{}) { + response(w, status, &Response{ + Data: data, + }) +} + +func ErrorResponse(w http.ResponseWriter, status int, code ErrorCode, data interface{}) { + response(w, status, &Response{ + Error: &Error{code, data}, + }) +} + +func response(w http.ResponseWriter, status int, res *Response) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + if err := encoder.Encode(res); err != nil { + panic(err) + } +} diff --git a/api/bind.go b/api/bind.go new file mode 100644 index 0000000..520aaf7 --- /dev/null +++ b/api/bind.go @@ -0,0 +1,77 @@ +package api + +import ( + "encoding/json" + "net/http" + + validator "gopkg.in/go-playground/validator.v9" +) + +const ( + ErrCodeMalformedRequest ErrorCode = "malformed-request" + ErrCodeInvalidRequest ErrorCode = "invalid-request" + ErrCodeInvalidFieldValue ErrorCode = "invalid-field-value" +) + +// Bind parses and bind the request body to the given target +// Bind wil also validates the received data with a validator instance. +// If the data does not match the target constraints, an ErrorResponse will be +// sent back. +// See gopkg.in/go-playground/validator.v9 for more informations about data +// validation. +func Bind(w http.ResponseWriter, r *http.Request, target interface{}) (ok bool) { + if err := parseBody(r, target); err != nil { + ErrorResponse( + w, http.StatusBadRequest, + ErrCodeMalformedRequest, + nil, + ) + return false + } + validate := validator.New() + if err := validate.Struct(target); err != nil { + handleValidateError(w, err) + return false + } + return true +} + +func parseBody(r *http.Request, target interface{}) error { + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(target); err != nil { + return err + } + return nil +} + +type fieldError struct { + Field string + Tag string + Param string +} + +func handleValidateError(w http.ResponseWriter, err error) { + if validationErrors, ok := err.(validator.ValidationErrors); ok { + fields := make([]fieldError, 0) + for _, e := range validationErrors { + fields = append(fields, fieldError{ + Field: e.Field(), + Tag: e.Tag(), + Param: e.Param(), + }) + } + ErrorResponse( + w, http.StatusBadRequest, + ErrCodeInvalidFieldValue, + map[string]interface{}{ + "Fields": fields, + }, + ) + return + } + ErrorResponse( + w, http.StatusBadRequest, + ErrCodeInvalidRequest, + nil, + ) +}