package main import ( "encoding/json" "fmt" "log" "net/http" "strconv" "strings" "sync" "sync/atomic" "github.com/prometheus/client_golang/prometheus/promhttp" ) // --------------------------------------------------------------------------- // Domain model // --------------------------------------------------------------------------- type Item struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` } type ItemRequest struct { Name string `json:"name"` Description string `json:"description"` } // --------------------------------------------------------------------------- // In-memory store // --------------------------------------------------------------------------- var ( store sync.Map counter atomic.Int64 ) func nextID() int { return int(counter.Add(1)) } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func itemIDFromPath(path string) (int, bool) { parts := strings.Split(strings.TrimSuffix(path, "/"), "/") if len(parts) < 3 { return 0, false } id, err := strconv.Atoi(parts[len(parts)-1]) return id, err == nil } // --------------------------------------------------------------------------- // Handlers // --------------------------------------------------------------------------- func healthHandler(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "UP"}) } func itemsHandler(w http.ResponseWriter, r *http.Request) { // Route: /api/items or /api/items/{id} hasSub := strings.Count(strings.TrimSuffix(r.URL.Path, "/"), "/") >= 3 if hasSub { id, ok := itemIDFromPath(r.URL.Path) if !ok { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"}) return } switch r.Method { case http.MethodGet: v, exists := store.Load(id) if !exists { writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) return } writeJSON(w, http.StatusOK, v) case http.MethodPut: var req ItemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } existingRaw, exists := store.Load(id) if !exists { writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) return } existing := existingRaw.(Item) if req.Name == "" { req.Name = existing.Name } if req.Description == "" { req.Description = existing.Description } updated := Item{ID: id, Name: req.Name, Description: req.Description} store.Store(id, updated) writeJSON(w, http.StatusOK, updated) case http.MethodDelete: store.Delete(id) writeJSON(w, http.StatusOK, map[string]int{"deleted": id}) default: w.WriteHeader(http.StatusMethodNotAllowed) } return } // /api/items — list or create switch r.Method { case http.MethodGet: var items []Item store.Range(func(_, v any) bool { items = append(items, v.(Item)) return true }) if items == nil { items = []Item{} } writeJSON(w, http.StatusOK, items) case http.MethodPost: var req ItemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } if req.Name == "" { req.Name = "unnamed" } item := Item{ID: nextID(), Name: req.Name, Description: req.Description} store.Store(item.ID, item) writeJSON(w, http.StatusCreated, item) default: w.WriteHeader(http.StatusMethodNotAllowed) } } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- func main() { mux := http.NewServeMux() mux.HandleFunc("/health", healthHandler) mux.Handle("/metrics", promhttp.Handler()) mux.HandleFunc("/api/items/", itemsHandler) mux.HandleFunc("/api/items", itemsHandler) addr := ":8080" fmt.Printf("tmpl-test-go listening on %s\n", addr) log.Fatal(http.ListenAndServe(addr, mux)) }