Everything under code.aneur.in/go/* except validate was archived. Swap
each for its replacement; validate stays.
- rest -> internal/rest: the JSON response/error helpers zampler actually
uses (~110 lines), same {"message","data"} wire shape and status codes.
- sqan -> internal/scan: a filepath.WalkDir walk. zampler only filtered
by extension; sqan's path semantics (absolute root, rooted RelativePath,
"/" RelativeDir for root-level files) are kept.
- sqimple + sqaffold query building -> github.com/doug-martin/goqu/v9,
which also does the row scanning that record + sqaffold provided. A
fileRow struct with db tags maps to types.File (millis <-> time.Time).
- sqaffold.Migrate + version.Migration -> a ~40-line migrator in
internal/database/migration.go tracking applied names in a
schema_migrations table. goose was disproportionate for one migration.
- version -> gone (only its Migration helper was used).
Net dependency change: -6 code.aneur.in/go modules, +1 (goqu).
go build / go vet / go test ./... green; manual run verified migration,
scan+insert, list/get/search/sort/paging, 404 and 400 shapes, and
rescan idempotency (on conflict replace).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
112 lines
3.1 KiB
Go
112 lines
3.1 KiB
Go
// Package rest holds the small JSON response and error helpers the HTTP
|
|
// handlers share. It replaces the archived code.aneur.in/go/rest with just
|
|
// the pieces zampler uses, keeping the same {"message", "data"} wire shape.
|
|
package rest
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
)
|
|
|
|
// REST API errors. Err matches any error produced here (errors.Is).
|
|
var (
|
|
Err = Error{}
|
|
|
|
ErrBadRequest = NewError(http.StatusBadRequest, "")
|
|
ErrNotFound = NewError(http.StatusNotFound, "")
|
|
ErrInternalServerError = NewError(http.StatusInternalServerError, "")
|
|
)
|
|
|
|
// Error is a JSON-serialisable API error with an HTTP status code and
|
|
// optional structured data.
|
|
type Error struct {
|
|
StatusCode int `json:"-"`
|
|
Message string `json:"message"`
|
|
Data map[string]any `json:"data,omitempty"`
|
|
}
|
|
|
|
func (e Error) Error() string { return e.Message }
|
|
|
|
// Is reports whether target is an Error with the same status code, or an
|
|
// empty Error (which matches any).
|
|
func (e Error) Is(target error) bool {
|
|
t, ok := target.(Error)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return t.StatusCode == e.StatusCode || t.StatusCode == 0
|
|
}
|
|
|
|
// WithData returns a copy with data merged in.
|
|
func (e Error) WithData(data map[string]any) Error {
|
|
merged := map[string]any{}
|
|
for k, v := range e.Data {
|
|
merged[k] = v
|
|
}
|
|
for k, v := range data {
|
|
merged[k] = v
|
|
}
|
|
e.Data = merged
|
|
return e
|
|
}
|
|
|
|
// WithError returns a copy with err as the message if there is none yet,
|
|
// otherwise attached under data.error.
|
|
func (e Error) WithError(err error) Error {
|
|
if e.Message == "" {
|
|
return e.WithMessage(err.Error())
|
|
}
|
|
return e.WithData(map[string]any{"error": err.Error()})
|
|
}
|
|
|
|
// WithMessage returns a copy with the given message.
|
|
func (e Error) WithMessage(message string) Error {
|
|
e.Message = message
|
|
return e
|
|
}
|
|
|
|
// WithValue returns a copy with a single data value added.
|
|
func (e Error) WithValue(name string, value any) Error {
|
|
return e.WithData(map[string]any{name: value})
|
|
}
|
|
|
|
// WriteJSON writes the error to the response as JSON.
|
|
func (e Error) WriteJSON(w http.ResponseWriter) error {
|
|
if e.StatusCode == 0 {
|
|
e.StatusCode = http.StatusOK
|
|
}
|
|
return WriteResponseJSON(w, e.StatusCode, e)
|
|
}
|
|
|
|
// NewError creates an Error, defaulting the message to the standard status
|
|
// text when empty.
|
|
func NewError(statusCode int, message string) Error {
|
|
if message == "" {
|
|
message = http.StatusText(statusCode)
|
|
}
|
|
return Error{StatusCode: statusCode, Message: message}
|
|
}
|
|
|
|
// WriteResponseJSON marshals data and writes it with the given status code.
|
|
func WriteResponseJSON(w http.ResponseWriter, statusCode int, data any) error {
|
|
b, err := json.Marshal(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
_, err = w.Write(b)
|
|
return err
|
|
}
|
|
|
|
// WriteErrorJSON writes err as JSON. An Error is written as-is; anything
|
|
// else is wrapped as a 500 with the message under data.error.
|
|
func WriteErrorJSON(w http.ResponseWriter, err error) error {
|
|
var e Error
|
|
if errors.As(err, &e) {
|
|
return e.WriteJSON(w)
|
|
}
|
|
return ErrInternalServerError.WithError(err).WriteJSON(w)
|
|
}
|