commit 1fbda13edadcdaba0b407251bde30bfc95bbdb68 Author: Aneurin Barker Snook Date: Mon Oct 9 07:18:30 2023 +0100 add rest package diff --git a/body.go b/body.go new file mode 100644 index 0000000..3c17a6e --- /dev/null +++ b/body.go @@ -0,0 +1,28 @@ +package rest + +import ( + "encoding/json" + "io" + "net/http" +) + +// ReadRequestJSON reads the body of an HTTP request into a target reference. +func ReadRequestJSON(req *http.Request, v any) error { + data, err := io.ReadAll(req.Body) + if err != nil { + return err + } + return json.Unmarshal(data, v) +} + +// WriteResponseJSON writes an HTTP response as JSON. +func WriteResponseJSON(w http.ResponseWriter, statusCode int, data any) error { + b, err := json.Marshal(data) + if err != nil { + return err + } + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(statusCode) + w.Write(b) + return nil +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..33fa254 --- /dev/null +++ b/error.go @@ -0,0 +1,89 @@ +package rest + +import ( + "net/http" +) + +// Error represents a REST API error. +// It can be marshaled to JSON with ease. +type Error struct { + StatusCode int `json:"statusCode"` // HTTP status code (200, 404, 500 etc.) + Message string `json:"message"` // Status message ("OK", "Not found", "Internal server error" etc.) + Data map[string]interface{} `json:"data,omitempty"` // Optional additional data. +} + +// Error retrieves the message of a REST API Error. +// If it has a "error" string attached using WithData or WithError, that message is returned. +// Otherwise, the Error's own message is returned. +func (e Error) Error() string { + if e.Data != nil && e.Data["error"] != nil { + if value, ok := e.Data["error"].(string); ok { + return value + } + } + return e.Message +} + +// Is determines whether the Error is an instance of the target. +// https://pkg.go.dev/errors#Is +// +// This function supports matching both any error and a specific error, based on the status code. +// Use rest.Err (an empty error) for the former: +// +// errors.Is(err, rest.Err) // True if any REST API Error +// errors.Is(err, rest.ErrBadRequest) // True if error is REST 400 Bad Request +func (e Error) Is(target error) bool { + t, ok := target.(Error) + if !ok { + return false + } + if t.StatusCode == 0 { + return true + } + return t.StatusCode == e.StatusCode +} + +// WithData returns a copy of the HTTP error with the given data merged in. +func (e Error) WithData(data map[string]interface{}) Error { + if e.Data == nil { + e.Data = map[string]any{} + } + if data != nil { + for key, value := range data { + e.Data[key] = value + } + } + return e +} + +// WithError returns a copy of the HTTP error with the given error's message merged in to its additional data. +func (e Error) WithError(err error) Error { + return e.WithData(map[string]interface{}{ + "error": err.Error(), + }) +} + +// WithMessage returns a copy of the HTTP error with the given message. +func (e Error) WithMessage(message string) Error { + e.Message = message + return e +} + +// WithValue returns a copy of the HTTP error with a single data value added. +func (e Error) WithValue(name string, value any) Error { + return e.WithData(map[string]any{ + name: value, + }) +} + +// Write writes the HTTP error to an HTTP response as plain text. +// Additional data is omitted. +func (e Error) Write(w http.ResponseWriter) { + w.WriteHeader(e.StatusCode) + w.Write([]byte(e.Message)) +} + +// WriteJSON writes the HTTP error to an HTTP response as JSON. +func (e Error) WriteJSON(w http.ResponseWriter) error { + return WriteResponseJSON(w, e.StatusCode, e) +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..9912542 --- /dev/null +++ b/errors.go @@ -0,0 +1,30 @@ +package rest + +import "net/http" + +// Err is an empty error, representing any REST API Error. +var Err = Error{} + +// ErrBadRequest represents HTTP 400 Bad Request. +var ErrBadRequest = Error{ + StatusCode: http.StatusBadRequest, + Message: "Bad request", +} + +// ErrInternalServerError represents HTTP 500 Internal Server Error. +var ErrInternalServerError = Error{ + StatusCode: http.StatusInternalServerError, + Message: "Internal server error", +} + +// ErrNotFound represents HTTP 404 Not Found. +var ErrNotFound = Error{ + StatusCode: http.StatusNotFound, + Message: "Not found", +} + +// ErrUnavailable represents HTTP 503 Service Unavailable. +var ErrUnavailable = Error{ + StatusCode: http.StatusServiceUnavailable, + Message: "Service unavailable", +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2378ec4 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/recipeer/go/rest + +go 1.21.2