feat: GTFS Frequencies Phase 3 (API Integration & Handlers)#680
feat: GTFS Frequencies Phase 3 (API Integration & Handlers)#6803rabiii wants to merge 3 commits intoOneBusAway:mainfrom
Conversation
… location handlers
aaronbrethorst
left a comment
There was a problem hiding this comment.
Adel, this is a clean Phase 3 — wiring the frequency data through every handler that needs it, with batch queries to avoid N+1 problems and a comprehensive test suite covering both exact_times modes. The ScheduleFrequency type for the schedule endpoints is the right call since those responses need serviceId and tripId context that the trip-details Frequency struct doesn't carry. The test structure with insertFrequencyData + deferred cleanup is solid.
Two items need fixing before merge, plus a suggestion for a follow-up cleanup.
Important
-
time.Now()used instead ofapi.Clock.Now()in two places — breaks mock clock testing-
internal/restapi/trips_helper.go:223:active := gtfsInternal.GetActiveHeadwayForTime(frequencies, serviceDate, time.Now().In(serviceDate.Location()))
Should be:
active := gtfsInternal.GetActiveHeadwayForTime(frequencies, serviceDate, api.Clock.Now().In(serviceDate.Location()))
-
internal/restapi/trips_for_location_handler.go:821:serviceDate := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, currentLocation)
Should use
api.Clock.Now(). The function already receives acurrentLocation— it just needs the clock-aware time.
The rest of the codebase consistently uses
api.Clock.Now()(e.g.,trips_for_location_handler.go:40). Usingtime.Now()means these code paths will produce wrong results in tests and make the frequency window lookup non-deterministic relative to the mock clock. -
-
Tests silently pass when the endpoint doesn't return 200 (
frequency_integration_test.go, multiple locations)Many test cases guard their assertions with
if model.Code == 200 { ... }, which means the test passes without verifying anything if the endpoint returns an error. Examples:TestTripForVehicleFrequencyIntegrationlines 510, 546TestTripsForLocationFrequencyIntegrationlines 442, 460TestSingularArrivalAndDepartureFrequencyIntegrationlines 862, 879
These should use
require.Equal(t, 200, model.Code)before the assertions. If the endpoint can legitimately return non-200 (e.g., trip not serving this stop), uset.Skip()with a clear reason rather than silently passing.
Suggestions
-
Extract the repeated frequency lookup into a helper (
trips_helper.go)The same ~15-line block appears in four places:
trip_details_handler.go:175-207trip_for_vehicle_handler.go:113-143trips_helper.go:214-232arrival_and_departure_for_stop_handler.go:389-404
Something like:
func (api *RestAPI) lookupTripFrequency(ctx context.Context, tripID string, serviceDate time.Time, now time.Time) *models.Frequency { frequencies, err := api.GtfsManager.GetFrequenciesForTrip(ctx, tripID) if err != nil || len(frequencies) == 0 { return nil } active := gtfsInternal.GetActiveHeadwayForTime(frequencies, serviceDate, now) if active == nil { active = &frequencies[0] } freq := models.NewFrequencyFromDB(*active, serviceDate) return &freq }
This would reduce each call site to a single line and make the fallback-to-first-entry behavior consistent everywhere.
Description
This PR implements Phase 3 (the final phase) of the GTFS Frequencies feature. It wires the database storage and core logic (built in Phases 1 & 2) #582 directly into the API handlers and response models.
Key Changes
This PR is cleanly separated into 3 logical commits to make reviewing easier:
1. Core Models & Single-Trip Handlers:
ScheduleFrequencystruct tostop_time_schedule.gofor the schedule endpoints.NewFrequencyFromDBfor proper Nanosecond to Epoch MS time conversion.trip_details_handler.go,trip_for_vehicle_handler.go, andarrivals_and_departure_for_stop.goto look up and populate theFrequencyobject for frequency-based trips (handling bothexact_times=0active window matching andexact_times=1).2. Batch Queries & Schedule Handlers:
schedule_for_stopandschedule_for_routehandlers to buildScheduleFrequenciesarrays and expand virtual stop times forexact_times=1trips.trips_for_routeandtrips_for_locationto use batch queries (GetFrequenciesForTrips) to populate the headway*int64field efficiently without N+1 query issues.3. Testing:
frequency_integration_test.go.exact_times=0,exact_times=1, andnull/empty fallbacks.@aaronbrethorst
closes : #666