Second checkpoint
This commit is contained in:
92
pkg/httputil/httputil.go
Normal file
92
pkg/httputil/httputil.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user