diff --git a/RULES.md b/RULES.md index 266dea9d02..92698a19c5 100644 --- a/RULES.md +++ b/RULES.md @@ -85,6 +85,9 @@ Each Notice is associated with a severity: `INFO`, `WARNING`, `ERROR`. | [`stop_time_with_arrival_before_previous_departure_time`](#stop_time_with_arrival_before_previous_departure_time) | Backwards time travel between stops in `stop_times.txt` | | [`stop_time_with_only_arrival_or_departure_time`](#stop_time_with_only_arrival_or_departure_time) | Missing `stop_times.arrival_time` or `stop_times.departure_time`. | | [`stop_without_zone_id`](#stop_without_zone_id) | Stop without value for `stops.zone_id`. | +| [`transfer_with_invalid_stop_location_type`](#transfer_with_invalid_stop_location_type) | A stop id field from GTFS file `transfers.txt` references a stop that has a `location_type` other than 0 or 1 (aka Stop/Platform or Station). | +| [`transfer_with_invalid_trip_and_route`](#transfer_with_invalid_trip_and_route) | A trip id field from GTFS file `transfers.txt` references a route that does not match its `trips.txt` `route_id`. | +| [`transfer_with_invalid_trip_and_stop`](#transfer_with_invalid_trip_and_stop) | A trip id field from GTFS file `transfers.txt` references a stop that is not included in the referenced trip's stop-times. | | [`translation_foreign_key_violation`](#translation_foreign_key_violation) | An entity with the given `record_id` and `record_sub_id` cannot be found in the referenced table. | | [`translation_unexpected_value`](#translation_unexpected_value) | A field in a translations row has value but must be empty. | | [`wrong_parent_location_type`](#wrong_parent_location_type) | Incorrect type of the parent location. | @@ -1455,6 +1458,73 @@ If `fare_rules.txt` is provided, and `fare_rules.txt` uses at least one column a + + +### transfer_with_invalid_stop_location_type + +A `from_stop_id` or `to_stop_id` field from GTFS file `transfers.txt` references a stop that has a `location_type` other than 0 or 1 (aka Stop/Platform or Station). + +#### References +* [transfers.txt specification](http://gtfs.org/reference/static/#transferstxt) + +
+ +#### Notice fields description +| Field name | Description | Type | +|---------------------|---------------------------------------------------------------------------|--------| +| `csvRowNumber` | The row number from `transfers.txt` for the faulty entry. | long | +| `stopIdFieldName` | The name of the stop id field (e.g. `from_stop_id`) referencing the stop. | String | +| `stopId` | The referenced stop id. | String | +| `locationTypeValue` | The numeric value of the invalid location type. | int | +| `locationTypeName` | The name of the invalid location type. | String | + +
+ +
+ +### transfer_with_invalid_trip_and_route + +A `from_trip_id` or `to_trip_id` field from GTFS file `transfers.txt` references a route that does not match its `trips.txt` `route_id`. + +#### References +* [transfers.txt specification](http://gtfs.org/reference/static/#transferstxt) + +
+ +#### Notice fields description +| Field name | Description | Type | +|-------------------|------------------------------------------------------------------------------|--------| +| `csvRowNumber` | The row number from `transfers.txt` for the faulty entry. | long | +| `tripFieldName` | The name of the trip id field (e.g. `from_trip_id`) referencing a trip. | String | +| `tripId` | The referenced trip id. | String | +| `routeFieldName` | The name of the route id field (e.g. `from_route_id`) referencing the route. | String | +| `routeId` | The referenced route id. | String | +| `expectedRouteId` | The expected route id from `trips.txt`. | String | + +
+ +
+ +### transfer_with_invalid_trip_and_stop + +A `from_trip_id` or `to_trip_id` field from GTFS file `transfers.txt` references a stop that is not included in the referenced trip's stop-times. + +#### References +* [transfers.txt specification](http://gtfs.org/reference/static/#transferstxt) + +
+ +#### Notice fields description +| Field name | Description | Type | +|-----------------|----------------------------------------------------------------------------|--------| +| `csvRowNumber` | The row number from `transfers.txt` for the faulty entry. | long | +| `tripFieldName` | The name of the trip id field (e.g. `from_trip_id`) referencing a trip. | String | +| `tripId` | The referenced trip id. | String | +| `stopFieldName` | The name of the stop id field (e.g. `stop_route_id`) referencing the stop. | String | +| `stopId` | The referenced stop id. | String | + +
+
### translation_foreign_key_violation diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TransfersStopTypeValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TransfersStopTypeValidator.java new file mode 100644 index 0000000000..c4a38c7399 --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TransfersStopTypeValidator.java @@ -0,0 +1,102 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import java.util.Optional; +import javax.inject.Inject; +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.notice.SeverityLevel; +import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; +import org.mobilitydata.gtfsvalidator.table.GtfsLocationType; +import org.mobilitydata.gtfsvalidator.table.GtfsStop; +import org.mobilitydata.gtfsvalidator.table.GtfsStopTableContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsTransfer; +import org.mobilitydata.gtfsvalidator.table.GtfsTransferTableContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsTransferTableLoader; + +/** + * Validates that {@code transfers.from_stop_id} and {@code to_stop_id} reference stops or stations. + */ +@GtfsValidator +public class TransfersStopTypeValidator extends FileValidator { + private final GtfsTransferTableContainer transfersContainer; + private final GtfsStopTableContainer stopsContainer; + + @Inject + public TransfersStopTypeValidator( + GtfsTransferTableContainer transfersContainer, GtfsStopTableContainer stopsContainer) { + this.transfersContainer = transfersContainer; + this.stopsContainer = stopsContainer; + } + + @Override + public void validate(NoticeContainer noticeContainer) { + for (GtfsTransfer entity : transfersContainer.getEntities()) { + validateEntity(entity, noticeContainer); + } + } + + public void validateEntity(GtfsTransfer entity, NoticeContainer noticeContainer) { + validateStopType( + entity, + GtfsTransferTableLoader.FROM_STOP_ID_FIELD_NAME, + entity.fromStopId(), + noticeContainer); + validateStopType( + entity, GtfsTransferTableLoader.TO_STOP_ID_FIELD_NAME, entity.toStopId(), noticeContainer); + } + + private void validateStopType( + GtfsTransfer entity, String stopIdFieldName, String stopId, NoticeContainer noticeContainer) { + Optional optStop = stopsContainer.byStopId(stopId); + if (optStop.isEmpty()) { + // Foreign key reference is validated elsewhere. + return; + } + + GtfsLocationType locationType = optStop.get().locationType(); + if (!isValidTransferStopType(locationType)) { + noticeContainer.addValidationNotice( + new TransferWithInvalidStopLocationTypeNotice( + entity.csvRowNumber(), stopIdFieldName, stopId, locationType)); + } + } + + private static boolean isValidTransferStopType(GtfsLocationType locationType) { + switch (locationType) { + case STOP: + case STATION: + return true; + default: + return false; + } + } + + /** + * A `from_stop_id` or `to_stop_id` field from GTFS file `transfers.txt` references a stop that + * has a `location_type` other than 0 or 1 (aka Stop/Platform or Station). + * + *

Severity: {@code SeverityLevel.ERROR} + */ + public static final class TransferWithInvalidStopLocationTypeNotice extends ValidationNotice { + // The row number from `transfers.txt` for the faulty entry. + private final long csvRowNumber; + // The name of the stop id field (e.g. `from_stop_id`) referencing the stop. + private final String stopIdFieldName; + // The referenced stop id. + private final String stopId; + // The numeric value of the invalid location type. + private final int locationTypeValue; + // The name of the invalid location type. + private String locationTypeName; + + public TransferWithInvalidStopLocationTypeNotice( + long csvRowNumber, String stopIdFieldName, String stopId, GtfsLocationType locationType) { + super(SeverityLevel.ERROR); + this.csvRowNumber = csvRowNumber; + this.stopIdFieldName = stopIdFieldName; + this.stopId = stopId; + this.locationTypeValue = locationType.getNumber(); + this.locationTypeName = locationType.toString(); + } + } +} diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TransfersTripReferenceValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TransfersTripReferenceValidator.java new file mode 100644 index 0000000000..a2ff52df55 --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TransfersTripReferenceValidator.java @@ -0,0 +1,219 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import com.google.common.collect.ImmutableSet; +import java.util.List; +import java.util.Optional; +import javax.inject.Inject; +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.notice.SeverityLevel; +import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; +import org.mobilitydata.gtfsvalidator.table.GtfsLocationType; +import org.mobilitydata.gtfsvalidator.table.GtfsStop; +import org.mobilitydata.gtfsvalidator.table.GtfsStopTableContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsStopTime; +import org.mobilitydata.gtfsvalidator.table.GtfsStopTimeTableContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsTransfer; +import org.mobilitydata.gtfsvalidator.table.GtfsTransferTableContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsTransferTableLoader; +import org.mobilitydata.gtfsvalidator.table.GtfsTrip; +import org.mobilitydata.gtfsvalidator.table.GtfsTripTableContainer; + +/** + * Validates that if a transfers.txt entry references a trip, then any corresponding route reference + * or stop reference for the transfer are actually associated with the trip. + */ +@GtfsValidator +public class TransfersTripReferenceValidator extends FileValidator { + + private final GtfsTransferTableContainer transfersContainer; + private final GtfsTripTableContainer tripsContainer; + private final GtfsStopTimeTableContainer stopTimeContainer; + private final GtfsStopTableContainer stopsContainer; + + @Inject + public TransfersTripReferenceValidator( + GtfsTransferTableContainer transfersContainer, + GtfsTripTableContainer tripsContainer, + GtfsStopTimeTableContainer stopTimeContainer, + GtfsStopTableContainer stopsContainer) { + this.transfersContainer = transfersContainer; + this.tripsContainer = tripsContainer; + this.stopTimeContainer = stopTimeContainer; + this.stopsContainer = stopsContainer; + } + + @Override + public void validate(NoticeContainer noticeContainer) { + for (GtfsTransfer transfer : transfersContainer.getEntities()) { + validateEntity(transfer, noticeContainer); + } + } + + public void validateEntity(GtfsTransfer entity, NoticeContainer noticeContainer) { + validateTripReferences( + entity, + GtfsTransferTableLoader.FROM_TRIP_ID_FIELD_NAME, + optional(entity.hasFromTripId(), entity.fromTripId()), + GtfsTransferTableLoader.FROM_ROUTE_ID_FIELD_NAME, + optional(entity.hasFromRouteId(), entity.fromRouteId()), + GtfsTransferTableLoader.FROM_STOP_ID_FIELD_NAME, + optional(entity.hasFromStopId(), entity.fromStopId()), + noticeContainer); + validateTripReferences( + entity, + GtfsTransferTableLoader.TO_TRIP_ID_FIELD_NAME, + optional(entity.hasToTripId(), entity.toTripId()), + GtfsTransferTableLoader.TO_ROUTE_ID_FIELD_NAME, + optional(entity.hasToRouteId(), entity.toRouteId()), + GtfsTransferTableLoader.TO_STOP_ID_FIELD_NAME, + optional(entity.hasToStopId(), entity.toStopId()), + noticeContainer); + } + + void validateTripReferences( + GtfsTransfer entity, + String tripFieldName, + Optional tripId, + String routeFieldName, + Optional routeId, + String stopFieldName, + Optional stopId, + NoticeContainer noticeContainer) { + if (tripId.isEmpty()) { + return; + } + Optional optTrip = tripsContainer.byTripId(tripId.get()); + if (optTrip.isEmpty()) { + // The foreign key reference is validated elsewhere. + return; + } + GtfsTrip trip = optTrip.get(); + if (routeId.isPresent()) { + if (!trip.routeId().equals(routeId.get())) { + noticeContainer.addValidationNotice( + new TransferWithInvalidTripAndRouteNotice( + entity.csvRowNumber(), + tripFieldName, + tripId.get(), + routeFieldName, + routeId.get(), + trip.routeId())); + } + } + if (stopId.isPresent()) { + validateTripStopReference( + entity, tripFieldName, tripId, stopFieldName, stopId.get(), noticeContainer); + } + } + + private void validateTripStopReference( + GtfsTransfer entity, + String tripFieldName, + Optional tripId, + String stopFieldName, + String stopId, + NoticeContainer noticeContainer) { + Optional optStop = stopsContainer.byStopId(stopId); + if (optStop.isEmpty()) { + // The foreign key reference is validated elsewhere. + return; + } + ImmutableSet stops = expandStationIfNeeded(optStop.get()); + ImmutableSet ids = + stops.stream().map(GtfsStop::stopId).collect(ImmutableSet.toImmutableSet()); + + List stopTimes = stopTimeContainer.byTripId(tripId.get()); + if (!stopTimes.stream().anyMatch((st) -> ids.contains(st.stopId()))) { + noticeContainer.addValidationNotice( + new TransferWithInvalidTripAndStopNotice( + entity.csvRowNumber(), tripFieldName, tripId.get(), stopFieldName, stopId)); + } + } + + private static Optional optional(boolean hasValue, T value) { + return hasValue ? Optional.of(value) : Optional.empty(); + } + + private ImmutableSet expandStationIfNeeded(GtfsStop stop) { + if (stop.locationType() == GtfsLocationType.STOP) { + return ImmutableSet.of(stop); + } else if (stop.locationType() == GtfsLocationType.STATION) { + List stops = stopsContainer.byParentStation(stop.stopId()); + return ImmutableSet.copyOf(stops); + } else { + // Invalid stop location types are validated elsewhere. + return ImmutableSet.of(); + } + } + + /** + * A `from_trip_id` or `to_trip_id` field from GTFS file `transfers.txt` references a route that + * does not match its `trips.txt` `route_id`. + * + *

Severity: {@code SeverityLevel.ERROR} + */ + public static class TransferWithInvalidTripAndRouteNotice extends ValidationNotice { + // The row number from `transfers.txt` for the faulty entry. + private final long csvRowNumber; + // The name of the trip id field (e.g. `from_trip_id`) referencing a trip. + private final String tripFieldName; + // The referenced trip id. + private final String tripId; + // The name of the route id field (e.g. `from_route_id`) referencing the route. + private final String routeFieldName; + // The referenced route id. + private final String routeId; + // The expected route id from `trips.txt`. + private final String expectedRouteId; + + public TransferWithInvalidTripAndRouteNotice( + long csvRowNumber, + String tripFieldName, + String tripId, + String routeFieldName, + String routeId, + String expectedRouteId) { + super(SeverityLevel.ERROR); + this.csvRowNumber = csvRowNumber; + this.tripFieldName = tripFieldName; + this.tripId = tripId; + this.routeFieldName = routeFieldName; + this.routeId = routeId; + this.expectedRouteId = expectedRouteId; + } + } + + /** + * A `from_trip_id` or `to_trip_id` field from GTFS file `transfers.txt` references a stop that is + * not included in the referenced trip's stop-times. + * + *

Severity: {@code SeverityLevel.ERROR} + */ + public static class TransferWithInvalidTripAndStopNotice extends ValidationNotice { + // The row number from `transfers.txt` for the faulty entry. + private final long csvRowNumber; + // The name of the trip id field (e.g. `from_trip_id`) referencing a trip. + private final String tripFieldName; + // The referenced trip id. + private final String tripId; + // The name of the stop id field (e.g. `stop_route_id`) referencing the stop. + private final String stopFieldName; + // The referenced stop id. + private final String stopId; + + public TransferWithInvalidTripAndStopNotice( + long csvRowNumber, + String tripFieldName, + String tripId, + String stopFieldName, + String stopId) { + super(SeverityLevel.ERROR); + this.csvRowNumber = csvRowNumber; + this.tripFieldName = tripFieldName; + this.tripId = tripId; + this.stopFieldName = stopFieldName; + this.stopId = stopId; + } + } +} diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TransfersStopTypeValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TransfersStopTypeValidatorTest.java new file mode 100644 index 0000000000..06dccbfd8b --- /dev/null +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TransfersStopTypeValidatorTest.java @@ -0,0 +1,76 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsLocationType; +import org.mobilitydata.gtfsvalidator.table.GtfsStop.Builder; +import org.mobilitydata.gtfsvalidator.table.GtfsStopTableContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsTransfer; +import org.mobilitydata.gtfsvalidator.table.GtfsTransferTableContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsTransferType; +import org.mobilitydata.gtfsvalidator.validator.TransfersStopTypeValidator.TransferWithInvalidStopLocationTypeNotice; + +public class TransfersStopTypeValidatorTest { + + private NoticeContainer noticeContainer = new NoticeContainer(); + + @Test + public void testStopToStationTransfer() { + // Transfers between a stop and a station are allowed. + GtfsStopTableContainer stops = + GtfsStopTableContainer.forEntities( + ImmutableList.of( + new Builder().setStopId("s0").setLocationType(GtfsLocationType.STOP).build(), + new Builder().setStopId("s1").setLocationType(GtfsLocationType.STATION).build()), + noticeContainer); + GtfsTransferTableContainer transfers = + GtfsTransferTableContainer.forEntities( + ImmutableList.of( + new GtfsTransfer.Builder() + .setFromStopId("s0") + .setToStopId("s1") + .setTransferType(GtfsTransferType.RECOMMENDED) + .build()), + noticeContainer); + + new TransfersStopTypeValidator(transfers, stops).validate(noticeContainer); + + assertThat(noticeContainer.getValidationNotices()).isEmpty(); + } + + @Test + public void testEntranceToGenericNodeTransfer() { + // Transfers between an entrance and a generic pathway node are NOT allowed. + GtfsStopTableContainer stops = + GtfsStopTableContainer.forEntities( + ImmutableList.of( + new Builder().setStopId("s0").setLocationType(GtfsLocationType.ENTRANCE).build(), + new Builder() + .setStopId("s1") + .setLocationType(GtfsLocationType.GENERIC_NODE) + .build()), + noticeContainer); + GtfsTransferTableContainer transfers = + GtfsTransferTableContainer.forEntities( + ImmutableList.of( + new GtfsTransfer.Builder() + .setCsvRowNumber(2) + .setFromStopId("s0") + .setToStopId("s1") + .setTransferType(GtfsTransferType.RECOMMENDED) + .build()), + noticeContainer); + + new TransfersStopTypeValidator(transfers, stops).validate(noticeContainer); + + assertThat(noticeContainer.getValidationNotices()) + .containsExactly( + new TransferWithInvalidStopLocationTypeNotice( + 2, "from_stop_id", "s0", GtfsLocationType.ENTRANCE), + new TransferWithInvalidStopLocationTypeNotice( + 2, "to_stop_id", "s1", GtfsLocationType.GENERIC_NODE)); + } +} diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TransfersTripReferenceValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TransfersTripReferenceValidatorTest.java new file mode 100644 index 0000000000..f541be2e17 --- /dev/null +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TransfersTripReferenceValidatorTest.java @@ -0,0 +1,122 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsLocationType; +import org.mobilitydata.gtfsvalidator.table.GtfsStop; +import org.mobilitydata.gtfsvalidator.table.GtfsStopTableContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsStopTime; +import org.mobilitydata.gtfsvalidator.table.GtfsStopTimeTableContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsTransfer; +import org.mobilitydata.gtfsvalidator.table.GtfsTransferTableContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsTransferType; +import org.mobilitydata.gtfsvalidator.table.GtfsTrip.Builder; +import org.mobilitydata.gtfsvalidator.table.GtfsTripTableContainer; +import org.mobilitydata.gtfsvalidator.validator.TransfersTripReferenceValidator.TransferWithInvalidTripAndRouteNotice; +import org.mobilitydata.gtfsvalidator.validator.TransfersTripReferenceValidator.TransferWithInvalidTripAndStopNotice; + +public class TransfersTripReferenceValidatorTest { + + private NoticeContainer noticeContainer = new NoticeContainer(); + + @Test + public void testValidTripReferences() { + // Trips with valid stop and route references. For the second trip, the transfer references + // the parent station for a stop reference. + GtfsTripTableContainer trips = + GtfsTripTableContainer.forEntities( + ImmutableList.of( + new Builder().setTripId("t0").setRouteId("r0").build(), + new Builder().setTripId("t1").setRouteId("r1").build()), + noticeContainer); + GtfsStopTableContainer stops = + GtfsStopTableContainer.forEntities( + ImmutableList.of( + new GtfsStop.Builder().setStopId("s0").build(), + new GtfsStop.Builder().setStopId("s1_stop").setParentStation("s1_station").build(), + new GtfsStop.Builder() + .setStopId("s1_station") + .setLocationType(GtfsLocationType.STATION) + .build()), + noticeContainer); + GtfsStopTimeTableContainer stopTimes = + GtfsStopTimeTableContainer.forEntities( + ImmutableList.of( + new GtfsStopTime.Builder().setTripId("t0").setStopId("s0").build(), + new GtfsStopTime.Builder().setTripId("t1").setStopId("s1_stop").build()), + noticeContainer); + GtfsTransferTableContainer transfers = + GtfsTransferTableContainer.forEntities( + ImmutableList.of( + new GtfsTransfer.Builder() + .setCsvRowNumber(2) + .setFromStopId("s0") + .setFromRouteId("r0") + .setFromTripId("t0") + .setToStopId("s1_station") + .setToRouteId("r1") + .setToTripId("t1") + .setTransferType(GtfsTransferType.IMPOSSIBLE) + .build()), + noticeContainer); + + new TransfersTripReferenceValidator(transfers, trips, stopTimes, stops) + .validate(noticeContainer); + + assertThat(noticeContainer.getValidationNotices()).isEmpty(); + } + + @Test + public void testInvalidTripReferences() { + // Trips with invalid stop and route references. For the from-trip, the route id reference is + // invalid. For to-trip, the stop id reference doesn't match the stop-times associated with the + // trip. + GtfsTripTableContainer trips = + GtfsTripTableContainer.forEntities( + ImmutableList.of( + new Builder().setTripId("t0").setRouteId("r0").build(), + new Builder().setTripId("t1").setRouteId("r1").build()), + noticeContainer); + GtfsStopTableContainer stops = + GtfsStopTableContainer.forEntities( + ImmutableList.of( + new GtfsStop.Builder().setStopId("s0").build(), + new GtfsStop.Builder().setStopId("s1").build(), + new GtfsStop.Builder().setStopId("s2").build()), + noticeContainer); + GtfsStopTimeTableContainer stopTimes = + GtfsStopTimeTableContainer.forEntities( + ImmutableList.of( + new GtfsStopTime.Builder().setTripId("t0").setStopId("s0").build(), + new GtfsStopTime.Builder().setTripId("t1").setStopId("s1").build()), + noticeContainer); + GtfsTransferTableContainer transfers = + GtfsTransferTableContainer.forEntities( + ImmutableList.of( + new GtfsTransfer.Builder() + .setCsvRowNumber(2) + .setFromStopId("s0") + // This is not the expected route id. + .setFromRouteId("DNE") + .setFromTripId("t0") + // This stop is not associated with the trip's stop-times. + .setToStopId("s2") + .setToRouteId("r1") + .setToTripId("t1") + .setTransferType(GtfsTransferType.IMPOSSIBLE) + .build()), + noticeContainer); + + new TransfersTripReferenceValidator(transfers, trips, stopTimes, stops) + .validate(noticeContainer); + + assertThat(noticeContainer.getValidationNotices()) + .containsExactly( + new TransferWithInvalidTripAndRouteNotice( + 2, "from_trip_id", "t0", "from_route_id", "DNE", "r0"), + new TransferWithInvalidTripAndStopNotice(2, "to_trip_id", "t1", "to_stop_id", "s2")); + } +}