Skip to content

Add OTP 2.x GraphQL API support alongside the 1.x REST API - #150

Merged
aaronbrethorst merged 5 commits into
mainfrom
graphql
Jul 29, 2026
Merged

Add OTP 2.x GraphQL API support alongside the 1.x REST API#150
aaronbrethorst merged 5 commits into
mainfrom
graphql

Conversation

@aaronbrethorst

@aaronbrethorst aaronbrethorst commented Jul 28, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds GraphQLAPIService, an actor implementing the existing APIService protocol against the OTP 2.x GTFS GraphQL API (<server>/otp/gtfs/v1), using the plan query and mapping responses onto the same public models the REST path uses — so OTP 1.x and 2.x servers work interchangeably with zero changes to the ViewModel/UI layers.
  • Routing failures (routingErrors) map onto the existing ErrorResponse/ErrorResponseCode path (with logging for unrecognized codes); top-level GraphQL errors throw OTPKitError.apiError. TransportMode values are explicitly translated to the GraphQL Mode vocabulary (.bikeBICYCLE; the REST token BIKE fails GraphQL enum validation — verified against the live server).
  • Shared wire plumbing (HTTP 200 validation, epoch-millis JSON decoder, OTP routers/<id> URL handling) is extracted into APIServiceSupport.swift and used by both services. The demo app's regions now declare an API type, with a new "Seattle (OTP 2.x GraphQL)" region backed by https://sound-transit-otp.ibi-transit.com/otp/.

Test plan

  • 13 new GraphQLAPIServiceTests (fixtures captured from the live Sound Transit OTP 2.10 server): endpoint URL normalization ×3, request-body construction, bike→BICYCLE mode mapping, itinerary/leg/place/step mapping (incl. GTFS extended route type 12 degrading to nil RouteType, intermediate stops, computed transitTime), synthesized requestParameters, routing-error mapping, unknown-code fallback, top-level GraphQL error, HTTP error.
  • Full OTPKit test suite passes; SwiftLint 0 violations.
  • Live end-to-end in the iOS simulator: fresh install → onboarding with "Seattle (OTP 2.x GraphQL)" selected (persisted defaults confirmed apiType: graphQL + sound-transit URL) → planned Current Location → Space Needle → 3 itineraries rendered (walk → Seattle Center Monorail → walk), matching the server's routing.
  • Live-server probes: plan query with REST-style date/time wire formats, maxWalkDistance, BICYCLE vs BIKE validation, and OUTSIDE_BOUNDS routing-error responses.

Review notes

  • The legacy plan query is used rather than planConnection because it maps 1:1 onto the existing models and exists in every OTP 2.x release; migrating to planConnection later is contained entirely within GraphQLAPIService. Design doc: docs/superpowers/specs/2026-07-28-graphql-api-service-design.md.
  • Pre-existing, unrelated to this change: the demo app crashes at launch on the iOS 27.0 beta simulator (__UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption) because it uses AppDelegate-only lifecycle with no scene manifest. End-to-end verification was done on iOS 26.3. Worth a follow-up issue for UIScene adoption.
  • GraphQLAPIService keeps REST parity choices: it's an actor with no mutable state (matches RestAPIService), exposes baseURL, and synthesizes REST-style requestParameters so OTPResponse stays uniform across backends.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features
    • Added OTP 2.x GraphQL trip-planning support alongside the existing REST API.
    • Updated demo region onboarding to support selecting a GraphQL-backed Seattle region.
    • Mapped GraphQL plans into the existing itinerary, transit, step, and routing-error response formats.
  • Bug Fixes
    • Improved REST network request validation and decoding behavior.
  • Documentation
    • Clarified that itinerary timing values are measured in seconds.
    • Updated API support documentation to reflect GraphQL implementation.
  • Tests
    • Added GraphQL request/response, URL handling, and error-handling coverage.

