Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inject defaults into the planConnection query in the GTFS GraphQL schema #6339

Open
wants to merge 24 commits into
base: dev-2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c10fb01
Draft for injecting defaults into GraphQL schema
optionsome Dec 17, 2024
2c42d14
Don't rely on directive for injecting defaults
optionsome Dec 17, 2024
c51b975
Use dependency injection for providing GraphQL schema
optionsome Dec 30, 2024
f639ce5
Only construct schema if feature flag is on
optionsome Dec 30, 2024
38b5a5b
Use a slightly prettier method for getting parent name
optionsome Dec 30, 2024
698349e
Replace preferences with route request
optionsome Dec 31, 2024
7162c7a
Add structure for injecting defaults for arguments and use it for
optionsome Dec 31, 2024
280fd45
Add default for search window and allow resetting it to null
optionsome Dec 31, 2024
45eea03
Add the rest of defaults
optionsome Dec 31, 2024
d298811
Fix schema comment about duration being seconds
optionsome Dec 31, 2024
5fe8da8
Move schema tests to new file
optionsome Dec 31, 2024
4f89618
Add tests for default injection
optionsome Dec 31, 2024
6f4a6fb
Merge remote-tracking branch 'upstream/dev-2.x' into plan-connection-…
optionsome Dec 31, 2024
c105fc2
Fix comment
optionsome Dec 31, 2024
3beb9be
Use schema visitor instead of directive wiring
optionsome Jan 3, 2025
813bbbf
Initialize schema on server start up
optionsome Jan 7, 2025
9157359
Remove unnecessary annotation
optionsome Jan 8, 2025
7cd815e
Split factory method into two versions
optionsome Jan 8, 2025
3b3d7ff
Merge remote-tracking branch 'upstream/dev-2.x' into plan-connection-…
optionsome Jan 8, 2025
9f72862
Add javadoc
optionsome Jan 8, 2025
f7585cc
Remove service wrapper
optionsome Jan 10, 2025
69de270
Fix comment
optionsome Jan 10, 2025
fc7c77f
Add missing annotation
optionsome Jan 14, 2025
76db9b5
Refactor defaults
optionsome Jan 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package org.opentripplanner.apis.gtfs;

import graphql.language.BooleanValue;
import graphql.language.FloatValue;
import graphql.language.IntValue;
import graphql.language.StringValue;
import graphql.language.Value;
import graphql.schema.GraphQLArgument;
import graphql.schema.GraphQLInputObjectField;
import graphql.schema.idl.SchemaDirectiveWiring;
import graphql.schema.idl.SchemaDirectiveWiringEnvironment;
import org.opentripplanner.routing.api.request.RouteRequest;

