gis/pkg/httputil/httputil.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"
}
}