diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/BookingRulesEntityValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/BookingRulesEntityValidator.java new file mode 100644 index 0000000000..0c7983b2c6 --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/BookingRulesEntityValidator.java @@ -0,0 +1,86 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import java.util.ArrayList; +import java.util.List; +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice; +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice.FileRefs; +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.GtfsBookingRules; +import org.mobilitydata.gtfsvalidator.table.GtfsBookingRulesSchema; +import org.mobilitydata.gtfsvalidator.table.GtfsBookingType; + +@GtfsValidator +public class BookingRulesEntityValidator extends SingleEntityValidator { + + @Override + public void validate(GtfsBookingRules entity, NoticeContainer noticeContainer) { + // Only validate entities with REALTIME booking type + if (entity.bookingType() != GtfsBookingType.REALTIME) { + return; + } + + // Retrieve the list of forbidden fields + List forbiddenFields = findForbiddenRealTimeFields(entity); + + // If there are any forbidden fields, add a validation notice + if (!forbiddenFields.isEmpty()) { + noticeContainer.addValidationNotice( + new ForbiddenRealTimeBookingFieldValueNotice(entity, forbiddenFields)); + } + } + + /** Finds forbidden fields that should not be present for real-time booking rules. */ + public static List findForbiddenRealTimeFields(GtfsBookingRules bookingRule) { + List fields = new ArrayList<>(); + + // Check each field and add its name to the list if it's present + if (bookingRule.hasPriorNoticeDurationMin()) { + fields.add(GtfsBookingRules.PRIOR_NOTICE_DURATION_MIN_FIELD_NAME); + } + if (bookingRule.hasPriorNoticeDurationMax()) { + fields.add(GtfsBookingRules.PRIOR_NOTICE_DURATION_MAX_FIELD_NAME); + } + if (bookingRule.hasPriorNoticeLastDay()) { + fields.add(GtfsBookingRules.PRIOR_NOTICE_LAST_DAY_FIELD_NAME); + } + if (bookingRule.hasPriorNoticeLastTime()) { + fields.add(GtfsBookingRules.PRIOR_NOTICE_LAST_TIME_FIELD_NAME); + } + if (bookingRule.hasPriorNoticeStartDay()) { + fields.add(GtfsBookingRules.PRIOR_NOTICE_START_DAY_FIELD_NAME); + } + if (bookingRule.hasPriorNoticeStartTime()) { + fields.add(GtfsBookingRules.PRIOR_NOTICE_START_TIME_FIELD_NAME); + } + if (bookingRule.hasPriorNoticeServiceId()) { + fields.add(GtfsBookingRules.PRIOR_NOTICE_SERVICE_ID_FIELD_NAME); + } + + return fields; + } + + /** A forbidden field value is present for a real-time booking rule in `booking_rules.txt`. */ + @GtfsValidationNotice( + severity = SeverityLevel.ERROR, + files = @FileRefs(GtfsBookingRulesSchema.class)) + static class ForbiddenRealTimeBookingFieldValueNotice extends ValidationNotice { + /** The row number of the faulty record. */ + private final int csvRowNumber; + + /** The `booking_rules.booking_rule_id` of the faulty record. */ + private final String bookingRuleId; + + /** The names of the forbidden fields comma-separated. */ + private final String fieldNames; + + ForbiddenRealTimeBookingFieldValueNotice( + GtfsBookingRules bookingRule, List forbiddenFields) { + this.csvRowNumber = bookingRule.csvRowNumber(); + this.bookingRuleId = bookingRule.bookingRuleId(); + this.fieldNames = String.join(", ", forbiddenFields); + } + } +} diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/BookingRulesEntityValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/BookingRulesEntityValidatorTest.java new file mode 100644 index 0000000000..98ba39b459 --- /dev/null +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/BookingRulesEntityValidatorTest.java @@ -0,0 +1,92 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import static com.google.common.truth.Truth.assertThat; +import static org.mobilitydata.gtfsvalidator.table.GtfsBookingType.REALTIME; + +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; +import org.mobilitydata.gtfsvalidator.table.GtfsBookingRules; +import org.mobilitydata.gtfsvalidator.table.GtfsBookingType; +import org.mobilitydata.gtfsvalidator.type.GtfsTime; +import org.mobilitydata.gtfsvalidator.validator.BookingRulesEntityValidator.ForbiddenRealTimeBookingFieldValueNotice; + +@RunWith(JUnit4.class) +public class BookingRulesEntityValidatorTest { + + private static List generateNotices(GtfsBookingRules bookingRule) { + NoticeContainer noticeContainer = new NoticeContainer(); + BookingRulesEntityValidator validator = new BookingRulesEntityValidator(); + validator.validate(bookingRule, noticeContainer); + return noticeContainer.getValidationNotices(); + } + + @Test + public void realTimeBookingWithForbiddenFieldsShouldGenerateNotice() { + GtfsBookingRules bookingRule = + new GtfsBookingRules.Builder() + .setCsvRowNumber(1) + .setBookingRuleId("rule-1") + .setBookingType(REALTIME) + .setPriorNoticeDurationMin(30) // Forbidden field + .setPriorNoticeLastDay(2) // Forbidden field + .build(); + + assertThat(generateNotices(bookingRule)) + .containsExactly( + new ForbiddenRealTimeBookingFieldValueNotice( + bookingRule, + List.of( + GtfsBookingRules.PRIOR_NOTICE_DURATION_MIN_FIELD_NAME, + GtfsBookingRules.PRIOR_NOTICE_LAST_DAY_FIELD_NAME))); + } + + @Test + public void realTimeBookingWithoutForbiddenFieldsShouldNotGenerateNotice() { + GtfsBookingRules bookingRule = + new GtfsBookingRules.Builder() + .setCsvRowNumber(1) + .setBookingRuleId("rule-2") + .setBookingType(REALTIME) + .build(); + + assertThat(generateNotices(bookingRule)).isEmpty(); + } + + @Test + public void scheduledBookingShouldNotGenerateNotice() { + GtfsBookingRules bookingRule = + new GtfsBookingRules.Builder() + .setCsvRowNumber(1) + .setBookingRuleId("rule-3") + .setBookingType(GtfsBookingType.SAMEDAY) + .build(); + + assertThat(generateNotices(bookingRule)).isEmpty(); + } + + @Test + public void realTimeBookingWithMultipleForbiddenFieldsShouldGenerateNotice() { + GtfsBookingRules bookingRule = + new GtfsBookingRules.Builder() + .setCsvRowNumber(1) + .setBookingRuleId("rule-4") + .setBookingType(REALTIME) + .setPriorNoticeDurationMax(60) // Forbidden field + .setPriorNoticeStartTime(GtfsTime.fromSecondsSinceMidnight(2)) // Forbidden field + .setPriorNoticeServiceId("service-1") // Forbidden field + .build(); + + assertThat(generateNotices(bookingRule)) + .containsExactly( + new ForbiddenRealTimeBookingFieldValueNotice( + bookingRule, + List.of( + GtfsBookingRules.PRIOR_NOTICE_DURATION_MAX_FIELD_NAME, + GtfsBookingRules.PRIOR_NOTICE_START_TIME_FIELD_NAME, + GtfsBookingRules.PRIOR_NOTICE_SERVICE_ID_FIELD_NAME))); + } +} diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java index 0e55eda83f..3ce85cb493 100644 --- a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java @@ -199,7 +199,9 @@ public void testNoticeClassFieldNames() { "pathwayMode", "isBidirectional", "locationGroupId", - "locationId"); + "locationId", + "bookingRuleId", + "fieldNames"); } private static List discoverValidationNoticeFieldNames() {