rdev/pkg/api/response.go
jordan 4a042a8b71 feat: Add rdev-api Go server with OpenAPI docs
Implements a fully documented API server following the aeries chassis pattern:

- pkg/api: Simplified chassis with App, Response helpers, and OpenAPI builder
- cmd/rdev-api: Entry point with full OpenAPI spec for all v0.4 endpoints
- internal/handlers: Stubbed project handlers (list, get, claude, shell, git, events)

Endpoints:
- GET  /health, /ready     - Health checks
- GET  /docs, /openapi.json - Scalar API docs
- GET  /projects           - List projects
- GET  /projects/{id}      - Get project
- POST /projects/{id}/claude, shell, git - Run commands
- GET  /projects/{id}/events - SSE streaming

Uses Scalar for dark-mode API documentation at /docs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 20:56:27 -07:00

100 lines
2.8 KiB
Go

package api
import (
"encoding/json"
"net/http"
"time"
"github.com/go-chi/chi/v5/middleware"
)
// Response is the standard envelope for all API responses.
type Response struct {
Data any `json:"data,omitempty"`
Error *Error `json:"error,omitempty"`
Meta Meta `json:"meta"`
}
// Error represents an API error.
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Details []any `json:"details,omitempty"`
}
// Meta contains response metadata.
type Meta struct {
RequestID string `json:"request_id,omitempty"`
Timestamp string `json:"timestamp"`
}
// newMeta creates a Meta with current timestamp and request ID from context.
func newMeta(r *http.Request) Meta {
return Meta{
RequestID: middleware.GetReqID(r.Context()),
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
}
// WriteJSON writes a JSON response with the given status code.
func WriteJSON(w http.ResponseWriter, r *http.Request, status int, data any) {
resp := Response{
Data: data,
Meta: newMeta(r),
}
writeResponse(w, status, resp)
}
// WriteSuccess writes a successful JSON response with status 200 OK.
func WriteSuccess(w http.ResponseWriter, r *http.Request, data any) {
WriteJSON(w, r, http.StatusOK, data)
}
// WriteCreated writes a successful JSON response with status 201 Created.
func WriteCreated(w http.ResponseWriter, r *http.Request, data any) {
WriteJSON(w, r, http.StatusCreated, data)
}
// WriteNoContent writes a successful response with status 204 No Content.
func WriteNoContent(w http.ResponseWriter) {
w.WriteHeader(http.StatusNoContent)
}
// WriteError writes an error response with the given status code.
func WriteError(w http.ResponseWriter, r *http.Request, status int, code, message string, details ...any) {
resp := Response{
Error: &Error{
Code: code,
Message: message,
Details: details,
},
Meta: newMeta(r),
}
writeResponse(w, status, resp)
}
// WriteBadRequest writes a 400 Bad Request error response.
func WriteBadRequest(w http.ResponseWriter, r *http.Request, message string, details ...any) {
WriteError(w, r, http.StatusBadRequest, "BAD_REQUEST", message, details...)
}
// WriteNotFound writes a 404 Not Found error response.
func WriteNotFound(w http.ResponseWriter, r *http.Request, message string) {
WriteError(w, r, http.StatusNotFound, "NOT_FOUND", message)
}
// WriteInternalError writes a 500 Internal Server Error response.
func WriteInternalError(w http.ResponseWriter, r *http.Request, message string) {
WriteError(w, r, http.StatusInternalServerError, "INTERNAL_ERROR", message)
}
// writeResponse marshals and writes the response.
func writeResponse(w http.ResponseWriter, status int, resp Response) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(resp); err != nil {
return
}
}