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>
100 lines
2.8 KiB
Go
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
|
|
}
|
|
}
|