Files
zampler/internal/rest/rest.go
T
claudeandClaude Sonnet 5 800404203a
PR Checks / checks (pull_request) Successful in 1m43s
Build / build-and-push (push) Successful in 1m8s
replace archived code.aneur.in/go/* dependencies
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>
2026-09-07 17:00:45 +01:00

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)
}