/
/
1 package main 2
3 import ( 4 "encoding/json" 5 "net/http" 6 "time" 7
8 "github.com/go-chi/chi/v5" 9 "github.com/google/uuid" 10 ) 11
12 // @gitian:note Handler holds a reference to the store and exposes HTTP methods 13 type Handler struct { 14 store *Store 15 } 16
17 func NewHandler(s *Store) *Handler { 18 return &Handler{store: s} 19 } 20
21 /* @gitian 22 * --group=api 23 * List returns all todos, optionally filtered by status. 24 * Accepts ?status=active or ?status=completed as query params. 25 */ 26 func (h *Handler) List(w http.ResponseWriter, r *http.Request) { 27 status := r.URL.Query().Get("status") 28 todos := h.store.All() 29
30 // @gitian:perf Full table scan with in-memory filter — fine for small 31 // datasets but should use indexed queries past ~10k todos 32 if status != "" { 33 filtered := make([]Todo, 0, len(todos)) 34 for _, t := range todos { 35 if status == "completed" && t.Completed { 36 filtered = append(filtered, t) 37 } else if status == "active" && !t.Completed { 38 filtered = append(filtered, t) 39 } 40 } 41 todos = filtered 42 } 43
44 writeJSON(w, http.StatusOK, todos) 45 } 46
47 /* @gitian 48 * --group=api 49 * Create validates the request body and inserts a new todo. 50 * Returns 201 with the created todo on success. 51 */ 52 func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { 53 var req struct { 54 Text string `json:"text"` 55 Priority string `json:"priority"` 56 } 57 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 58 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) 59 return 60 } 61
62 // @gitian:security Validate input length to prevent storage abuse 63 if len(req.Text) == 0 || len(req.Text) > 500 { 64 writeJSON(w, http.StatusBadRequest, map[string]string{ 65 "error": "text must be 1-500 characters", 66 }) 67 return 68 } 69
70 priority := req.Priority 71 if priority == "" { 72 priority = "medium" 73 } 74
75 todo := Todo{ 76 ID: uuid.New().String(), 77 Text: req.Text, 78 Completed: false, 79 CreatedAt: time.Now(), 80 Priority: priority, 81 } 82
83 h.store.Insert(todo) 84 writeJSON(w, http.StatusCreated, todo) 85 } 86
87 /* @gitian:warning 88 * --group=api 89 * Get looks up a single todo by ID. Returns 404 if the ID 90 * doesn't exist — no distinction between "never existed" 91 * and "was deleted", which may confuse API consumers. 92 */ 93 func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { 94 id := chi.URLParam(r, "id") 95 todo, ok := h.store.Find(id) 96 if !ok { 97 writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) 98 return 99 } 100 writeJSON(w, http.StatusOK, todo) 101 } 102
103 /* @gitian 104 * --group=api 105 * Update applies partial changes to an existing todo. 106 * Only the fields present in the request body are updated. 107 */ 108 func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { 109 id := chi.URLParam(r, "id") 110 todo, ok := h.store.Find(id) 111 if !ok { 112 writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) 113 return 114 } 115
116 // @gitian:todo Support JSON Merge Patch (RFC 7396) for partial updates 117 var patch struct { 118 Text *string `json:"text"` 119 Completed *bool `json:"completed"` 120 Priority *string `json:"priority"` 121 } 122 if err := json.NewDecoder(r.Body).Decode(&patch); err != nil { 123 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) 124 return 125 } 126
127 if patch.Text != nil { 128 todo.Text = *patch.Text 129 } 130 if patch.Completed != nil { 131 todo.Completed = *patch.Completed 132 } 133 if patch.Priority != nil { 134 todo.Priority = *patch.Priority 135 } 136
137 h.store.Update(todo) 138 writeJSON(w, http.StatusOK, todo) 139 } 140
141 // @gitian Delete removes a todo by ID. Idempotent — deleting a 142 // non-existent ID returns 204 without error. 143 func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { 144 id := chi.URLParam(r, "id") 145 h.store.Remove(id) 146 w.WriteHeader(http.StatusNoContent) 147 } 148
149 func writeJSON(w http.ResponseWriter, status int, v any) { 150 w.Header().Set("Content-Type", "application/json") 151 w.WriteHeader(status) 152 json.NewEncoder(w).Encode(v) 153 } 154