public class DefaultValueDirectiveWiring implements SchemaDirectiveWiring {

private final RouteRequest defaultRouteRequest;

public DefaultValueDirectiveWiring(RouteRequest defaultRouteRequest) {
this.defaultRouteRequest = defaultRouteRequest;
}

@Override
public GraphQLArgument onArgument(SchemaDirectiveWiringEnvironment<GraphQLArgument> environment) {
GraphQLArgument argument = environment.getElement();
var defaultValue = getDefaultValueForSchemaObject(environment);
if (defaultValue != null) {
return argument.transform(builder -> builder.defaultValueLiteral(defaultValue).build());
}
return argument;
}

@Override
public GraphQLInputObjectField onInputObjectField(
SchemaDirectiveWiringEnvironment<GraphQLInputObjectField> environment
) {
GraphQLInputObjectField field = environment.getElement();
var defaultValue = getDefaultValueForSchemaObject(environment);
if (defaultValue != null) {
return field.transform(builder -> builder.defaultValueLiteral(defaultValue).build());
}
return field;
}

private Value getDefaultValueForSchemaObject(SchemaDirectiveWiringEnvironment<?> environment) {
// Arguments and input fields always have a parent
var parentName = environment.getNodeParentTree().getParentInfo().get().getNode().getName();
var key = parentName + "_" + environment.getElement().getName();
var preferences = defaultRouteRequest.preferences();
switch (key) {
case "planConnection_first":
return IntValue.of(defaultRouteRequest.numItineraries());
case "planConnection_searchWindow":
return defaultRouteRequest.searchWindow() != null
? StringValue.of(defaultRouteRequest.searchWindow().toString())
: null;
case "AlightPreferencesInput_slack":
return StringValue.of(preferences.transit().alightSlack().defaultValue().toString());
case "BicycleParkingPreferencesInput_unpreferredCost":
return IntValue.of(
preferences.bike().parking().unpreferredVehicleParkingTagCost().toSeconds()
);
case "BicyclePreferencesInput_boardCost":
return IntValue.of(preferences.bike().boardCost());
case "BicyclePreferencesInput_reluctance":
return FloatValue.of(preferences.bike().reluctance());
case "BicyclePreferencesInput_speed":
return FloatValue.of(preferences.bike().speed());
case "BicycleWalkPreferencesCostInput_mountDismountCost":
return IntValue.of(preferences.bike().walking().mountDismountCost().toSeconds());
case "BicycleWalkPreferencesCostInput_reluctance":
return FloatValue.of(preferences.bike().walking().reluctance());
case "BicycleWalkPreferencesInput_mountDismountTime":
return StringValue.of(preferences.bike().walking().mountDismountTime().toString());
case "BicycleWalkPreferencesInput_speed":
return FloatValue.of(preferences.bike().walking().speed());
case "BoardPreferencesInput_slack":
return StringValue.of(preferences.transit().boardSlack().defaultValue().toString());
case "BoardPreferencesInput_waitReluctance":
return FloatValue.of(preferences.transfer().waitReluctance());
case "CarParkingPreferencesInput_unpreferredCost":
return IntValue.of(
preferences.car().parking().unpreferredVehicleParkingTagCost().toSeconds()
);
case "CarPreferencesInput_reluctance":
return FloatValue.of(preferences.car().reluctance());
case "DestinationBicyclePolicyInput_allowKeeping":
return BooleanValue.of(
preferences.bike().rental().allowArrivingInRentedVehicleAtDestination()
);
case "DestinationBicyclePolicyInput_keepingCost":
return IntValue.of(
preferences.bike().rental().arrivingInRentalVehicleAtDestinationCost().toSeconds()
);
case "DestinationScooterPolicyInput_allowKeeping":
return BooleanValue.of(
preferences.scooter().rental().allowArrivingInRentedVehicleAtDestination()
);
case "DestinationScooterPolicyInput_keepingCost":
return IntValue.of(
preferences.scooter().rental().arrivingInRentalVehicleAtDestinationCost().toSeconds()
);
case "ScooterPreferencesInput_reluctance":
return FloatValue.of(preferences.scooter().reluctance());
case "ScooterPreferencesInput_speed":
return FloatValue.of(preferences.scooter().speed());
case "TimetablePreferencesInput_excludeRealTimeUpdates":
return BooleanValue.of(preferences.transit().ignoreRealtimeUpdates());
case "TimetablePreferencesInput_includePlannedCancellations":
return BooleanValue.of(preferences.transit().includePlannedCancellations());
case "TimetablePreferencesInput_includeRealTimeCancellations":
return BooleanValue.of(preferences.transit().includeRealtimeCancellations());
case "TransferPreferencesInput_cost":
return IntValue.of(preferences.transfer().cost());
case "TransferPreferencesInput_maximumAdditionalTransfers":
return IntValue.of(preferences.transfer().maxAdditionalTransfers());
case "TransferPreferencesInput_maximumTransfers":
// Max transfers are wrong in the internal model but fixed in the API mapping
return IntValue.of(preferences.transfer().maxTransfers() - 1);
case "TransferPreferencesInput_slack":
return StringValue.of(preferences.transfer().slack().toString());
case "WalkPreferencesInput_boardCost":
return IntValue.of(preferences.walk().boardCost());
case "WalkPreferencesInput_reluctance":
return FloatValue.of(preferences.walk().reluctance());
case "WalkPreferencesInput_safetyFactor":
return FloatValue.of(preferences.walk().safetyFactor());
case "WalkPreferencesInput_speed":
return FloatValue.of(preferences.walk().speed());
case "WheelchairPreferencesInput_enabled":
return BooleanValue.of(defaultRouteRequest.wheelchair());
default:
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.opentripplanner.apis.gtfs;

import org.opentripplanner.apis.gtfs.service.SchemaService;
import org.opentripplanner.routing.api.RoutingService;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.routing.fares.FareService;
Expand All @@ -17,6 +18,7 @@ public record GraphQLRequestContext(
VehicleRentalService vehicleRentalService,
VehicleParkingService vehicleParkingService,
RealtimeVehicleService realTimeVehicleService,
SchemaService schemaService,
GraphFinder graphFinder,
RouteRequest defaultRouteRequest
) {
Expand All @@ -28,6 +30,7 @@ public static GraphQLRequestContext ofServerContext(OtpServerRequestContext cont
context.vehicleRentalService(),
context.vehicleParkingService(),
context.realtimeVehicleService(),
context.schemaService(),
context.graphFinder(),
context.defaultRouteRequest()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,203 +7,22 @@
import graphql.execution.AbortExecutionException;
import graphql.execution.instrumentation.ChainedInstrumentation;
import graphql.execution.instrumentation.Instrumentation;
import graphql.scalars.ExtendedScalars;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import io.micrometer.core.instrument.Metrics;
import jakarta.ws.rs.core.Response;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.opentripplanner.apis.gtfs.datafetchers.AgencyImpl;
import org.opentripplanner.apis.gtfs.datafetchers.AlertEntityTypeResolver;
import org.opentripplanner.apis.gtfs.datafetchers.AlertImpl;
import org.opentripplanner.apis.gtfs.datafetchers.BikeParkImpl;
import org.opentripplanner.apis.gtfs.datafetchers.BikeRentalStationImpl;
import org.opentripplanner.apis.gtfs.datafetchers.BookingInfoImpl;
import org.opentripplanner.apis.gtfs.datafetchers.BookingTimeImpl;
import org.opentripplanner.apis.gtfs.datafetchers.CallScheduledTimeTypeResolver;
import org.opentripplanner.apis.gtfs.datafetchers.CallStopLocationTypeResolver;
import org.opentripplanner.apis.gtfs.datafetchers.CarParkImpl;
import org.opentripplanner.apis.gtfs.datafetchers.ContactInfoImpl;
import org.opentripplanner.apis.gtfs.datafetchers.CoordinatesImpl;
import org.opentripplanner.apis.gtfs.datafetchers.CurrencyImpl;
import org.opentripplanner.apis.gtfs.datafetchers.DefaultFareProductImpl;
import org.opentripplanner.apis.gtfs.datafetchers.DepartureRowImpl;
import org.opentripplanner.apis.gtfs.datafetchers.EstimatedTimeImpl;
import org.opentripplanner.apis.gtfs.datafetchers.FareProductTypeResolver;
import org.opentripplanner.apis.gtfs.datafetchers.FareProductUseImpl;
import org.opentripplanner.apis.gtfs.datafetchers.FeedImpl;
import org.opentripplanner.apis.gtfs.datafetchers.GeometryImpl;
import org.opentripplanner.apis.gtfs.datafetchers.ItineraryImpl;
import org.opentripplanner.apis.gtfs.datafetchers.LegImpl;
import org.opentripplanner.apis.gtfs.datafetchers.LegTimeImpl;
import org.opentripplanner.apis.gtfs.datafetchers.MoneyImpl;
import org.opentripplanner.apis.gtfs.datafetchers.NodeTypeResolver;
import org.opentripplanner.apis.gtfs.datafetchers.OpeningHoursImpl;
import org.opentripplanner.apis.gtfs.datafetchers.PatternImpl;
import org.opentripplanner.apis.gtfs.datafetchers.PlaceImpl;
import org.opentripplanner.apis.gtfs.datafetchers.PlaceInterfaceTypeResolver;
import org.opentripplanner.apis.gtfs.datafetchers.PlanConnectionImpl;
import org.opentripplanner.apis.gtfs.datafetchers.PlanImpl;
import org.opentripplanner.apis.gtfs.datafetchers.QueryTypeImpl;
import org.opentripplanner.apis.gtfs.datafetchers.RealTimeEstimateImpl;
import org.opentripplanner.apis.gtfs.datafetchers.RentalPlaceTypeResolver;
import org.opentripplanner.apis.gtfs.datafetchers.RentalVehicleImpl;
import org.opentripplanner.apis.gtfs.datafetchers.RentalVehicleTypeImpl;
import org.opentripplanner.apis.gtfs.datafetchers.RideHailingEstimateImpl;
import org.opentripplanner.apis.gtfs.datafetchers.RouteImpl;
import org.opentripplanner.apis.gtfs.datafetchers.RouteTypeImpl;
import org.opentripplanner.apis.gtfs.datafetchers.RoutingErrorImpl;
import org.opentripplanner.apis.gtfs.datafetchers.StopCallImpl;
import org.opentripplanner.apis.gtfs.datafetchers.StopGeometriesImpl;
import org.opentripplanner.apis.gtfs.datafetchers.StopImpl;
import org.opentripplanner.apis.gtfs.datafetchers.StopOnRouteImpl;
import org.opentripplanner.apis.gtfs.datafetchers.StopOnTripImpl;
import org.opentripplanner.apis.gtfs.datafetchers.StopRelationshipImpl;
import org.opentripplanner.apis.gtfs.datafetchers.StoptimeImpl;
import org.opentripplanner.apis.gtfs.datafetchers.StoptimesInPatternImpl;
import org.opentripplanner.apis.gtfs.datafetchers.SystemNoticeImpl;
import org.opentripplanner.apis.gtfs.datafetchers.TicketTypeImpl;
import org.opentripplanner.apis.gtfs.datafetchers.TranslatedStringImpl;
import org.opentripplanner.apis.gtfs.datafetchers.TripImpl;
import org.opentripplanner.apis.gtfs.datafetchers.TripOccupancyImpl;
import org.opentripplanner.apis.gtfs.datafetchers.TripOnServiceDateImpl;
import org.opentripplanner.apis.gtfs.datafetchers.UnknownImpl;
import org.opentripplanner.apis.gtfs.datafetchers.VehicleParkingImpl;
import org.opentripplanner.apis.gtfs.datafetchers.VehiclePositionImpl;
import org.opentripplanner.apis.gtfs.datafetchers.VehicleRentalNetworkImpl;
import org.opentripplanner.apis.gtfs.datafetchers.VehicleRentalStationImpl;
import org.opentripplanner.apis.gtfs.datafetchers.debugOutputImpl;
import org.opentripplanner.apis.gtfs.datafetchers.elevationProfileComponentImpl;
import org.opentripplanner.apis.gtfs.datafetchers.placeAtDistanceImpl;
import org.opentripplanner.apis.gtfs.datafetchers.serviceTimeRangeImpl;
import org.opentripplanner.apis.gtfs.datafetchers.stepImpl;
import org.opentripplanner.apis.gtfs.datafetchers.stopAtDistanceImpl;
import org.opentripplanner.apis.gtfs.model.StopPosition;
import org.opentripplanner.apis.support.graphql.LoggingDataFetcherExceptionHandler;
import org.opentripplanner.ext.actuator.MicrometerGraphQLInstrumentation;
import org.opentripplanner.framework.application.OTPFeature;
import org.opentripplanner.framework.graphql.GraphQLResponseSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class GtfsGraphQLIndex {

static final Logger LOG = LoggerFactory.getLogger(GtfsGraphQLIndex.class);

private static final GraphQLSchema indexSchema = buildSchema();

protected static GraphQLSchema buildSchema() {
try {
URL url = Objects.requireNonNull(GtfsGraphQLIndex.class.getResource("schema.graphqls"));
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(url.openStream());
IntrospectionTypeWiring typeWiring = new IntrospectionTypeWiring(typeRegistry);
RuntimeWiring runtimeWiring = RuntimeWiring
.newRuntimeWiring()
.scalar(GraphQLScalars.DURATION_SCALAR)
.scalar(GraphQLScalars.POLYLINE_SCALAR)
.scalar(GraphQLScalars.GEOJSON_SCALAR)
.scalar(GraphQLScalars.GRAPHQL_ID_SCALAR)
.scalar(GraphQLScalars.GRAMS_SCALAR)
.scalar(GraphQLScalars.OFFSET_DATETIME_SCALAR)
.scalar(GraphQLScalars.RATIO_SCALAR)
.scalar(GraphQLScalars.COORDINATE_VALUE_SCALAR)
.scalar(GraphQLScalars.COST_SCALAR)
.scalar(GraphQLScalars.RELUCTANCE_SCALAR)
.scalar(GraphQLScalars.LOCAL_DATE_SCALAR)
.scalar(ExtendedScalars.GraphQLLong)
.scalar(ExtendedScalars.Locale)
.scalar(
ExtendedScalars
.newAliasedScalar("Speed")
.aliasedScalar(ExtendedScalars.NonNegativeFloat)
.build()
)
.type("Node", type -> type.typeResolver(new NodeTypeResolver()))
.type("PlaceInterface", type -> type.typeResolver(new PlaceInterfaceTypeResolver()))
.type("RentalPlace", type -> type.typeResolver(new RentalPlaceTypeResolver()))
.type("StopPosition", type -> type.typeResolver(new StopPosition() {}))
.type("FareProduct", type -> type.typeResolver(new FareProductTypeResolver()))
.type("AlertEntity", type -> type.typeResolver(new AlertEntityTypeResolver()))
.type("CallStopLocation", type -> type.typeResolver(new CallStopLocationTypeResolver()))
.type("CallScheduledTime", type -> type.typeResolver(new CallScheduledTimeTypeResolver()))
.type(typeWiring.build(AgencyImpl.class))
.type(typeWiring.build(AlertImpl.class))
.type(typeWiring.build(BikeParkImpl.class))
.type(typeWiring.build(VehicleParkingImpl.class))
.type(typeWiring.build(BikeRentalStationImpl.class))
.type(typeWiring.build(CarParkImpl.class))
.type(typeWiring.build(CoordinatesImpl.class))
.type(typeWiring.build(debugOutputImpl.class))
.type(typeWiring.build(DepartureRowImpl.class))
.type(typeWiring.build(elevationProfileComponentImpl.class))
.type(typeWiring.build(FeedImpl.class))
.type(typeWiring.build(GeometryImpl.class))
.type(typeWiring.build(ItineraryImpl.class))
.type(typeWiring.build(LegImpl.class))
.type(typeWiring.build(PatternImpl.class))
.type(typeWiring.build(PlaceImpl.class))
.type(typeWiring.build(placeAtDistanceImpl.class))
.type(typeWiring.build(PlanConnectionImpl.class))
.type(typeWiring.build(PlanImpl.class))
.type(typeWiring.build(QueryTypeImpl.class))
.type(typeWiring.build(RouteImpl.class))
.type(typeWiring.build(serviceTimeRangeImpl.class))
.type(typeWiring.build(stepImpl.class))
.type(typeWiring.build(StopImpl.class))
.type(typeWiring.build(stopAtDistanceImpl.class))
.type(typeWiring.build(StoptimeImpl.class))
.type(typeWiring.build(StoptimesInPatternImpl.class))
.type(typeWiring.build(TicketTypeImpl.class))
.type(typeWiring.build(TranslatedStringImpl.class))
.type(typeWiring.build(TripImpl.class))
.type(typeWiring.build(SystemNoticeImpl.class))
.type(typeWiring.build(ContactInfoImpl.class))
.type(typeWiring.build(BookingTimeImpl.class))
.type(typeWiring.build(BookingInfoImpl.class))
.type(typeWiring.build(VehicleRentalStationImpl.class))
.type(typeWiring.build(VehicleRentalNetworkImpl.class))
.type(typeWiring.build(RentalVehicleImpl.class))
.type(typeWiring.build(RentalVehicleTypeImpl.class))
.type(typeWiring.build(StopOnRouteImpl.class))
.type(typeWiring.build(StopOnTripImpl.class))
.type(typeWiring.build(UnknownImpl.class))
.type(typeWiring.build(RouteTypeImpl.class))
.type(typeWiring.build(RoutingErrorImpl.class))
.type(typeWiring.build(StopGeometriesImpl.class))
.type(typeWiring.build(VehiclePositionImpl.class))
.type(typeWiring.build(StopRelationshipImpl.class))
.type(typeWiring.build(OpeningHoursImpl.class))
.type(typeWiring.build(RideHailingEstimateImpl.class))
.type(typeWiring.build(MoneyImpl.class))
.type(typeWiring.build(CurrencyImpl.class))
.type(typeWiring.build(FareProductUseImpl.class))
.type(typeWiring.build(DefaultFareProductImpl.class))
.type(typeWiring.build(TripOnServiceDateImpl.class))
.type(typeWiring.build(StopCallImpl.class))
.type(typeWiring.build(TripOccupancyImpl.class))
.type(typeWiring.build(LegTimeImpl.class))
.type(typeWiring.build(RealTimeEstimateImpl.class))
.type(typeWiring.build(EstimatedTimeImpl.class))
.build();
SchemaGenerator schemaGenerator = new SchemaGenerator();
return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
} catch (Exception e) {
LOG.error("Unable to build GTFS GraphQL Schema", e);
}
return null;
}

static ExecutionResult getGraphQLExecutionResult(
String query,
Map<String, Object> variables,
Expand All @@ -224,7 +43,7 @@ static ExecutionResult getGraphQLExecutionResult(
}

GraphQL graphQL = GraphQL
.newGraphQL(indexSchema)
.newGraphQL(requestContext.schemaService().schema())
.instrumentation(instrumentation)
.defaultDataFetcherExceptionHandler(new LoggingDataFetcherExceptionHandler())
.build();
Expand Down
Loading
Loading