2026-09-02 07:55:42 +07:00

242 lines
5.2 KiB
Go

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)
}
}