feat(api): add HTTP server with full REST API
Chi router with all entity CRUD endpoints, promote/demote/use actions, tag listing. CORS middleware for --dev mode. Graceful shutdown on SIGINT/SIGTERM. 22 API integration tests via httptest.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/lerko/nib/internal/db"
|
||||
)
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type EntityResponse struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ModifiedAt string `json:"modified_at"`
|
||||
Body string `json:"body"`
|
||||
Glyph string `json:"glyph"`
|
||||
TimeAnchor *string `json:"time_anchor"`
|
||||
CompletedAt *string `json:"completed_at"`
|
||||
Pinned bool `json:"pinned"`
|
||||
DeletedAt *string `json:"deleted_at"`
|
||||
Tags []string `json:"tags"`
|
||||
CardType *string `json:"card_type"`
|
||||
CardData *string `json:"card_data"`
|
||||
UseCount int `json:"use_count"`
|
||||
LastUsedAt *string `json:"last_used_at"`
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, code, message string) {
|
||||
writeJSON(w, status, ErrorResponse{Error: code, Message: message})
|
||||
}
|
||||
|
||||
func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool {
|
||||
if err := json.NewDecoder(r.Body).Decode(dst); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid_input", "malformed JSON: "+err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func entityToResponse(e *db.Entity) EntityResponse {
|
||||
resp := EntityResponse{
|
||||
ID: e.ID,
|
||||
CreatedAt: e.CreatedAt.Format(time.RFC3339),
|
||||
ModifiedAt: e.ModifiedAt.Format(time.RFC3339),
|
||||
Body: e.Body,
|
||||
Glyph: string(e.Glyph),
|
||||
Pinned: e.Pinned,
|
||||
Tags: e.Tags,
|
||||
UseCount: e.UseCount,
|
||||
}
|
||||
if resp.Tags == nil {
|
||||
resp.Tags = []string{}
|
||||
}
|
||||
resp.TimeAnchor = e.TimeAnchor
|
||||
resp.CompletedAt = formatTimeRespPtr(e.CompletedAt)
|
||||
resp.DeletedAt = formatTimeRespPtr(e.DeletedAt)
|
||||
resp.LastUsedAt = formatTimeRespPtr(e.LastUsedAt)
|
||||
if e.CardType != nil {
|
||||
s := string(*e.CardType)
|
||||
resp.CardType = &s
|
||||
}
|
||||
resp.CardData = e.CardData
|
||||
return resp
|
||||
}
|
||||
|
||||
func formatTimeRespPtr(t *time.Time) *string {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
s := t.Format(time.RFC3339)
|
||||
return &s
|
||||
}
|
||||
Reference in New Issue
Block a user