package api import ( "encoding/json" "errors" "fmt" "io" "mime" "net/http" "strconv" "strings" "havenllo/internal/domain" ) const maxJSONBody = 1 << 20 func writeJSON(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) } func writeError(w http.ResponseWriter, status int, code, message string) { writeJSON(w, status, errorResponse{Error: apiError{Code: code, Message: message}}) } func writeDomainError(w http.ResponseWriter, err error) { var validation domain.ValidationError switch { case errors.Is(err, domain.ErrNotFound): writeError(w, http.StatusNotFound, "not_found", "resource not found") case errors.Is(err, domain.ErrLastList): writeError(w, http.StatusConflict, "last_list", err.Error()) case errors.As(err, &validation): writeError(w, http.StatusUnprocessableEntity, "validation_error", validation.Message) default: writeError(w, http.StatusInternalServerError, "internal_error", "an unexpected error occurred") } } func requireJSON(w http.ResponseWriter, r *http.Request) bool { contentType := r.Header.Get("Content-Type") mediaType, _, err := mime.ParseMediaType(contentType) if err != nil || mediaType != "application/json" { writeError(w, http.StatusUnsupportedMediaType, "unsupported_media_type", "Content-Type must be application/json") return false } return true } func decodeJSON(w http.ResponseWriter, r *http.Request, target any) bool { r.Body = http.MaxBytesReader(w, r.Body, maxJSONBody) decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(target); err != nil { writeDecodeError(w, err) return false } if err := decoder.Decode(&struct{}{}); err != io.EOF { writeError(w, http.StatusBadRequest, "invalid_json", "request body must contain one JSON value") return false } return true } func writeDecodeError(w http.ResponseWriter, err error) { var syntaxError *json.SyntaxError var typeError *json.UnmarshalTypeError switch { case errors.As(err, &syntaxError): writeError(w, http.StatusBadRequest, "invalid_json", "request body contains malformed JSON") case errors.Is(err, io.ErrUnexpectedEOF): writeError(w, http.StatusBadRequest, "invalid_json", "request body contains malformed JSON") case errors.As(err, &typeError): writeError(w, http.StatusBadRequest, "invalid_json", fmt.Sprintf("invalid value for %s", typeError.Field)) case strings.HasPrefix(err.Error(), "json: unknown field "): writeError(w, http.StatusBadRequest, "invalid_json", "request body contains an unknown field") case errors.Is(err, io.EOF): writeError(w, http.StatusBadRequest, "invalid_json", "request body must not be empty") case strings.Contains(err.Error(), "http: request body too large"): writeError(w, http.StatusBadRequest, "invalid_json", "request body is too large") default: writeError(w, http.StatusBadRequest, "invalid_json", "request body is invalid") } } func pathID(w http.ResponseWriter, r *http.Request, key string) (int64, bool) { id, err := strconv.ParseInt(r.PathValue(key), 10, 64) if err != nil || id < 1 { writeError(w, http.StatusBadRequest, "invalid_id", "resource ID must be a positive integer") return 0, false } return id, true }