93 lines
2.6 KiB
Go
93 lines
2.6 KiB
Go
// Package httputil provides small, generic helpers for JSON HTTP handlers:
|
|
// response writing, request decoding, and validation-error formatting.
|
|
package httputil
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
)
|
|
|
|
// maxBodyBytes caps the size of a decoded JSON request body.
|
|
const maxBodyBytes = 1 << 20 // 1 MiB
|
|
|
|
// WriteJSON writes data as a JSON response with the given status code.
|
|
func WriteJSON(w http.ResponseWriter, status int, data any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
if data != nil {
|
|
_ = json.NewEncoder(w).Encode(data)
|
|
}
|
|
}
|
|
|
|
// ErrorResponse is the JSON error envelope.
|
|
type ErrorResponse struct {
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// ValidationErrorResponse is the field-level validation error envelope.
|
|
type ValidationErrorResponse struct {
|
|
Errors map[string]string `json:"errors"`
|
|
}
|
|
|
|
// WriteError writes a JSON error envelope: {"error": "..."}.
|
|
func WriteError(w http.ResponseWriter, status int, msg string) {
|
|
WriteJSON(w, status, ErrorResponse{Error: msg})
|
|
}
|
|
|
|
// DecodeJSON reads and validates a JSON body into a value of type T. It caps the
|
|
// body size and rejects unknown fields.
|
|
func DecodeJSON[T any](w http.ResponseWriter, r *http.Request) (T, error) {
|
|
var v T
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
|
|
|
|
dec := json.NewDecoder(r.Body)
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&v); err != nil {
|
|
return v, err
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// WriteValidationErrors renders validator.ValidationErrors as a field->message
|
|
// map under {"errors": {...}} with a 422 status.
|
|
func WriteValidationErrors(w http.ResponseWriter, err error) {
|
|
var ve validator.ValidationErrors
|
|
if !errors.As(err, &ve) {
|
|
WriteError(w, http.StatusBadRequest, "invalid request")
|
|
return
|
|
}
|
|
|
|
problems := make(map[string]string, len(ve))
|
|
for _, fe := range ve {
|
|
problems[fe.Field()] = messageForTag(fe)
|
|
}
|
|
WriteJSON(w, http.StatusUnprocessableEntity, ValidationErrorResponse{Errors: problems})
|
|
}
|
|
|
|
func messageForTag(fe validator.FieldError) string {
|
|
switch fe.Tag() {
|
|
case "required":
|
|
return "is required"
|
|
case "email":
|
|
return "must be a valid email address"
|
|
case "uuid", "uuid4":
|
|
return "must be a valid UUID"
|
|
case "min":
|
|
return fmt.Sprintf("must be at least %s characters", fe.Param())
|
|
case "max":
|
|
return fmt.Sprintf("must be at most %s characters", fe.Param())
|
|
case "gte":
|
|
return fmt.Sprintf("must be %s or greater", fe.Param())
|
|
case "lte":
|
|
return fmt.Sprintf("must be %s or less", fe.Param())
|
|
case "oneof":
|
|
return fmt.Sprintf("must be one of: %s", fe.Param())
|
|
default:
|
|
return "is invalid"
|
|
}
|
|
}
|