- New GraphQLAPIService actor implementing APIService via the GTFS
  GraphQL 'plan' query, mapping responses onto the existing public
  models so REST and GraphQL servers work interchangeably.
- Routing errors map onto the existing ErrorResponse/ErrorResponseCode
  path; top-level GraphQL errors throw OTPKitError.apiError.
- Demo app: regions declare an API type; added a Puget Sound OTP 2.x
  region backed by https://sound-transit-otp.ibi-transit.com/otp/.
- Tests use fixtures captured from the live Sound Transit server.
- Map TransportMode.bike to the GraphQL Mode token BICYCLE; the REST
  rawValue BIKE fails GraphQL enum validation (verified live).
- Extract shared wire plumbing (HTTP 200 validation, epoch-millis
  decoder, OTP router-path URL handling) used by both API services.
- Drop fabricated OTP 1.x error ids from the routing-error mapping;
  consumers key off messageCode only.
- Tests: reuse TestFixtures.makeTripPlanRequest and collapse repeated
  fixture arrange/act into a helper.
Mirrors the PR #146 guidance for REST error codes: unrecognized GraphQL
routing error codes now log a warning instead of vanishing, with a test
covering the .unknown mapping. Also corrects stale Itinerary doc
comments (walk/transit/waiting times are seconds, not minutes).
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds OTP 2.x GTFS GraphQL trip planning, maps GraphQL responses into existing OTP models, shares HTTP handling with REST, adds coverage and fixtures, and lets the demo select REST or GraphQL services per region.

Changes

OTP 2.x GraphQL support

Layer / File(s) Summary
Shared network foundation
OTPKit/Sources/OTPKit/Network/APIServiceSupport.swift, OTPKit/Sources/OTPKit/Network/RestAPIService.swift, OTPKit/Sources/OTPKit/Core/Models/OTP/ErrorResponse.swift
Shared HTTP validation, date decoding, router URL utilities, and optional GraphQL-compatible error identifiers are added and adopted by the REST service.
GraphQL planning and model mapping
OTPKit/Sources/OTPKit/Network/GraphQLAPIService.swift, OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift, OTPKit/Sources/OTPKit/Core/Models/OTP/Itinerary.swift
GraphQL plan requests are sent to normalized endpoints, decoded through internal response types, and mapped to existing OTP responses, itineraries, legs, places, steps, and routing errors.
GraphQL fixtures and test coverage
OTPKit/Tests/GraphQLAPIServiceTests.swift, OTPKit/Tests/Fixtures/graphql_plan_*.json, OTPKit/Tests/Helpers/OTPTestCase.swift, docs/superpowers/specs/...
Tests, fixtures, shared test setup, and the implementation design cover endpoint normalization, request variables, response mapping, synthesized parameters, and error handling.
Demo API selection
Demo/OTPKitDemo/models/OTPRegionInfo.swift, Demo/OTPKitDemo/OnboardingViewController.swift, Demo/OTPKitDemo/OTPDemoViewController.swift, CLAUDE.md
Regions persist an API type, onboarding adds a Seattle GraphQL option, and the demo constructs REST or GraphQL services accordingly.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DemoUser
  participant OTPDemoViewController
  participant GraphQLAPIService
  participant OTPServer
  DemoUser->>OTPDemoViewController: select GraphQL region
  OTPDemoViewController->>GraphQLAPIService: construct service for region URL
  OTPDemoViewController->>GraphQLAPIService: fetchPlan(request)
  GraphQLAPIService->>OTPServer: POST GraphQL plan query
  OTPServer-->>GraphQLAPIService: plan response or routing errors
  GraphQLAPIService-->>OTPDemoViewController: OTPResponse
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding OTP 2.x GraphQL support alongside the existing REST API.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch graphql

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift (2)

119-235: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the mapping extensions into a dedicated file.

The GraphQL-to-public-model mapping extensions are substantial; place them in a descriptive mapping extension file, such as GraphQLPlanResponse+Mapping.swift.

