cronnoss /
tk-api
| 1 | package internalhttp |
||
| 2 | |||
| 3 | import ( |
||
| 4 | "context" |
||
| 5 | "encoding/json" |
||
| 6 | "fmt" |
||
| 7 | "io" |
||
| 8 | "net" |
||
| 9 | "net/http" |
||
| 10 | "time" |
||
| 11 | |||
| 12 | _ "github.com/cronnoss/tickets-api/docs" // nolint: revive |
||
| 13 | "github.com/cronnoss/tk-api/internal/common/srv" |
||
| 14 | "github.com/cronnoss/tk-api/internal/model" |
||
| 15 | "github.com/cronnoss/tk-api/internal/server" |
||
| 16 | "github.com/cronnoss/tk-api/internal/storage/models" |
||
| 17 | "github.com/gorilla/mux" |
||
| 18 | httpSwagger "github.com/swaggo/http-swagger" |
||
| 19 | ) |
||
| 20 | |||
| 21 | type ctxKeyID int |
||
| 22 | |||
| 23 | const ( |
||
| 24 | KeyLoggerID ctxKeyID = iota |
||
| 25 | ) |
||
| 26 | |||
| 27 | type Server struct { |
||
| 28 | srv http.Server |
||
| 29 | app server.Application |
||
| 30 | log Logger |
||
| 31 | host string |
||
| 32 | port string |
||
| 33 | } |
||
| 34 | |||
| 35 | type Logger interface { |
||
| 36 | Fatalf(format string, a ...interface{}) |
||
| 37 | Errorf(format string, a ...interface{}) |
||
| 38 | Warningf(format string, a ...interface{}) |
||
| 39 | Infof(format string, a ...interface{}) |
||
| 40 | Debugf(format string, a ...interface{}) |
||
| 41 | } |
||
| 42 | |||
| 43 | func NewServer(log Logger, app server.Application, host, port string) *Server { |
||
| 44 | return &Server{log: log, app: app, host: host, port: port} |
||
| 45 | } |
||
| 46 | |||
| 47 | func (s *Server) helperDecode(stream io.ReadCloser, w http.ResponseWriter, data interface{}) error { // nolint: unused |
||
| 48 | decoder := json.NewDecoder(stream) |
||
| 49 | if err := decoder.Decode(&data); err != nil { |
||
| 50 | s.log.Errorf("Can't decode json:%v\n", err) |
||
| 51 | w.WriteHeader(http.StatusBadRequest) |
||
| 52 | w.Write([]byte(fmt.Sprintf("{\"error\": \"Can't decode json:%v\"}\n", err))) |
||
| 53 | return err |
||
| 54 | } |
||
| 55 | return nil |
||
| 56 | } |
||
| 57 | |||
| 58 | // @Summary Get shows |
||
| 59 | // @Tags shows |
||
| 60 | // @Description Get shows from remote API and store them in the local service |
||
| 61 | // @ID get-shows |
||
| 62 | // @Accept json |
||
| 63 | // @Produce json |
||
| 64 | // @Success 200 {object} ShowListResponse |
||
| 65 | // @Failure 400,404 {object} server.ErrorResponse |
||
| 66 | // @Failure 500 {object} server.ErrorResponse |
||
| 67 | // @Router /shows [get]. |
||
| 68 | func (s *Server) GetShows(w http.ResponseWriter, r *http.Request) { |
||
| 69 | // Step 1: Make a GET request to the remote API |
||
| 70 | remoteURL := "https://leadbook.ru/test-task-api/shows" |
||
| 71 | req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, remoteURL, nil) |
||
| 72 | if err != nil { |
||
| 73 | srv.RespondWithError(fmt.Errorf("failed to create request: %w", err), w, r) |
||
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
| 74 | return |
||
| 75 | } |
||
| 76 | resp, err := http.DefaultClient.Do(req) |
||
| 77 | if err != nil { |
||
| 78 | srv.RespondWithError(fmt.Errorf("failed to do request: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 79 | return |
||
| 80 | } |
||
| 81 | defer resp.Body.Close() |
||
| 82 | |||
| 83 | // Step 2: Decode the response |
||
| 84 | body, err := io.ReadAll(resp.Body) |
||
| 85 | if err != nil { |
||
| 86 | srv.RespondWithError(fmt.Errorf("failed to read response body: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 87 | return |
||
| 88 | } |
||
| 89 | |||
| 90 | var showListResponse model.ShowListResponse |
||
| 91 | if err := json.Unmarshal(body, &showListResponse); err != nil { |
||
| 92 | srv.RespondWithError(fmt.Errorf("failed to decode response: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 93 | return |
||
| 94 | } |
||
| 95 | |||
| 96 | // Step 3: Iterate over shows and store them in the local service |
||
| 97 | for _, show := range showListResponse.Response { |
||
| 98 | if err := showListResponse.ShowListResponseValidate(); err != nil { |
||
| 99 | srv.RespondWithError(fmt.Errorf("failed to validate response: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 100 | return |
||
| 101 | } |
||
| 102 | |||
| 103 | _, err := s.app.CreateShow(r.Context(), models.Show{ |
||
| 104 | ID: show.ID, |
||
| 105 | Name: show.Name, |
||
| 106 | }) |
||
| 107 | if err != nil { |
||
| 108 | srv.RespondWithError(fmt.Errorf("failed to create show: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 109 | return |
||
| 110 | } |
||
| 111 | } |
||
| 112 | |||
| 113 | srv.RespondOK(showListResponse.Response, w, r) |
||
| 114 | } |
||
| 115 | |||
| 116 | // @Summary Get events |
||
| 117 | // @Tags events |
||
| 118 | // @Description Get events by show ID |
||
| 119 | // @ID get-events |
||
| 120 | // @Accept json |
||
| 121 | // @Produce json |
||
| 122 | // @Param id path int true "show ID" |
||
| 123 | // @Success 200 {object} EventListResponse |
||
| 124 | // @Failure 400,404 {object} server.ErrorResponse |
||
| 125 | // @Failure 500 {object} server.ErrorResponse |
||
| 126 | // @Router /shows/{id}/events [get]. |
||
| 127 | func (s *Server) GetEvents(w http.ResponseWriter, r *http.Request) { |
||
| 128 | // Step 1: Make a GET request to the remote API |
||
| 129 | vars := mux.Vars(r) |
||
| 130 | id := vars["id"] |
||
| 131 | remoteURL := "https://leadbook.ru/test-task-api/shows/" + id + "/events" |
||
| 132 | req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, remoteURL, nil) |
||
| 133 | if err != nil { |
||
| 134 | srv.RespondWithError(fmt.Errorf("failed to create request: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 135 | return |
||
| 136 | } |
||
| 137 | resp, err := http.DefaultClient.Do(req) |
||
| 138 | if err != nil { |
||
| 139 | srv.RespondWithError(fmt.Errorf("failed to do request: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 140 | return |
||
| 141 | } |
||
| 142 | defer resp.Body.Close() |
||
| 143 | |||
| 144 | // Step 2: Decode the response |
||
| 145 | body, err := io.ReadAll(resp.Body) |
||
| 146 | if err != nil { |
||
| 147 | srv.RespondWithError(fmt.Errorf("failed to read response body: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 148 | return |
||
| 149 | } |
||
| 150 | |||
| 151 | var eventListResponse model.EventListResponse |
||
| 152 | if err := json.Unmarshal(body, &eventListResponse); err != nil { |
||
| 153 | srv.RespondWithError(fmt.Errorf("failed to decode response: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 154 | return |
||
| 155 | } |
||
| 156 | |||
| 157 | // Step 3: Iterate over events and store them in the local service |
||
| 158 | for _, event := range eventListResponse.Response { |
||
| 159 | if err := eventListResponse.EventListResponseValidate(); err != nil { |
||
| 160 | srv.RespondWithError(fmt.Errorf("failed to validate response: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 161 | return |
||
| 162 | } |
||
| 163 | |||
| 164 | _, err := s.app.CreateEvent(r.Context(), models.Event{ |
||
| 165 | ID: event.ID, |
||
| 166 | ShowID: event.ShowID, |
||
| 167 | Date: event.Date, |
||
| 168 | }) |
||
| 169 | if err != nil { |
||
| 170 | srv.RespondWithError(fmt.Errorf("failed to create event: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 171 | return |
||
| 172 | } |
||
| 173 | } |
||
| 174 | |||
| 175 | srv.RespondOK(eventListResponse.Response, w, r) |
||
| 176 | } |
||
| 177 | |||
| 178 | // @Summary Get places |
||
| 179 | // @Tags places |
||
| 180 | // @Description Get places by event ID |
||
| 181 | // @ID get-places |
||
| 182 | // @Accept json |
||
| 183 | // @Produce json |
||
| 184 | // @Param id path int true "event ID" |
||
| 185 | // @Success 200 {object} PlaceListResponse |
||
| 186 | // @Failure 400,404 {object} server.ErrorResponse |
||
| 187 | // @Failure 500 {object} server.ErrorResponse |
||
| 188 | // @Router /events/{id}/places [get]. |
||
| 189 | func (s *Server) GetPlaces(w http.ResponseWriter, r *http.Request) { |
||
| 190 | // Step 1: Make a GET request to the remote API |
||
| 191 | vars := mux.Vars(r) |
||
| 192 | id := vars["id"] |
||
| 193 | remoteURL := "https://leadbook.ru/test-task-api/events/" + id + "/places" |
||
| 194 | req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, remoteURL, nil) |
||
| 195 | if err != nil { |
||
| 196 | srv.RespondWithError(fmt.Errorf("failed to create request: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 197 | return |
||
| 198 | } |
||
| 199 | resp, err := http.DefaultClient.Do(req) |
||
| 200 | if err != nil { |
||
| 201 | srv.RespondWithError(fmt.Errorf("failed to do request: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 202 | return |
||
| 203 | } |
||
| 204 | defer resp.Body.Close() |
||
| 205 | |||
| 206 | // Step 2: Decode the response |
||
| 207 | body, err := io.ReadAll(resp.Body) |
||
| 208 | if err != nil { |
||
| 209 | srv.RespondWithError(fmt.Errorf("failed to read response body: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 210 | return |
||
| 211 | } |
||
| 212 | |||
| 213 | var placeListResponse model.PlaceListResponse |
||
| 214 | if err := json.Unmarshal(body, &placeListResponse); err != nil { |
||
| 215 | srv.RespondWithError(fmt.Errorf("failed to decode response: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 216 | return |
||
| 217 | } |
||
| 218 | |||
| 219 | // Step 3: Iterate over places and store them in the local service |
||
| 220 | for _, place := range placeListResponse.Response { |
||
| 221 | if err := placeListResponse.PlaceListResponseValidate(); err != nil { |
||
| 222 | srv.RespondWithError(fmt.Errorf("failed to validate response: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 223 | return |
||
| 224 | } |
||
| 225 | |||
| 226 | _, err := s.app.CreatePlace(r.Context(), models.Place{ |
||
| 227 | ID: place.ID, |
||
| 228 | X: place.X, |
||
| 229 | Y: place.Y, |
||
| 230 | Width: place.Width, |
||
| 231 | Height: place.Height, |
||
| 232 | IsAvailable: place.IsAvailable, |
||
| 233 | }) |
||
| 234 | if err != nil { |
||
| 235 | srv.RespondWithError(fmt.Errorf("failed to create place: %w", err), w, r) |
||
|
0 ignored issues
–
show
|
|||
| 236 | return |
||
| 237 | } |
||
| 238 | } |
||
| 239 | |||
| 240 | srv.RespondOK(placeListResponse.Response, w, r) |
||
| 241 | } |
||
| 242 | |||
| 243 | // @title Ticket API |
||
| 244 | // @version 1 |
||
| 245 | // @description API Server for remote Tickets Application. |
||
| 246 | func (s *Server) Start(ctx context.Context) error { |
||
| 247 | addr := net.JoinHostPort(s.host, s.port) |
||
| 248 | midLogger := NewMiddlewareLogger() |
||
| 249 | |||
| 250 | router := mux.NewRouter() |
||
| 251 | |||
| 252 | router.Handle("/healthz", midLogger.setCommonHeadersMiddleware( |
||
| 253 | midLogger.loggingMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||
| 254 | w.WriteHeader(http.StatusOK) |
||
| 255 | w.Write([]byte("OK healthz\n")) |
||
| 256 | })))) |
||
| 257 | |||
| 258 | router.Handle("/readiness", midLogger.setCommonHeadersMiddleware( |
||
| 259 | midLogger.loggingMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||
| 260 | w.WriteHeader(http.StatusOK) |
||
| 261 | w.Write([]byte("OK readiness\n")) |
||
| 262 | })))) |
||
| 263 | |||
| 264 | router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler) |
||
| 265 | |||
| 266 | router.Handle("/shows", midLogger.setCommonHeadersMiddleware( |
||
| 267 | midLogger.loggingMiddleware(http.HandlerFunc(s.GetShows)))) |
||
| 268 | router.Handle("/shows/{id:[0-9]+}/events", midLogger.setCommonHeadersMiddleware( |
||
| 269 | midLogger.loggingMiddleware(http.HandlerFunc(s.GetEvents)))) |
||
| 270 | router.Handle("/events/{id:[0-9]+}/places", midLogger.setCommonHeadersMiddleware( |
||
| 271 | midLogger.loggingMiddleware(http.HandlerFunc(s.GetPlaces)))) |
||
| 272 | |||
| 273 | s.srv = http.Server{ |
||
| 274 | Addr: addr, |
||
| 275 | Handler: router, |
||
| 276 | ReadHeaderTimeout: 2 * time.Second, |
||
| 277 | BaseContext: func(_ net.Listener) context.Context { |
||
| 278 | bCtx := context.WithValue(ctx, KeyLoggerID, s.log) |
||
| 279 | return bCtx |
||
| 280 | }, |
||
| 281 | } |
||
| 282 | |||
| 283 | s.log.Infof("http server started on %s:%s\n", s.host, s.port) |
||
| 284 | return s.srv.ListenAndServe() |
||
| 285 | } |
||
| 286 | |||
| 287 | func (s *Server) Stop(ctx context.Context) error { |
||
| 288 | if err := s.srv.Shutdown(ctx); err != nil { |
||
| 289 | return err |
||
| 290 | } |
||
| 291 | s.log.Infof("http server shutdown\n") |
||
| 292 | return nil |
||
| 293 | } |
||
| 294 |