package main import ( "context" "encoding/json" "errors" "fmt" "log" "net/http" "os" "os/signal" "slices" "strings" "sync/atomic" "syscall" "time" ) type server struct { fileServerHits atomic.Int32 } type chirpRequest struct { Body string `json:"body"` } type validationError struct { Err string `json:"error"` } type chirpResponse struct { CleanedBody string `json:"cleaned_body"` } func writeJsonResponse(w http.ResponseWriter, response any) { encoder := json.NewEncoder(w) if err := encoder.Encode(response); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (s *server) HandleFiles(prefix string) http.Handler { fileServer := http.StripPrefix(prefix, http.FileServer(http.Dir("."))) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.fileServerHits.Add(1) fileServer.ServeHTTP(w, r) }) } func (s *server) HandleHealthChecks(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) } func (s *server) HandleReset(w http.ResponseWriter, r *http.Request) { s.fileServerHits.Store(0) w.Header().Add("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) } func (s *server) HandleMetrics(w http.ResponseWriter, r *http.Request) { hits := s.fileServerHits.Load() w.Header().Add("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) template := `

Welcome, Chirpy Admin

Chirpy has been visited %d times!

` fmt.Fprintf(w, template, hits) } func (s *server) HandleChirpValidation(w http.ResponseWriter, r *http.Request) { blacklist := []string{ "kerfuffle", "sharbert", "fornax", } var chirpRequest chirpRequest defer r.Body.Close() w.Header().Add("Content-Type", "application/json") err := json.NewDecoder(r.Body).Decode(&chirpRequest) if err != nil { w.WriteHeader(http.StatusBadRequest) writeJsonResponse(w, validationError{ Err: "Unable to deserialize JSON", }) return } if len(chirpRequest.Body) > 140 { w.WriteHeader(http.StatusBadRequest) writeJsonResponse(w, validationError{ Err: "Chirp is too long", }) return } words := strings.Split(chirpRequest.Body, " ") sanitized := []string{} for _, word := range words { if slices.Contains(blacklist, strings.ToLower(word)) { sanitized = append(sanitized, "****") continue } sanitized = append(sanitized, word) } cleanedBody := strings.Join(sanitized, " ") w.WriteHeader(http.StatusOK) writeJsonResponse(w, chirpResponse{CleanedBody: cleanedBody}) } func main() { server := &server{} mux := http.NewServeMux() appRoot := "/app/" mux.Handle(appRoot, server.HandleFiles(appRoot)) mux.HandleFunc("GET /admin/metrics", server.HandleMetrics) mux.HandleFunc("POST /admin/reset", server.HandleReset) mux.HandleFunc("POST /api/validate_chirp", server.HandleChirpValidation) mux.HandleFunc("GET /api/healthz", server.HandleHealthChecks) port := ":8080" httpServer := http.Server{ Handler: mux, Addr: port, } go func() { log.Printf("Server started and listening on %s\n", port) err := httpServer.ListenAndServe() if !errors.Is(err, http.ErrServerClosed) { log.Fatalf("HTTP Server error: %v", err) } log.Println("Stopped serving new connections") }() sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) <-sigChan shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) defer shutdownRelease() if err := httpServer.Shutdown(shutdownCtx); err != nil { log.Fatalf("HTTP shutdown error: %v", err) } log.Println("Graceful shutdown complete") }