package main import ( "crypto/rand" "encoding/json" "fmt" "log" "net/http" "os" "strings" "sync" ) // Task represents a single to-do item. // Exactly two fields as required by the assignment schema. type Task struct { ID string `json:"id"` Name string `json:"name"` } // store is a thread-safe in-memory collection of tasks. type store struct { mu sync.Mutex tasks map[string]Task } func newStore() *store { return &store{ tasks: make(map[string]Task), } } // generateID creates a random hex string to use as a task ID. func generateID() string { b := make([]byte, 8) if _, err := rand.Read(b); err != nil { // Extremely unlikely, but fall back to something still unique-ish. return fmt.Sprintf("id-%p", b) } return fmt.Sprintf("%x", b) } func (s *store) create(name string) Task { s.mu.Lock() defer s.mu.Unlock() id := generateID() // Guard against the (astronomically unlikely) case of a collision. for { if _, exists := s.tasks[id]; !exists { break } id = generateID() } t := Task{ID: id, Name: name} s.tasks[id] = t return t } func (s *store) list() []Task { s.mu.Lock() defer s.mu.Unlock() // IMPORTANT: initialize as an empty slice, never a nil slice. // A nil slice marshals to `null`, an empty slice marshals to `[]`. result := []Task{} for _, t := range s.tasks { result = append(result, t) } return result } func (s *store) get(id string) (Task, bool) { s.mu.Lock() defer s.mu.Unlock() t, ok := s.tasks[id] return t, ok } func (s *store) update(id, name string) (Task, bool) { s.mu.Lock() defer s.mu.Unlock() t, ok := s.tasks[id] if !ok { return Task{}, false } t.Name = name s.tasks[id] = t return t, true } func (s *store) delete(id string) bool { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.tasks[id]; !ok { return false } delete(s.tasks, id) return true } // ---- HTTP helpers ---- func writeJSON(w http.ResponseWriter, status int, body interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if body != nil { // Encoding a well-known struct/slice here never fails in practice, // so we don't need to handle an error beyond logging. if err := json.NewEncoder(w).Encode(body); err != nil { log.Printf("failed to encode response: %v", err) } } } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]string{"error": message}) } type createOrUpdateRequest struct { ID string `json:"id"` Name string `json:"name"` } // ---- Handlers ---- type api struct { s *store } // handleTasks handles requests to /tasks (no id). func (a *api) handleTasks(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: writeJSON(w, http.StatusOK, a.s.list()) case http.MethodPost: a.createTask(w, r) default: writeError(w, http.StatusMethodNotAllowed, "method not allowed") } } // handleTaskByID handles requests to /tasks/{id}. func (a *api) handleTaskByID(w http.ResponseWriter, r *http.Request) { id := strings.TrimPrefix(r.URL.Path, "/tasks/") id = strings.Trim(id, "/") if id == "" { writeError(w, http.StatusNotFound, "task id required") return } switch r.Method { case http.MethodGet: a.getTask(w, id) case http.MethodPut: a.updateTask(w, r, id) case http.MethodDelete: a.deleteTask(w, id) default: writeError(w, http.StatusMethodNotAllowed, "method not allowed") } } func (a *api) createTask(w http.ResponseWriter, r *http.Request) { var req createOrUpdateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON body") return } // The client may not supply an id; if they do, it's ignored and // a fresh one is generated regardless. if strings.TrimSpace(req.Name) == "" { writeError(w, http.StatusBadRequest, "name is required") return } t := a.s.create(req.Name) writeJSON(w, http.StatusCreated, t) } func (a *api) getTask(w http.ResponseWriter, id string) { t, ok := a.s.get(id) if !ok { writeError(w, http.StatusNotFound, "task not found") return } writeJSON(w, http.StatusOK, t) } func (a *api) updateTask(w http.ResponseWriter, r *http.Request, id string) { var req createOrUpdateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON body") return } if strings.TrimSpace(req.Name) == "" { writeError(w, http.StatusBadRequest, "name is required") return } t, ok := a.s.update(id, req.Name) if !ok { writeError(w, http.StatusNotFound, "task not found") return } writeJSON(w, http.StatusOK, t) } func (a *api) deleteTask(w http.ResponseWriter, id string) { if !a.s.delete(id) { writeError(w, http.StatusNotFound, "task not found") return } // 204 No Content: no body, no Content-Type needed (rubric excludes it). w.WriteHeader(http.StatusNoContent) } func main() { port := os.Getenv("PORT") if port == "" { port = "8080" } a := &api{s: newStore()} mux := http.NewServeMux() mux.HandleFunc("/tasks", a.handleTasks) mux.HandleFunc("/tasks/", a.handleTaskByID) addr := "localhost:" + port log.Printf("Task Manager API listening on http://%s", addr) if err := http.ListenAndServe(addr, mux); err != nil { log.Fatal(err) } }