🤖 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 119 -
235, Move the GraphQL-to-public-model mapping extensions for GraphQLPlan,
GraphQLRoutingError, GraphQLItinerary, GraphQLLeg, GraphQLPlace, and GraphQLStep
into a dedicated file named GraphQLPlanResponse+Mapping.swift. Preserve all
existing mapping behavior and leave the response model definitions in
GraphQLPlanResponse.swift.

Source: Coding guidelines


23-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the GraphQL wire models Codable.

These response models are Decodable only, contrary to the repository requirement that models conform to Codable. Their properties already support encoding.

Proposed fix
-struct GraphQLResponseEnvelope: Decodable {
+struct GraphQLResponseEnvelope: Codable {
🤖 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 23 -
115, Update all GraphQL wire model declarations in GraphQLResponseEnvelope,
GraphQLErrorMessage, GraphQLPlanData, GraphQLPlan, GraphQLRoutingError,
GraphQLItinerary, GraphQLLeg, GraphQLRoute, GraphQLAgency, GraphQLPlace,
GraphQLStop, GraphQLLegGeometry, and GraphQLStep to conform to Codable instead
of Decodable, preserving their existing properties and structure.

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/Network/GraphQLPlanResponse.swift`:
- Around line 161-162: Update the ErrorResponse construction in the GraphQL
routing-error conversion path to preserve the missing ID rather than assigning
the fabricated -1 sentinel. Use the existing optional-ID representation
supported by ErrorResponse, while retaining description as message and
messageCode unchanged.

---

Nitpick comments:
In `@OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift`:
- Around line 119-235: Move the GraphQL-to-public-model mapping extensions for
GraphQLPlan, GraphQLRoutingError, GraphQLItinerary, GraphQLLeg, GraphQLPlace,
and GraphQLStep into a dedicated file named GraphQLPlanResponse+Mapping.swift.
Preserve all existing mapping behavior and leave the response model definitions
in GraphQLPlanResponse.swift.
- Around line 23-115: Update all GraphQL wire model declarations in
GraphQLResponseEnvelope, GraphQLErrorMessage, GraphQLPlanData, GraphQLPlan,
GraphQLRoutingError, GraphQLItinerary, GraphQLLeg, GraphQLRoute, GraphQLAgency,
GraphQLPlace, GraphQLStop, GraphQLLegGeometry, and GraphQLStep to conform to
Codable instead of Decodable, preserving their existing properties and
structure.
🪄 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: 4a567bbd-76b6-4797-bbc3-ae38bad70f0f

📥 Commits

Reviewing files that changed from the base of the PR and between 372a65c and 15d5a58.

📒 Files selected for processing (14)
  • CLAUDE.md
  • Demo/OTPKitDemo/OTPDemoViewController.swift
  • Demo/OTPKitDemo/OnboardingViewController.swift
  • Demo/OTPKitDemo/models/OTPRegionInfo.swift
  • OTPKit/Sources/OTPKit/Core/Models/OTP/Itinerary.swift
  • OTPKit/Sources/OTPKit/Network/APIServiceSupport.swift
  • OTPKit/Sources/OTPKit/Network/GraphQLAPIService.swift
  • OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift
  • OTPKit/Sources/OTPKit/Network/RestAPIService.swift
  • OTPKit/Tests/Fixtures/graphql_plan_routing_error.json
  • OTPKit/Tests/Fixtures/graphql_plan_success.json
  • OTPKit/Tests/GraphQLAPIServiceTests.swift
  • OTPKit/Tests/Helpers/OTPTestCase.swift
  • docs/superpowers/specs/2026-07-28-graphql-api-service-design.md

Comment thread OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift Outdated
GraphQL routing errors carry no wire id, so preserve its absence
rather than synthesizing a -1 sentinel. REST decoding is unaffected;
nothing consumes the id, and consumers key off messageCode.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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/ErrorResponse.swift`:
- Around line 23-25: Document the public API change to ErrorResponse.id in the
release notes, noting that its type changed from Int to Int? because OTP 2.x
GraphQL errors may not provide an identifier. Include migration guidance for
callers that compare, initialize, or otherwise treat id as non-optional,
requiring them to unwrap or handle nil.
🪄 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: 5e0e5e2c-fa65-4731-a4e2-e20b8bcdee2b

📥 Commits

Reviewing files that changed from the base of the PR and between 15d5a58 and 3752e59.

📒 Files selected for processing (3)
  • OTPKit/Sources/OTPKit/Core/Models/OTP/ErrorResponse.swift
  • OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift
  • OTPKit/Tests/GraphQLAPIServiceTests.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift
  • OTPKit/Tests/GraphQLAPIServiceTests.swift

Comment on lines +23 to +25
/// A unique identifier for the error. Only the OTP 1.x REST API supplies one;
/// errors mapped from the OTP 2.x GraphQL API have no id.
public let id: Int?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n --glob '*.swift' '\bErrorResponse\b|\.id\b' OTPKit Demo

Repository: OneBusAway/otpkit

Length of output: 3320


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- ErrorResponse.swift ---\n'
cat -n OTPKit/Sources/OTPKit/Core/Models/OTP/ErrorResponse.swift

printf '\n--- GraphQL mapping snippets ---\n'
sed -n '100,175p' OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift | cat -n

printf '\n--- all ErrorResponse usages ---\n'
rg -n --glob '*.swift' '\bErrorResponse(\b|<|<.*>)|\.error\b|error\.id\s*' OTPKit Demo .

printf '\n--- relevant diff/stat files (if available) ---\n'
git diff --stat || true
git diff -- OTPKit/Sources/OTPKit/Core/Models/OTP/ErrorResponse.swift OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift OTPKit/Tests/GraphQLAPIServiceTests.swift 2>/dev/null | sed -n '1,220p' || true

Repository: OneBusAway/otpkit

Length of output: 246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ErrorResponse.swift ---'
cat -n OTPKit/Sources/OTPKit/Core/Models/OTP/ErrorResponse.swift

printf '%s\n' ''
printf '%s\n' '--- GraphQL mapping snippets ---'
sed -n '100,175p' OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift | cat -n

printf '%s\n' ''
printf '%s\n' '--- all ErrorResponse usages ---'
rg -n --glob '*.swift' 'ErrorResponse(\b|<|<.*>)|\.error\b|error\.id\s*' OTPKit Demo . || true

printf '%s\n' ''
printf '%s\n' '--- relevant diff/stat files (if available) ---'
git diff --stat || true
git diff -- OTPKit/Sources/OTPKit/Core/Models/OTP/ErrorResponse.swift OTPKit/Sources/OTPKit/Network/GraphQLPlanResponse.swift OTPKit/Tests/GraphQLAPIServiceTests.swift 2>/dev/null | sed -n '1,220p' || true

Repository: OneBusAway/otpkit

Length of output: 18261


Document the public API breaking change and include the migration impact in the release notes.

ErrorResponse.id is public, so changing it from Int to Int? will break callers that compare, initialize, or treat it as a non-optional integer. Repo call sites already handle nil, but documented migration guidance is needed for external API consumers.

🤖 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/ErrorResponse.swift` around lines 23 -
25, Document the public API change to ErrorResponse.id in the release notes,
noting that its type changed from Int to Int? because OTP 2.x GraphQL errors may
not provide an identifier. Include migration guidance for callers that compare,
initialize, or otherwise treat id as non-optional, requiring them to unwrap or
handle nil.

@aaronbrethorst
aaronbrethorst merged commit 50f4a50 into main Jul 29, 2026
5 checks passed
@aaronbrethorst
aaronbrethorst deleted the graphql branch July 29, 2026 00:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant