Add vehicle rental data layer and rental-aware trip planning - #154
Conversation
Bikeshare spec v5.1 Phases 0-1 for OTPKit:
- Extend the GraphQL plan query with rentedBike and vehicleRentalStation/
rentalVehicle refs; map them to new Leg.rentedBike and Place.bikeShareId
- Add TransportMode.bikeRental (REST wire token BICYCLE_RENT; GraphQL
{mode: BICYCLE, qualifier: RENT} via new qualifier support)
- Add VehicleRentalService protocol, VehicleRental union model with
fail-open FormFactor decoding, and fetchVehicleRentals(in:formFactors:)
on GraphQLAPIService with partial-success tolerance
- Gate rental modes on service capability via availableTransportModes;
default/reset mode selection honors the same filter
- Localize new strings across all 13 languages
Verified against the live Sound Transit OTP server: rental itineraries
return rentedBike=true with vehicle refs, and vehicleRentalsByBbox decodes
2,555 live entities through the shipped query documents.
📝 WalkthroughWalkthroughAdds docked and free-floating vehicle rental models, GraphQL rental fetching and decoding, rental-aware trip mapping, capability-aware transport mode selection, localized labels, and comprehensive fixtures and tests. ChangesVehicle rental domain models
GraphQL rental flow
Capability-aware presentation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant TripPlannerViewModel
participant VehicleRentalService
participant GraphQLAPIService
participant OTPGraphQL
TripPlannerViewModel->>VehicleRentalService: fetchVehicleRentals(boundingBox, formFactors)
VehicleRentalService->>GraphQLAPIService: rental fetch request
GraphQLAPIService->>OTPGraphQL: vehicleRentalsByBbox
OTPGraphQL-->>GraphQLAPIService: rental data and partial errors
GraphQLAPIService-->>TripPlannerViewModel: VehicleRentalFetchResult
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift (1)
110-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake the new rental reference models
Codable.
GraphQLVehicleRentalStationRefandGraphQLRentalVehicleRefare JSON models but only conform toDecodable.Proposed fix
-struct GraphQLVehicleRentalStationRef: Decodable { +struct GraphQLVehicleRentalStationRef: Codable { let stationId: String? } -struct GraphQLRentalVehicleRef: Decodable { +struct GraphQLRentalVehicleRef: Codable { let vehicleId: String? }As per coding guidelines, “All models must conform to
Codablefor JSON serialization.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift` around lines 110 - 116, Update GraphQLVehicleRentalStationRef and GraphQLRentalVehicleRef to conform to Codable instead of only Decodable, preserving their existing optional stationId and vehicleId properties.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@OTPKit/Sources/OTPKit/Core/Models/OTP/VehicleRentalStation.swift`:
- Around line 60-71: Update VehicleRentalStation.matches so only missing typed
availability (availableVehicles?.byType == nil) fails open; an explicitly empty
byType array must return false, while preserving the existing matching behavior
for populated arrays. Add a regression test in VehicleRentalTests.swift covering
an empty byType array and a form-factor filter.
In `@OTPKit/Sources/OTPKit/Network/GraphQLAPIService.swift`:
- Around line 307-310: Update the station inventory GraphQL selection under
availableVehicles.byType to request propulsionType alongside formFactor, then
extend graphql_rentals_mixed.json with a station e-bike vehicle type and update
its mapping test to verify VehicleType.isPowered distinguishes it correctly.
---
Nitpick comments:
In `@OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift`:
- Around line 110-116: Update GraphQLVehicleRentalStationRef and
GraphQLRentalVehicleRef to conform to Codable instead of only Decodable,
preserving their existing optional stationId and vehicleId properties.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: ec3100af-b263-4fc6-8fc5-f156d024ceeb
📒 Files selected for processing (38)
OTPKit/Sources/OTPKit/Core/Extensions/StringExtension.swiftOTPKit/Sources/OTPKit/Core/Models/OTP/Leg.swiftOTPKit/Sources/OTPKit/Core/Models/OTP/Place.swiftOTPKit/Sources/OTPKit/Core/Models/OTP/RentalVehicle.swiftOTPKit/Sources/OTPKit/Core/Models/OTP/TransportMode.swiftOTPKit/Sources/OTPKit/Core/Models/OTP/VehicleFormFactor.swiftOTPKit/Sources/OTPKit/Core/Models/OTP/VehicleRental.swiftOTPKit/Sources/OTPKit/Core/Models/OTP/VehicleRentalBoundingBox.swiftOTPKit/Sources/OTPKit/Core/Models/OTP/VehicleRentalFetchResult.swiftOTPKit/Sources/OTPKit/Core/Models/OTP/VehicleRentalStation.swiftOTPKit/Sources/OTPKit/Core/Models/OTP/VehicleRentalSupportingTypes.swiftOTPKit/Sources/OTPKit/Core/OTPConfiguration.swiftOTPKit/Sources/OTPKit/Network/GraphQLAPIService.swiftOTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swiftOTPKit/Sources/OTPKit/Network/GraphQLRentalsResponse.swiftOTPKit/Sources/OTPKit/Network/VehicleRentalService.swiftOTPKit/Sources/OTPKit/Presentation/ViewModel/TripPlannerViewModel.swiftOTPKit/Sources/OTPKit/Resources/ar.lproj/Localizable.stringsOTPKit/Sources/OTPKit/Resources/en.lproj/Localizable.stringsOTPKit/Sources/OTPKit/Resources/es.lproj/Localizable.stringsOTPKit/Sources/OTPKit/Resources/fil.lproj/Localizable.stringsOTPKit/Sources/OTPKit/Resources/fr.lproj/Localizable.stringsOTPKit/Sources/OTPKit/Resources/it.lproj/Localizable.stringsOTPKit/Sources/OTPKit/Resources/ko.lproj/Localizable.stringsOTPKit/Sources/OTPKit/Resources/pl.lproj/Localizable.stringsOTPKit/Sources/OTPKit/Resources/pt-BR.lproj/Localizable.stringsOTPKit/Sources/OTPKit/Resources/ru.lproj/Localizable.stringsOTPKit/Sources/OTPKit/Resources/vi.lproj/Localizable.stringsOTPKit/Sources/OTPKit/Resources/zh-Hans.lproj/Localizable.stringsOTPKit/Sources/OTPKit/Resources/zh-Hant.lproj/Localizable.stringsOTPKit/Tests/Fixtures/graphql_plan_rental.jsonOTPKit/Tests/Fixtures/graphql_rentals_mixed.jsonOTPKit/Tests/Fixtures/graphql_rentals_partial_error.jsonOTPKit/Tests/GraphQLAPIServiceTests.swiftOTPKit/Tests/Helpers/TestFixtures.swiftOTPKit/Tests/TripPlanRequestTests.swiftOTPKit/Tests/TripPlannerViewModelTests.swiftOTPKit/Tests/VehicleRentalTests.swift
| /// True when the station stocks any vehicle matching one of the given form factors. | ||
| /// Fail-open: a station with no typed availability breakdown is assumed to match. | ||
| public func matches(formFactors: Set<VehicleFormFactor>) -> Bool { | ||
| guard let byType = availableVehicles?.byType, !byType.isEmpty else { | ||
| return true | ||
| } | ||
|
|
||
| return byType.contains { typeCount in | ||
| guard let formFactor = typeCount.vehicleType.formFactor else { return true } | ||
| return formFactors.contains(formFactor) | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Empty byType array is treated the same as missing data.
The guard !byType.isEmpty makes a station with an explicit, empty typed-availability breakdown (zero vehicle types currently at the station) fail-open to true, even though the docstring's fail-open rationale is specifically for missing typed data (nil), not an explicitly-empty one. This can surface a station with zero available vehicles as "matching" any form-factor filter.
Also, RentalVehicle.matches and VehicleRentalStation.matches currently have no direct test coverage in OTPKit/Tests/VehicleRentalTests.swift — worth adding a regression test for this specific empty-array case alongside the fix.
🐛 Proposed fix
public func matches(formFactors: Set<VehicleFormFactor>) -> Bool {
- guard let byType = availableVehicles?.byType, !byType.isEmpty else {
+ guard let byType = availableVehicles?.byType else {
return true
}
return byType.contains { typeCount in
guard let formFactor = typeCount.vehicleType.formFactor else { return true }
return formFactors.contains(formFactor)
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// True when the station stocks any vehicle matching one of the given form factors. | |
| /// Fail-open: a station with no typed availability breakdown is assumed to match. | |
| public func matches(formFactors: Set<VehicleFormFactor>) -> Bool { | |
| guard let byType = availableVehicles?.byType, !byType.isEmpty else { | |
| return true | |
| } | |
| return byType.contains { typeCount in | |
| guard let formFactor = typeCount.vehicleType.formFactor else { return true } | |
| return formFactors.contains(formFactor) | |
| } | |
| } | |
| /// True when the station stocks any vehicle matching one of the given form factors. | |
| /// Fail-open: a station with no typed availability breakdown is assumed to match. | |
| public func matches(formFactors: Set<VehicleFormFactor>) -> Bool { | |
| guard let byType = availableVehicles?.byType else { | |
| return true | |
| } | |
| return byType.contains { typeCount in | |
| guard let formFactor = typeCount.vehicleType.formFactor else { return true } | |
| return formFactors.contains(formFactor) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@OTPKit/Sources/OTPKit/Core/Models/OTP/VehicleRentalStation.swift` around
lines 60 - 71, Update VehicleRentalStation.matches so only missing typed
availability (availableVehicles?.byType == nil) fails open; an explicitly empty
byType array must return false, while preserving the existing matching behavior
for populated arrays. Add a regression test in VehicleRentalTests.swift covering
an empty byType array and a form-factor filter.
| availableVehicles { | ||
| total | ||
| byType { count vehicleType { formFactor } } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Select propulsionType for station vehicle types.
Station inventory currently decodes VehicleType without propulsionType, unlike free-floating vehicles. Docked e-bikes will therefore appear indistinguishable from human-powered bikes through VehicleType.isPowered. Add the field here and extend graphql_rentals_mixed.json/its mapping test with a station e-bike type.
Proposed fix
- byType { count vehicleType { formFactor } }
+ byType { count vehicleType { formFactor propulsionType } }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| availableVehicles { | |
| total | |
| byType { count vehicleType { formFactor } } | |
| } | |
| availableVehicles { | |
| total | |
| byType { count vehicleType { formFactor propulsionType } } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@OTPKit/Sources/OTPKit/Network/GraphQLAPIService.swift` around lines 307 -
310, Update the station inventory GraphQL selection under
availableVehicles.byType to request propulsionType alongside formFactor, then
extend graphql_rentals_mixed.json with a station e-bike vehicle type and update
its mapping test to verify VehicleType.isPowered distinguishes it correctly.
Summary
planquery now selectsrentedBikeandvehicleRentalStation/rentalVehiclerefs, mapped into newLeg.rentedBikeandPlace.bikeShareId; newTransportMode.bikeRentalserializes asBICYCLE_RENTon REST and{mode: BICYCLE, qualifier: RENT}on GraphQL (mode building gained qualifier support).VehicleRentalServiceprotocol +VehicleRentalunion model (__typename-discriminated, fail-openVehicleFormFactordecoding covering all 8 OTP values), andfetchVehicleRentals(in:formFactors:)onGraphQLAPIService— off-actor decode, GraphQL partial-success tolerance viaVehicleRentalFetchResult.partialErrors, and unknown union members skipped (logged) rather than failing a 12k-entity payload.TripPlannerViewModel.availableTransportModeshides.bikeRentalunless the injected service supports rentals; default/reset mode selection goes through the same filter;OTPConfiguration's default mode list is now explicit so hosts never inherit new modes silently. New strings localized in all 13 languages.The stateful
VehicleRentalSource(viewport→diffed-snapshot pipeline) is the next PR, per the spec's phasing.Test plan
__typenameskip, HTTP errors,displayLabelderivation, capability-gated mode lists and default selection)https://sound-transit-otp.ibi-transit.com/otp/gtfs/v1using the exact shipped query documents: rental itinerary returnsWALK → BICYCLE (rentedBike=true)with vehicle refs on leg places;vehicleRentalsByBboxreturned 2,555 entities (1 station, 2,554 free-floating; 628 bicycles / 1,926 scooters) matching the model shapesfuel.percentnull,rentalUrisnull, placeholder"Default vehicle type"namesReview notes (deliberate choices worth a look)
displayLabel/RentalNetwork.displayNamelive on the models (spec v5.1 seam decision: OTPKit owns derived labels so hosts don't reinvent them). The network-name heuristic takes the first identifier token, so multi-word operators (e.g.capital_bikeshare→ "Capital") will need a host override or mapping later.formFactorsparameter is client-side post-filtering (the OTP query has no server-side filter); it stays in the protocol because the upcomingVehicleRentalSourceis its primary consumer.VehicleRentalTestsuses Swift Testing, consistent with the newer package tests; CLAUDE.md still says package tests are XCTest and could use an update.vehiclesAvailableCount/docksAvailableCountconveniences preferring the typedavailableVehicles/availableSpacesdata — everything is nullable in the GraphQL schema.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by CodeRabbit
New Features
Bug Fixes
Localization