first commit

This commit is contained in:
2026-08-24 13:06:29 +05:30
commit 7d12ebef3d
86 changed files with 39996 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
package httpserver
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"github.com/krow/krow-backend/go-api/internal/domain"
)
// envelope is the success shape from api-contract.md §4.
type envelope struct {
Data any `json:"data"`
Meta *meta `json:"meta,omitempty"`
}
// meta accompanies a collection. `truncated` exists so the silent-truncation
// problem in §12.2 is fixable without another contract change.
type meta struct {
Total int `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
Returned int `json:"returned"`
Truncated bool `json:"truncated"`
}
// errorEnvelope is the failure shape from api-contract.md §5.
type errorEnvelope struct {
Error errorBody `json:"error"`
}
type errorBody struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]string `json:"details"`
}
func writeJSON(w http.ResponseWriter, code int, body any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(code)
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
_ = enc.Encode(body)
}
func writeRecord(w http.ResponseWriter, code int, rec domain.Record) {
writeJSON(w, code, envelope{Data: rec})
}
func writePage(w http.ResponseWriter, page *domain.Page) {
writeJSON(w, http.StatusOK, envelope{
Data: page.Records,
Meta: &meta{
Total: page.Total,
Limit: page.Limit,
Offset: page.Offset,
Returned: len(page.Records),
Truncated: page.Total > page.Offset+len(page.Records),
},
})
}
// statusFor maps the contract's error codes onto HTTP status codes.
func statusFor(code string) int {
switch code {
case "unauthorized":
return http.StatusUnauthorized
case "forbidden":
return http.StatusForbidden
case "rate_limited":
return http.StatusTooManyRequests
case "not_found":
return http.StatusNotFound
case "validation_failed":
return http.StatusUnprocessableEntity
case "invalid_query":
return http.StatusBadRequest
case "conflict":
return http.StatusConflict
default:
return http.StatusInternalServerError
}
}
// writeError renders any error as the documented envelope.
//
// An unrecognised error is deliberately flattened to a generic message: the
// detail goes to the log, not to the client.
func writeError(w http.ResponseWriter, log *slog.Logger, err error) {
var de *domain.Error
if !errors.As(err, &de) {
log.Error("unhandled error", "error", err)
writeJSON(w, http.StatusInternalServerError, errorEnvelope{Error: errorBody{
Code: "internal", Message: "internal error", Details: map[string]string{},
}})
return
}
if de.Code == "internal" {
log.Error("internal error", "error", de.Unwrap())
}
details := de.Details
if details == nil {
details = map[string]string{}
}
writeJSON(w, statusFor(de.Code), errorEnvelope{Error: errorBody{
Code: de.Code, Message: de.Message, Details: details,
}})
}