2026-09-01 14:43:06 +07:00

184 lines
3.8 KiB
Go

package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"sync/atomic"
)
type Task struct {
ID string `json:"id"`
Name string `json:"name"`
}
type createUpdateRequest struct {
Name string `json:"name"`
}
var (
store = make(map[string]Task)
mu sync.RWMutex
idSeq int64
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/tasks", handleTasks) // GET list + POST create
mux.HandleFunc("/tasks/", handleTaskByID) // GET/PUT/DELETE by id
fmt.Println("Server running on http://localhost:8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
panic(err)
}
}
func handleTasks(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
listTasks(w, r)
case http.MethodPost:
createTask(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func handleTaskByID(w http.ResponseWriter, r *http.Request) {
// Path: /tasks/{id}
id := strings.TrimPrefix(r.URL.Path, "/tasks/")
if id == "" || strings.Contains(id, "/") {
writeJSONError(w, "Invalid ID", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
getTask(w, r, id)
case http.MethodPut:
updateTask(w, r, id)
case http.MethodDelete:
deleteTask(w, r, id)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// ---------- Handlers ----------
func listTasks(w http.ResponseWriter, r *http.Request) {
mu.RLock()
defer mu.RUnlock()
// PENTING: selalu kembalikan slice kosong, bukan nil
tasks := make([]Task, 0, len(store))
for _, t := range store {
tasks = append(tasks, t)
}
writeJSON(w, tasks, http.StatusOK)
}
func createTask(w http.ResponseWriter, r *http.Request) {
var req createUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONError(w, "Invalid JSON body", http.StatusBadRequest)
return
}
if strings.TrimSpace(req.Name) == "" {
writeJSONError(w, "name is required and cannot be empty", http.StatusBadRequest)
return
}
// Generate ID (server-side only)
newID := strconv.FormatInt(atomic.AddInt64(&idSeq, 1), 10)
task := Task{
ID: newID,
Name: req.Name,
}
mu.Lock()
store[newID] = task
mu.Unlock()
writeJSON(w, task, http.StatusCreated)
}
func getTask(w http.ResponseWriter, r *http.Request, id string) {
mu.RLock()
task, exists := store[id]
mu.RUnlock()
if !exists {
writeJSONError(w, "Task not found", http.StatusNotFound)
return
}
writeJSON(w, task, http.StatusOK)
}
func updateTask(w http.ResponseWriter, r *http.Request, id string) {
var req createUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONError(w, "Invalid JSON body", http.StatusBadRequest)
return
}
if strings.TrimSpace(req.Name) == "" {
writeJSONError(w, "name is required and cannot be empty", http.StatusBadRequest)
return
}
mu.Lock()
defer mu.Unlock()
task, exists := store[id]
if !exists {
writeJSONError(w, "Task not found", http.StatusNotFound)
return
}
// Full replacement of name, ID remains the same
task.Name = req.Name
store[id] = task
writeJSON(w, task, http.StatusOK)
}
func deleteTask(w http.ResponseWriter, r *http.Request, id string) {
mu.Lock()
defer mu.Unlock()
if _, exists := store[id]; !exists {
writeJSONError(w, "Task not found", http.StatusNotFound)
return
}
delete(store, id)
// 204 No Content — JANGAN tulis body apapun
w.WriteHeader(http.StatusNoContent)
}
// ---------- Helper ----------
func writeJSON(w http.ResponseWriter, data interface{}, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeJSONError(w http.ResponseWriter, message string, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]string{
"error": message,
})
}