Initial commit
This commit is contained in:
commit
89053153cc
8152 files changed
+2086461
No files matched your search
@@ -0,0 +1,126 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.k6n.net/mats/fullstack-template/internal/items"
|
||||
)
|
||||
|
||||
// itemHandler adapts HTTP requests to the items service: parse input, call
|
||||
// the service, map errors to status codes. No business logic lives here.
|
||||
type itemHandler struct {
|
||||
svc *items.Service
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (h *itemHandler) list(w http.ResponseWriter, r *http.Request) {
|
||||
f := items.Filter{
|
||||
Status: items.Status(r.URL.Query().Get("status")),
|
||||
Limit: queryInt(r, "limit", items.DefaultLimit),
|
||||
Offset: queryInt(r, "offset", 0),
|
||||
}
|
||||
result, err := h.svc.List(r.Context(), f)
|
||||
if err != nil {
|
||||
h.fail(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *itemHandler) create(w http.ResponseWriter, r *http.Request) {
|
||||
var in items.CreateInput
|
||||
if !decodeJSON(w, r, &in) {
|
||||
return
|
||||
}
|
||||
item, err := h.svc.Create(r.Context(), in)
|
||||
if err != nil {
|
||||
h.fail(w, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Location", "/api/v1/items/"+strconv.FormatInt(item.ID, 10))
|
||||
writeJSON(w, http.StatusCreated, item)
|
||||
}
|
||||
|
||||
func (h *itemHandler) get(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := pathID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
item, err := h.svc.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
h.fail(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (h *itemHandler) update(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := pathID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var in items.UpdateInput
|
||||
if !decodeJSON(w, r, &in) {
|
||||
return
|
||||
}
|
||||
item, err := h.svc.Update(r.Context(), id, in)
|
||||
if err != nil {
|
||||
h.fail(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (h *itemHandler) delete(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := pathID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.svc.Delete(r.Context(), id); err != nil {
|
||||
h.fail(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// pathID parses the {id} path segment; writes a 400 and returns false on
|
||||
// input that can't be an id.
|
||||
func pathID(w http.ResponseWriter, r *http.Request) (int64, bool) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
writeError(w, http.StatusBadRequest, "invalid_id", "id must be a positive integer")
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
// queryInt reads an int query parameter, falling back when absent or junk.
|
||||
func queryInt(r *http.Request, key string, fallback int) int {
|
||||
v := r.URL.Query().Get(key)
|
||||
if v == "" {
|
||||
return fallback
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n < 0 {
|
||||
return fallback
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// fail is the single translation point from items errors to the wire:
|
||||
// ErrNotFound → 404, *ValidationError → 400, anything else → 500 (logged).
|
||||
func (h *itemHandler) fail(w http.ResponseWriter, err error) {
|
||||
var vErr *items.ValidationError
|
||||
switch {
|
||||
case errors.Is(err, items.ErrNotFound):
|
||||
writeError(w, http.StatusNotFound, "not_found", "item not found")
|
||||
case errors.As(err, &vErr):
|
||||
writeError(w, http.StatusBadRequest, "validation_failed", vErr.Error())
|
||||
default:
|
||||
h.logger.Error("request failed", "err", err)
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "an internal error occurred")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user