// HTTP handlers for /api/drivers and /api/clans. // // Routes: // // POST /api/clans create clan // GET /api/clans list clans (limit, offset) // GET /api/clans/{id} get clan by id // PUT /api/clans/{id} update clan // DELETE /api/clans/{id} delete clan // GET /api/clans/by-tag/{tag} get clan by 3-letter tag // // POST /api/drivers create driver // GET /api/drivers list drivers (limit, offset, clan_id) // GET /api/drivers/{id} get driver by id // PUT /api/drivers/{id} update driver // DELETE /api/drivers/{id} delete driver // GET /api/drivers/by-nick/{nick} get driver by 3-letter nickname package main import ( "encoding/json" "errors" "net/http" "strconv" "strings" "github.com/x0gp/server/internal/clans" "github.com/x0gp/server/internal/drivers" "github.com/x0gp/server/internal/lobby" "github.com/x0gp/server/internal/transport" ) // --------------------------------------------------------------------------- // /api/clans // --------------------------------------------------------------------------- // clansCreateHandler godoc // @Summary Create a clan // @Description Creates a new clan. `tag` must be exactly 3 uppercase ASCII letters (e.g. "ACE") and unique. // @Tags clans // @Accept json // @Produce json // @Param clan body transport.ClanCreateRequest true "Clan to create" // @Success 201 {object} transport.Clan "Created clan" // @Failure 400 {object} map[string]interface{} "Invalid input (tag must be 3 uppercase letters)" // @Failure 409 {object} map[string]interface{} "Clan with that tag already exists" // @Failure 500 {object} map[string]interface{} "Internal server error" // @Router /api/clans [post] func clansCreateHandler(svc *clans.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req transport.ClanCreateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "bad_request", err.Error()) return } c, err := svc.Create(r.Context(), clans.CreateInput{ ID: req.ID, Tag: req.Tag, Name: req.Name, AvatarURL: req.AvatarURL, }) if err != nil { if errors.Is(err, clans.ErrInvalidInput) { writeError(w, http.StatusBadRequest, "invalid_input", err.Error()) return } if errors.Is(err, clans.ErrAlreadyExists) { writeError(w, http.StatusConflict, "already_exists", err.Error()) return } writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } writeJSON(w, http.StatusCreated, clanToWire(c)) } } // clansListHandler godoc // @Summary List clans // @Description Returns clans ordered by tag. Limit 1..200, default 50. // @Tags clans // @Produce json // @Param limit query int false "Page size" // @Param offset query int false "Page offset" // @Success 200 {object} transport.ClanListResponse "Page of clans" // @Failure 500 {object} map[string]interface{} "Internal server error" // @Router /api/clans [get] func clansListHandler(svc *clans.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) rows, err := svc.List(r.Context(), limit, offset) if err != nil { writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } out := make([]transport.Clan, 0, len(rows)) for _, c := range rows { out = append(out, clanToWire(c)) } writeJSON(w, http.StatusOK, transport.ClanListResponse{Items: out, Count: len(out)}) } } // clansByIDHandler godoc // @Summary Get / update / delete a clan by id // @Description Dispatches on HTTP method. // @Tags clans // @Produce json // @Consume json // @Param id path string true "Clan id" // @Param clan body transport.ClanUpdateRequest false "Patch payload (only for PUT)" // @Success 200 {object} transport.Clan "Clan payload or update result" // @Success 204 {object} transport.Clan "Delete ack" // @Failure 400 {object} map[string]interface{} "Bad request" // @Failure 404 {object} map[string]interface{} "Clan not found" // @Failure 500 {object} map[string]interface{} "Internal server error" // @Router /api/clans/{id} [get] // @Router /api/clans/{id} [put] // @Router /api/clans/{id} [delete] func clansByIDHandler(svc *clans.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := strings.TrimPrefix(r.URL.Path, "/api/clans/") if id == "" { writeError(w, http.StatusBadRequest, "bad_request", "missing clan id") return } // Special-case: /api/clans/by-tag/{tag} if strings.HasPrefix(id, "by-tag/") { tag := strings.TrimPrefix(id, "by-tag/") if tag == "" { writeError(w, http.StatusBadRequest, "bad_request", "missing clan tag") return } c, err := svc.GetByTag(r.Context(), tag) if err != nil { if errors.Is(err, clans.ErrNotFound) { writeError(w, http.StatusNotFound, "not_found", "clan not found") return } writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } writeJSON(w, http.StatusOK, clanToWire(c)) return } switch r.Method { case http.MethodGet: c, err := svc.Get(r.Context(), id) if err != nil { if errors.Is(err, clans.ErrNotFound) { writeError(w, http.StatusNotFound, "not_found", "clan not found") return } writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } writeJSON(w, http.StatusOK, clanToWire(c)) case http.MethodPut: var req transport.ClanUpdateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "bad_request", err.Error()) return } c, err := svc.Update(r.Context(), id, clans.UpdateInput{ Name: req.Name, AvatarURL: req.AvatarURL, }) if err != nil { if errors.Is(err, clans.ErrNotFound) { writeError(w, http.StatusNotFound, "not_found", "clan not found") return } writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } writeJSON(w, http.StatusOK, clanToWire(c)) case http.MethodDelete: if err := svc.Delete(r.Context(), id); err != nil { if errors.Is(err, clans.ErrNotFound) { writeError(w, http.StatusNotFound, "not_found", "clan not found") return } writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "id": id}) default: w.Header().Set("Allow", "GET PUT DELETE") writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", r.Method) } } } // --------------------------------------------------------------------------- // /api/drivers // --------------------------------------------------------------------------- // driversCreateHandler godoc // @Summary Create a driver // @Description Creates a new driver. `nickname` must be exactly 3 uppercase ASCII letters (e.g. "ACE") and unique. `clan_id` is optional and must reference an existing clan. // @Tags drivers // @Accept json // @Produce json // @Param driver body transport.DriverCreateRequest true "Driver to create" // @Success 201 {object} transport.Driver "Created driver" // @Failure 400 {object} map[string]interface{} "Invalid input (nickname must be 3 uppercase letters)" // @Failure 409 {object} map[string]interface{} "Driver with that nickname already exists" // @Failure 500 {object} map[string]interface{} "Internal server error" // @Router /api/drivers [post] func driversCreateHandler(svc *drivers.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req transport.DriverCreateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "bad_request", err.Error()) return } d, err := svc.Create(r.Context(), drivers.CreateInput{ ID: req.ID, Nickname: req.Nickname, Name: req.Name, AvatarURL: req.AvatarURL, ClanID: req.ClanID, }) if err != nil { if errors.Is(err, drivers.ErrInvalidInput) { writeError(w, http.StatusBadRequest, "invalid_input", err.Error()) return } if errors.Is(err, drivers.ErrAlreadyExists) { writeError(w, http.StatusConflict, "already_exists", err.Error()) return } writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } writeJSON(w, http.StatusCreated, driverToWire(d)) } } // driversListHandler godoc // @Summary List drivers // @Description Returns drivers ordered by nickname. Optional `clan_id` filter. // @Tags drivers // @Produce json // @Param limit query int false "Page size" // @Param offset query int false "Page offset" // @Param clan_id query string false "Filter by clan id" // @Success 200 {object} transport.DriverListResponse "Page of drivers" // @Failure 500 {object} map[string]interface{} "Internal server error" // @Router /api/drivers [get] func driversListHandler(svc *drivers.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) clanID := r.URL.Query().Get("clan_id") rows, err := svc.List(r.Context(), limit, offset, clanID) if err != nil { writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } out := make([]transport.Driver, 0, len(rows)) for _, d := range rows { out = append(out, driverToWire(d)) } writeJSON(w, http.StatusOK, transport.DriverListResponse{Items: out, Count: len(out)}) } } // driversByIDHandler godoc // @Summary Get / update / delete a driver by id // @Description Dispatches on HTTP method. Path `/api/drivers/by-nick/{nick}` is a convenience that returns the driver by 3-letter nickname. // @Tags drivers // @Produce json // @Consume json // @Param id path string true "Driver id" // @Param driver body transport.DriverUpdateRequest false "Patch payload (only for PUT)" // @Success 200 {object} transport.Driver "Driver payload or update result" // @Success 204 {object} transport.Driver "Delete ack" // @Failure 400 {object} map[string]interface{} "Bad request" // @Failure 404 {object} map[string]interface{} "Driver not found" // @Failure 500 {object} map[string]interface{} "Internal server error" // @Router /api/drivers/{id} [get] // @Router /api/drivers/{id} [put] // @Router /api/drivers/{id} [delete] // @Router /api/drivers/{id}/car [put] func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := strings.TrimPrefix(r.URL.Path, "/api/drivers/") if id == "" { writeError(w, http.StatusBadRequest, "bad_request", "missing driver id") return } if strings.HasSuffix(id, "/car") { driverID := strings.TrimSuffix(id, "/car") if driverID == "" { writeError(w, http.StatusBadRequest, "bad_request", "missing driver id") return } if r.Method != http.MethodPut { w.Header().Set("Allow", "PUT") writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", r.Method) return } // Read car_id from JSON body or query param var carID *string type carRequest struct { CarID *string `json:"car_id"` } var req carRequest if r.ContentLength > 0 { if err := json.NewDecoder(r.Body).Decode(&req); err == nil { carID = req.CarID } } // Fallback to query parameter if body didn't provide it if carID == nil { qVal := r.URL.Query().Get("car_id") if qVal != "" { if qVal == "null" { carID = nil } else { carID = &qVal } } } // Validate driver exists in DB drv, err := svc.Get(r.Context(), driverID) if err != nil { if errors.Is(err, drivers.ErrNotFound) { writeError(w, http.StatusNotFound, "not_found", "driver not found") return } writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } // Make sure driver is registered in lobby _, _ = lobbySvc.AddDriver(driverID, drv.Name, "") lobbySvc.SetDriverProfile(driverID, drv.Nickname, drv.AvatarURL, drv.ClanID, "") // Select car in lobby (handles constraints and write-through) if err := lobbySvc.SelectCar(driverID, carID); err != nil { writeError(w, http.StatusBadRequest, "bad_request", err.Error()) return } // Return updated lobby driver status lobbyDriver, err := lobbySvc.GetDriver(driverID) if err != nil { writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } writeJSON(w, http.StatusOK, lobbyDriver) return } if strings.HasPrefix(id, "by-nick/") { nick := strings.TrimPrefix(id, "by-nick/") if nick == "" { writeError(w, http.StatusBadRequest, "bad_request", "missing driver nickname") return } d, err := svc.GetByNickname(r.Context(), nick) if err != nil { if errors.Is(err, drivers.ErrNotFound) { writeError(w, http.StatusNotFound, "not_found", "driver not found") return } writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } writeJSON(w, http.StatusOK, driverToWire(d)) return } switch r.Method { case http.MethodGet: d, err := svc.Get(r.Context(), id) if err != nil { if errors.Is(err, drivers.ErrNotFound) { writeError(w, http.StatusNotFound, "not_found", "driver not found") return } writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } writeJSON(w, http.StatusOK, driverToWire(d)) case http.MethodPut: var req transport.DriverUpdateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "bad_request", err.Error()) return } d, err := svc.Update(r.Context(), id, drivers.UpdateInput{ Name: req.Name, AvatarURL: req.AvatarURL, ClanID: req.ClanID, }) if err != nil { if errors.Is(err, drivers.ErrNotFound) { writeError(w, http.StatusNotFound, "not_found", "driver not found") return } writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } writeJSON(w, http.StatusOK, driverToWire(d)) case http.MethodDelete: if err := svc.Delete(r.Context(), id); err != nil { if errors.Is(err, drivers.ErrNotFound) { writeError(w, http.StatusNotFound, "not_found", "driver not found") return } writeError(w, http.StatusInternalServerError, "internal", err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "id": id}) default: w.Header().Set("Allow", "GET PUT DELETE") writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", r.Method) } } } // --------------------------------------------------------------------------- // Conversions // --------------------------------------------------------------------------- func clanToWire(c clans.Clan) transport.Clan { return transport.Clan{ ID: c.ID, Tag: c.Tag, Name: c.Name, AvatarURL: c.AvatarURL, CreatedMs: c.CreatedMs, UpdatedMs: c.UpdatedMs, } } func driverToWire(d drivers.Driver) transport.Driver { return transport.Driver{ ID: d.ID, Nickname: d.Nickname, Name: d.Name, AvatarURL: d.AvatarURL, ClanID: d.ClanID, CreatedMs: d.CreatedMs, UpdatedMs: d.UpdatedMs, } } // --------------------------------------------------------------------------- // Method routers // --------------------------------------------------------------------------- // clansRouter routes /api/clans to GET (list) or POST (create). func clansRouter(svc *clans.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: clansListHandler(svc)(w, r) case http.MethodPost: clansCreateHandler(svc)(w, r) default: w.Header().Set("Allow", "GET POST") writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", r.Method) } } } // driversRouter routes /api/drivers to GET (list) or POST (create). func driversRouter(svc *drivers.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: driversListHandler(svc)(w, r) case http.MethodPost: driversCreateHandler(svc)(w, r) default: w.Header().Set("Allow", "GET POST") writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", r.Method) } } }