package http import ( "context" "encoding/json" "errors" "fmt" "net/http" "strconv" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "go.uber.org/zap" ) type LogStorage interface { Get(ctx context.Context, first, second uint64) (bool, error) List(ctx context.Context) (string, error) } type Response struct { Dupes *bool `json:"dupes,omitempty"` Err string `json:"error,omitempty"` } func NewService(host string, port uint, log LogStorage, logger *zap.Logger) (*Service, error) { srv := http.Server{ Addr: fmt.Sprintf("%s:%d", host, port), ReadTimeout: time.Second, ReadHeaderTimeout: time.Millisecond * 500, WriteTimeout: time.Second * 10, IdleTimeout: time.Minute, MaxHeaderBytes: 1024 * 16, } service := Service{ server: &srv, log: log, logger: logger, } mux := chi.NewMux() mux.Use(middleware.StripSlashes) mux.Get("/{firstID:[0-9]+}/{secondID:[0-9]+}", service.handleDup) mux.Get("/list", service.List) srv.Handler = mux return &service, nil } type Service struct { server *http.Server log LogStorage logger *zap.Logger } func (s *Service) Start() { s.logger.Info("starting http service", zap.String("address", s.server.Addr)) if err := s.server.ListenAndServe(); err != nil { if !errors.Is(err, http.ErrServerClosed) { s.logger.Error("listen and serve failed", zap.Error(err)) } } } func (s *Service) Stop(ctx context.Context) { if err := s.server.Shutdown(ctx); err != nil { s.logger.Error("the graceful shutdown has failed", zap.Error(err)) } } func (s *Service) handleDup(w http.ResponseWriter, r *http.Request) { started := time.Now() firstIDStr := chi.URLParam(r, "firstID") firstID, err := strconv.ParseUint(firstIDStr, 10, 64) if err != nil { s.badRequest(w, Response{Err: fmt.Sprintf("invalid first id value: %s", firstIDStr)}) return } secondIDStr := chi.URLParam(r, "secondID") secondID, err := strconv.ParseUint(secondIDStr, 10, 64) if err != nil { s.badRequest(w, Response{Err: fmt.Sprintf("invalid second id value: %s", secondIDStr)}) return } logger := s.logger.With(zap.Uint64("first_id", firstID), zap.Uint64("second_id", secondID)) logger.Debug("new dup request") if secondID == firstID { t := true s.writeResponse(w, Response{Dupes: &t}, http.StatusOK) return } result, err := s.log.Get(r.Context(), firstID, secondID) if err != nil { logger.Error("log storage get failed", zap.Error(err)) s.internalError(w, Response{Err: fmt.Sprintf("storage error")}) return } logger.Debug("result", zap.Bool("dup", result)) w.Header().Add("Server-Timing", fmt.Sprintf("db;dur=%dus", time.Since(started).Microseconds())) s.writeResponse(w, Response{Dupes: &result}, http.StatusOK) } func (s *Service) badRequest(w http.ResponseWriter, response Response) { s.writeResponse(w, response, http.StatusBadRequest) } func (s *Service) internalError(w http.ResponseWriter, response Response) { s.writeResponse(w, response, http.StatusInternalServerError) } func (s *Service) writeResponse(w http.ResponseWriter, response Response, code int) { w.WriteHeader(code) err := json.NewEncoder(w).Encode(response) if err != nil { s.logger.Error("encode bad request response", zap.Error(err), zap.Any("response", response)) } } func (s *Service) List(w http.ResponseWriter, r *http.Request) { list, err := s.log.List(r.Context()) if err != nil { s.internalError(w, Response{Err: err.Error()}) return } _, _ = w.Write([]byte(list)) }