Files
2026-10-01 00:27:59 -04:00

204 lines
4.4 KiB
Go

package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"github.com/google/uuid"
)
type Task struct {
ID string `json:"id"`
Name string `json:"name"`
}
type TaskStore struct {
mu sync.RWMutex
tasks map[string]Task
}
func NewTaskStore() *TaskStore {
return &TaskStore{
tasks: make(map[string]Task),
}
}
func (s *TaskStore) Create(name string) Task {
s.mu.Lock()
defer s.mu.Unlock()
id := uuid.New().String()
task := Task{
ID: id,
Name: name,
}
s.tasks[id] = task
return task
}
func (s *TaskStore) GetAll() []Task {
s.mu.RLock()
defer s.mu.RUnlock()
// Guarantee an initialized, non-nil slice to serialize as [] instead of null
taskList := make([]Task, 0, len(s.tasks))
for _, task := range s.tasks {
taskList = append(taskList, task)
}
return taskList
}
func (s *TaskStore) GetByID(id string) (Task, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
task, exists := s.tasks[id]
return task, exists
}
func (s *TaskStore) Update(id string, name string) (Task, bool) {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.tasks[id]; !exists {
return Task{}, false
}
task := Task{
ID: id,
Name: name,
}
s.tasks[id] = task
return task, true
}
func (s *TaskStore) Delete(id string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.tasks[id]; !exists {
return false
}
delete(s.tasks, id)
return true
}
type Server struct {
store *TaskStore
}
func writeJSONResponse(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if data != nil {
json.NewEncoder(w).Encode(data)
}
}
func writeJSONError(w http.ResponseWriter, statusCode int, message string) {
writeJSONResponse(w, statusCode, map[string]string{"error": message})
}
// Handler for /tasks (POST, GET)
func (s *Server) handleTasks(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
tasks := s.store.GetAll()
writeJSONResponse(w, http.StatusOK, tasks)
case http.MethodPost:
var req struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONError(w, http.StatusBadRequest, "Invalid JSON body")
return
}
if strings.TrimSpace(req.Name) == "" {
writeJSONError(w, http.StatusBadRequest, "Name field is required and cannot be empty")
return
}
task := s.store.Create(req.Name)
writeJSONResponse(w, http.StatusCreated, task)
default:
writeJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
// Handler for /tasks/{id} (GET, PUT, DELETE)
func (s *Server) handleTaskByID(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/tasks/")
if id == "" || strings.Contains(id, "/") {
writeJSONError(w, http.StatusBadRequest, "Invalid ID path")
return
}
switch r.Method {
case http.MethodGet:
task, found := s.store.GetByID(id)
if !found {
writeJSONError(w, http.StatusNotFound, "Task not found")
return
}
writeJSONResponse(w, http.StatusOK, task)
case http.MethodPut:
var req struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONError(w, http.StatusBadRequest, "Invalid JSON body")
return
}
if strings.TrimSpace(req.Name) == "" {
writeJSONError(w, http.StatusBadRequest, "Name field is required and cannot be empty")
return
}
task, updated := s.store.Update(id, req.Name)
if !updated {
writeJSONError(w, http.StatusNotFound, "Task not found")
return
}
writeJSONResponse(w, http.StatusOK, task)
case http.MethodDelete:
deleted := s.store.Delete(id)
if !deleted {
writeJSONError(w, http.StatusNotFound, "Task not found")
return
}
// Must output exactly 204 with no body
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNoContent)
default:
writeJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
func main() {
server := &Server{
store: NewTaskStore(),
}
mux := http.NewServeMux()
mux.HandleFunc("/tasks", server.handleTasks)
mux.HandleFunc("/tasks/", server.handleTaskByID)
fmt.Println("Server running on port 8080...")
if err := http.ListenAndServe(":8080", mux); err != nil {
panic(err)
}
}