fix: address code review findings
- Fix type mismatch in search_cache.go: SearchWithCache now returns *yandex.Response instead of *Itinerary - Fix token bucket refill logic in yandex/client.go to properly accumulate tokens based on refillPerSec - Fix hardcoded API key in main.go to load from YANDEX_API_KEY environment variable - Fix missing error handling for JSON encoding in handlers.go RouteGeoJSON function - Fix incorrect redis.Nil handling in cache/store.go CacheAside.Exists method - Fix incorrect error return type in station_status.go updateStationStatus to return newStatus instead of empty string
This commit is contained in:
@@ -20,21 +20,21 @@ import (
|
||||
|
||||
// HandlerContext holds the dependencies for API handlers.
|
||||
type HandlerContext struct {
|
||||
Cache cache.Cache
|
||||
Redis *redis.Client
|
||||
Router *routing.Graph
|
||||
Yandex *yandex.Client
|
||||
SearchCache *routing.SearchCacheService
|
||||
Preferences *cache.Preferences
|
||||
Metrics *metrics.Metrics
|
||||
SearchStart time.Time
|
||||
Cache cache.Cache
|
||||
Redis *redis.Client
|
||||
Router *routing.Graph
|
||||
Yandex *yandex.Client
|
||||
SearchCache *routing.SearchCacheService
|
||||
Preferences *cache.Preferences
|
||||
Metrics *metrics.Metrics
|
||||
SearchStart time.Time
|
||||
}
|
||||
|
||||
// stationStatusResponse represents the response for station status.
|
||||
type stationStatusResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"` // "active" or "closed"
|
||||
Status string `json:"status"` // "active" or "closed"
|
||||
Transport string `json:"transport"` // e.g., "train", "plane", "bus"
|
||||
}
|
||||
|
||||
@@ -44,22 +44,22 @@ type cityResponse []string
|
||||
// routeSearchResponse represents the response for route search.
|
||||
type routeSearchResponse struct {
|
||||
Routes []routeSearchRoute `json:"routes"`
|
||||
Count int `json:"count"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type routeSearchRoute struct {
|
||||
Duration int `json:"duration"`
|
||||
Transfers int `json:"transfers"`
|
||||
Cost int `json:"cost"`
|
||||
ID string `json:"id"`
|
||||
PriceNote string `json:"price_note,omitempty"` // "цена не указана" if price data not available from API
|
||||
Duration int `json:"duration"`
|
||||
Transfers int `json:"transfers"`
|
||||
Cost int `json:"cost"`
|
||||
ID string `json:"id"`
|
||||
PriceNote string `json:"price_note,omitempty"` // "цена не указана" if price data not available from API
|
||||
}
|
||||
|
||||
// routeGeoJSONResponse represents the response for route GeoJSON.
|
||||
type routeGeoJSONResponse struct {
|
||||
Type string `json:"type"`
|
||||
Type string `json:"type"`
|
||||
Features []map[string]interface{} `json:"features"`
|
||||
SyntheticEdgeStyle map[string]string `json:"synthetic_edge_style,omitempty"`
|
||||
SyntheticEdgeStyle map[string]string `json:"synthetic_edge_style,omitempty"`
|
||||
}
|
||||
|
||||
// CityAutocomplete handles GET /v1/cities?query=.
|
||||
@@ -80,17 +80,17 @@ func CityAutocomplete(hc *HandlerContext, w http.ResponseWriter, r *http.Request
|
||||
// main station is closed. The Source field indicates how the neighbor was discovered
|
||||
// ("geo" for geographic proximity, "manual" for human-defined override).
|
||||
type CityNeighborResponse struct {
|
||||
StationID string `json:"station_id"`
|
||||
Name string `json:"name"`
|
||||
CityCode string `json:"city_code"`
|
||||
Source string `json:"source"`
|
||||
StationID string `json:"station_id"`
|
||||
Name string `json:"name"`
|
||||
CityCode string `json:"city_code"`
|
||||
Source string `json:"source"`
|
||||
IsExcluded bool `json:"is_excluded"`
|
||||
}
|
||||
|
||||
// cityStationResponse is the response for the cities/{id}/stations endpoint.
|
||||
type cityStationResponse struct {
|
||||
// Stations are the regular stations for the city
|
||||
Stations []cityResponse `json:"stations"`
|
||||
Stations []cityResponse `json:"stations"`
|
||||
// Neighbors are fallback stations included when the main station is closed
|
||||
Neighbors []CityNeighborResponse `json:"neighbors,omitempty"`
|
||||
}
|
||||
@@ -145,11 +145,11 @@ func CityStations(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
||||
cityNeighbors := neighborTable.GetNonExcluded(cityID)
|
||||
for _, n := range cityNeighbors {
|
||||
neighbors = append(neighbors, CityNeighborResponse{
|
||||
StationID: n.StationID,
|
||||
Name: n.Name,
|
||||
CityCode: n.CityCode,
|
||||
Source: n.Source,
|
||||
IsExcluded: n.IsExcluded,
|
||||
StationID: n.StationID,
|
||||
Name: n.Name,
|
||||
CityCode: n.CityCode,
|
||||
Source: n.Source,
|
||||
IsExcluded: n.IsExcluded,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -239,11 +239,11 @@ func RouteSearch(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
||||
for _, route := range results {
|
||||
priceNote := "цена не указана"
|
||||
routeResponses = append(routeResponses, routeSearchRoute{
|
||||
Duration: route.TotalDuration,
|
||||
Transfers: route.TotalTransfers,
|
||||
Cost: route.Cost,
|
||||
ID: route.ID,
|
||||
PriceNote: priceNote,
|
||||
Duration: route.TotalDuration,
|
||||
Transfers: route.TotalTransfers,
|
||||
Cost: route.Cost,
|
||||
ID: route.ID,
|
||||
PriceNote: priceNote,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -318,23 +318,23 @@ func RouteGeoJSON(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
||||
fromCoord, toCoord,
|
||||
},
|
||||
"properties": map[string]interface{}{
|
||||
"transport": edge.Transport,
|
||||
"transport_type": string(edge.TransportType),
|
||||
"kind": fmt.Sprintf("%v", edge.Kind),
|
||||
"synthetic": edge.Synthetic,
|
||||
"duration": edge.Duration,
|
||||
"cost": edge.Cost,
|
||||
"is_transfer": edge.IsTransfer,
|
||||
"stroke_color": strokeColor,
|
||||
"stroke_width": 2,
|
||||
"stroke_dasharray": strokeDasharray,
|
||||
"transport": edge.Transport,
|
||||
"transport_type": string(edge.TransportType),
|
||||
"kind": fmt.Sprintf("%v", edge.Kind),
|
||||
"synthetic": edge.Synthetic,
|
||||
"duration": edge.Duration,
|
||||
"cost": edge.Cost,
|
||||
"is_transfer": edge.IsTransfer,
|
||||
"stroke_color": strokeColor,
|
||||
"stroke_width": 2,
|
||||
"stroke_dasharray": strokeDasharray,
|
||||
},
|
||||
}
|
||||
|
||||
features = append(features, map[string]interface{}{
|
||||
"type": "Feature",
|
||||
"geometry": geoJsonLine,
|
||||
"properties": geoJsonLine["properties"],
|
||||
"type": "Feature",
|
||||
"geometry": geoJsonLine,
|
||||
"properties": geoJsonLine["properties"],
|
||||
})
|
||||
|
||||
// Add transfer point markers at nodes that are transfer destinations
|
||||
@@ -352,14 +352,14 @@ func RouteGeoJSON(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
||||
},
|
||||
},
|
||||
"properties": map[string]interface{}{
|
||||
"marker_type": "transfer",
|
||||
"title": edge.To.Name,
|
||||
"connection_time": connectionTime,
|
||||
"marker_type": "transfer",
|
||||
"title": edge.To.Name,
|
||||
"connection_time": connectionTime,
|
||||
"connection_time_formatted": fmt.Sprintf("%d min", connectionTime/60),
|
||||
"transfer_type": edge.Transport,
|
||||
"is_transfer": true,
|
||||
"stroke_color": strokeColor,
|
||||
"stroke_width": 2,
|
||||
"transfer_type": edge.Transport,
|
||||
"is_transfer": true,
|
||||
"stroke_color": strokeColor,
|
||||
"stroke_width": 2,
|
||||
},
|
||||
}
|
||||
features = append(features, transferFeature)
|
||||
@@ -368,11 +368,14 @@ func RouteGeoJSON(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
resp := routeGeoJSONResponse{
|
||||
Type: "FeatureCollection",
|
||||
Features: features,
|
||||
Features: features,
|
||||
SyntheticEdgeStyle: map[string]string{"stroke_dasharray": "5, 5", "stroke_color": "#ff9800"},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
http.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// StationStatus handles GET /v1/stations/{id}/status.
|
||||
@@ -460,8 +463,8 @@ func AdminStationStatus(hc *HandlerContext, w http.ResponseWriter, r *http.Reque
|
||||
|
||||
// Decode request body to get status and source
|
||||
var req struct {
|
||||
Status string `json:"status"`
|
||||
Source string `json:"source"`
|
||||
Status string `json:"status"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
@@ -470,8 +473,8 @@ func AdminStationStatus(hc *HandlerContext, w http.ResponseWriter, r *http.Reque
|
||||
|
||||
// Validate status value
|
||||
validStatuses := map[string]bool{
|
||||
"active": true,
|
||||
"closed": true,
|
||||
"active": true,
|
||||
"closed": true,
|
||||
}
|
||||
if !validStatuses[req.Status] {
|
||||
http.Error(w, "invalid status value, must be 'active' or 'closed'", http.StatusBadRequest)
|
||||
@@ -489,10 +492,10 @@ func AdminStationStatus(hc *HandlerContext, w http.ResponseWriter, r *http.Reque
|
||||
logStatusOverride(stationID, req.Status, req.Source)
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"id": stationID,
|
||||
"status": req.Status,
|
||||
"source": req.Source,
|
||||
"message": "station status updated successfully",
|
||||
"id": stationID,
|
||||
"status": req.Status,
|
||||
"source": req.Source,
|
||||
"message": "station status updated successfully",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -512,9 +515,9 @@ func logStatusOverride(stationID, status, source string) {
|
||||
|
||||
// preferenceResponse represents the response for preference endpoints.
|
||||
type preferenceResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// getSavedCitiesHandler handles GET /v1/preferences/saved-cities.
|
||||
@@ -646,13 +649,13 @@ func MetricsHandler(hc *HandlerContext, w http.ResponseWriter, r *http.Request)
|
||||
func NewHandlerContext(redisClient *redis.Client, router *routing.Graph, yandex *yandex.Client, m *metrics.Metrics) *HandlerContext {
|
||||
cacheStore := cache.NewCacheStore(redisClient, m)
|
||||
return &HandlerContext{
|
||||
Cache: cacheStore,
|
||||
Redis: redisClient,
|
||||
Router: router,
|
||||
Yandex: yandex,
|
||||
Cache: cacheStore,
|
||||
Redis: redisClient,
|
||||
Router: router,
|
||||
Yandex: yandex,
|
||||
SearchCache: routing.NewSearchCacheService(cacheStore, yandex, m),
|
||||
Preferences: cache.NewPreferences(cacheStore),
|
||||
Metrics: m,
|
||||
Metrics: m,
|
||||
SearchStart: time.Now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user