> getResolvedValidationNotices() {
return validationNotices;
}
diff --git a/core/src/main/java/org/mobilitydata/gtfsvalidator/notice/ResolvedNotice.java b/core/src/main/java/org/mobilitydata/gtfsvalidator/notice/ResolvedNotice.java
index feb26b2271..04a61be24d 100644
--- a/core/src/main/java/org/mobilitydata/gtfsvalidator/notice/ResolvedNotice.java
+++ b/core/src/main/java/org/mobilitydata/gtfsvalidator/notice/ResolvedNotice.java
@@ -81,4 +81,16 @@ public int hashCode() {
public boolean isError() {
return getSeverityLevel().ordinal() >= SeverityLevel.ERROR.ordinal();
}
+
+ /**
+ * Tells if this notice is a {@code WARNING}.
+ *
+ * This method is preferred to checking {@code severityLevel} directly since more levels may be
+ * added in the future.
+ *
+ * @return true if this notice is a warning, false otherwise
+ */
+ public boolean isWarning() {
+ return getSeverityLevel() == SeverityLevel.WARNING;
+ }
}
diff --git a/core/src/main/java/org/mobilitydata/gtfsvalidator/table/AnyTableLoader.java b/core/src/main/java/org/mobilitydata/gtfsvalidator/table/AnyTableLoader.java
index ed2baa720b..78dd9abfec 100644
--- a/core/src/main/java/org/mobilitydata/gtfsvalidator/table/AnyTableLoader.java
+++ b/core/src/main/java/org/mobilitydata/gtfsvalidator/table/AnyTableLoader.java
@@ -152,6 +152,10 @@ private static NoticeContainer validateHeaders(
.filter(GtfsColumnDescriptor::headerRequired)
.map(GtfsColumnDescriptor::columnName)
.collect(Collectors.toSet()),
+ columnDescriptors.stream()
+ .filter(GtfsColumnDescriptor::headerRecommended)
+ .map(GtfsColumnDescriptor::columnName)
+ .collect(Collectors.toSet()),
headerNotices);
return headerNotices;
}
diff --git a/core/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsColumnDescriptor.java b/core/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsColumnDescriptor.java
index d75c87933d..0546e479d1 100644
--- a/core/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsColumnDescriptor.java
+++ b/core/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsColumnDescriptor.java
@@ -11,6 +11,8 @@ public abstract class GtfsColumnDescriptor {
public abstract boolean headerRequired();
+ public abstract boolean headerRecommended();
+
public abstract FieldLevelEnum fieldLevel();
public abstract Optional numberBounds();
@@ -33,6 +35,8 @@ public abstract static class Builder {
public abstract Builder setHeaderRequired(boolean value);
+ public abstract Builder setHeaderRecommended(boolean value);
+
public abstract Builder setFieldLevel(FieldLevelEnum value);
public abstract Builder setNumberBounds(Optional value);
diff --git a/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/DefaultTableHeaderValidator.java b/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/DefaultTableHeaderValidator.java
index 6f9b993270..3ef0f174c6 100644
--- a/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/DefaultTableHeaderValidator.java
+++ b/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/DefaultTableHeaderValidator.java
@@ -23,6 +23,7 @@
import java.util.TreeSet;
import org.mobilitydata.gtfsvalidator.notice.DuplicatedColumnNotice;
import org.mobilitydata.gtfsvalidator.notice.EmptyColumnNameNotice;
+import org.mobilitydata.gtfsvalidator.notice.MissingRecommendedColumnNotice;
import org.mobilitydata.gtfsvalidator.notice.MissingRequiredColumnNotice;
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
import org.mobilitydata.gtfsvalidator.notice.UnknownColumnNotice;
@@ -36,14 +37,20 @@ public void validate(
CsvHeader actualHeader,
Set supportedColumns,
Set requiredColumns,
+ Set recommendedColumns,
NoticeContainer noticeContainer) {
if (actualHeader.getColumnCount() == 0) {
// This is an empty file.
return;
}
Map columnIndices = new HashMap<>();
- // Sorted tree set for stable order of notices.
+ // Sorted tree set of all the columns for stable order of notices.
+ // We remove the columns that are properly present and well formed from that set, and at the
+ // end only the missing required columns are left in the set.
TreeSet missingColumns = new TreeSet<>(requiredColumns);
+ // We also want to find the recommended columns that are absent. We use the same scheme for
+ // these.
+ TreeSet missingRecommendedColumns = new TreeSet<>(recommendedColumns);
for (int i = 0; i < actualHeader.getColumnCount(); ++i) {
String column = actualHeader.getColumnName(i);
// Column indices are zero-based. We add 1 to make them 1-based.
@@ -59,12 +66,23 @@ public void validate(
if (!supportedColumns.contains(column)) {
noticeContainer.addValidationNotice(new UnknownColumnNotice(filename, column, i + 1));
}
+
+ // If the column is present, it should not be in the missing required columns set
missingColumns.remove(column);
+
+ // If the column is present, it should not be in the missing recommended columns set
+ missingRecommendedColumns.remove(column);
}
if (!missingColumns.isEmpty()) {
for (String column : missingColumns) {
noticeContainer.addValidationNotice(new MissingRequiredColumnNotice(filename, column));
}
}
+
+ if (!missingRecommendedColumns.isEmpty()) {
+ for (String column : missingRecommendedColumns) {
+ noticeContainer.addValidationNotice(new MissingRecommendedColumnNotice(filename, column));
+ }
+ }
}
}
diff --git a/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/TableHeaderValidator.java b/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/TableHeaderValidator.java
index ef42d43770..3cc5732d2d 100644
--- a/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/TableHeaderValidator.java
+++ b/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/TableHeaderValidator.java
@@ -28,5 +28,6 @@ void validate(
CsvHeader actualHeader,
Set supportedHeaders,
Set requiredHeaders,
+ Set recommendedHeaders,
NoticeContainer noticeContainer);
}
diff --git a/core/src/test/java/org/mobilitydata/gtfsvalidator/parsing/RowParserTest.java b/core/src/test/java/org/mobilitydata/gtfsvalidator/parsing/RowParserTest.java
index c3c0db2cf7..260f3da2e2 100644
--- a/core/src/test/java/org/mobilitydata/gtfsvalidator/parsing/RowParserTest.java
+++ b/core/src/test/java/org/mobilitydata/gtfsvalidator/parsing/RowParserTest.java
@@ -56,6 +56,11 @@ public boolean headerRequired() {
return false;
}
+ @Override
+ public boolean headerRecommended() {
+ return false;
+ }
+
@Override
public FieldLevelEnum fieldLevel() {
return FieldLevelEnum.REQUIRED;
@@ -141,6 +146,7 @@ public void asString_recommended_missing() {
GtfsColumnDescriptor.builder()
.setColumnName("column name")
.setHeaderRequired(false)
+ .setHeaderRecommended(false)
.setFieldLevel(FieldLevelEnum.RECOMMENDED)
.setIsMixedCase(false)
.setIsCached(false)
diff --git a/core/src/test/java/org/mobilitydata/gtfsvalidator/table/AnyTableLoaderTest.java b/core/src/test/java/org/mobilitydata/gtfsvalidator/table/AnyTableLoaderTest.java
index c715a38e2f..29802babef 100644
--- a/core/src/test/java/org/mobilitydata/gtfsvalidator/table/AnyTableLoaderTest.java
+++ b/core/src/test/java/org/mobilitydata/gtfsvalidator/table/AnyTableLoaderTest.java
@@ -88,6 +88,7 @@ public void validate(
CsvHeader actualHeader,
Set supportedHeaders,
Set requiredHeaders,
+ Set recommendedHeaders,
NoticeContainer noticeContainer) {
noticeContainer.addValidationNotice(headerValidationNotice);
}
@@ -144,6 +145,7 @@ public void missingRequiredField() {
GtfsColumnDescriptor.builder()
.setColumnName(GtfsTestEntity.ID_FIELD_NAME)
.setHeaderRequired(true)
+ .setHeaderRecommended(false)
.setFieldLevel(FieldLevelEnum.REQUIRED)
.setIsMixedCase(false)
.setIsCached(false)
@@ -151,6 +153,7 @@ public void missingRequiredField() {
GtfsColumnDescriptor.builder()
.setColumnName(GtfsTestEntity.CODE_FIELD_NAME)
.setHeaderRequired(false)
+ .setHeaderRecommended(false)
.setFieldLevel(FieldLevelEnum.REQUIRED)
.setIsMixedCase(false)
.setIsCached(false)
diff --git a/core/src/test/java/org/mobilitydata/gtfsvalidator/testgtfs/GtfsTestTableDescriptor.java b/core/src/test/java/org/mobilitydata/gtfsvalidator/testgtfs/GtfsTestTableDescriptor.java
index eb42741abc..4e6d73b0b2 100644
--- a/core/src/test/java/org/mobilitydata/gtfsvalidator/testgtfs/GtfsTestTableDescriptor.java
+++ b/core/src/test/java/org/mobilitydata/gtfsvalidator/testgtfs/GtfsTestTableDescriptor.java
@@ -41,6 +41,7 @@ public ImmutableList getColumns() {
GtfsColumnDescriptor.builder()
.setColumnName(GtfsTestEntity.ID_FIELD_NAME)
.setHeaderRequired(true)
+ .setHeaderRecommended(false)
.setFieldLevel(FieldLevelEnum.REQUIRED)
.setIsMixedCase(false)
.setIsCached(false)
@@ -49,6 +50,7 @@ public ImmutableList getColumns() {
GtfsColumnDescriptor.builder()
.setColumnName(GtfsTestEntity.CODE_FIELD_NAME)
.setHeaderRequired(false)
+ .setHeaderRecommended(false)
.setFieldLevel(FieldLevelEnum.OPTIONAL)
.setIsMixedCase(false)
.setIsCached(false)
diff --git a/core/src/test/java/org/mobilitydata/gtfsvalidator/validator/TableHeaderValidatorTest.java b/core/src/test/java/org/mobilitydata/gtfsvalidator/validator/TableHeaderValidatorTest.java
index a26323d187..1946ecd024 100644
--- a/core/src/test/java/org/mobilitydata/gtfsvalidator/validator/TableHeaderValidatorTest.java
+++ b/core/src/test/java/org/mobilitydata/gtfsvalidator/validator/TableHeaderValidatorTest.java
@@ -19,11 +19,13 @@
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableSet;
+import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mobilitydata.gtfsvalidator.notice.DuplicatedColumnNotice;
import org.mobilitydata.gtfsvalidator.notice.EmptyColumnNameNotice;
+import org.mobilitydata.gtfsvalidator.notice.MissingRecommendedColumnNotice;
import org.mobilitydata.gtfsvalidator.notice.MissingRequiredColumnNotice;
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
import org.mobilitydata.gtfsvalidator.notice.UnknownColumnNotice;
@@ -40,6 +42,7 @@ public void expectedColumns() {
new CsvHeader(new String[] {"stop_id", "stop_name"}),
ImmutableSet.of("stop_id", "stop_name", "stop_lat", "stop_lon"),
ImmutableSet.of("stop_id"),
+ Set.of(),
container);
assertThat(container.getValidationNotices()).isEmpty();
@@ -55,6 +58,7 @@ public void unknownColumnShouldGenerateNotice() {
new CsvHeader(new String[] {"stop_id", "stop_name", "stop_extra"}),
ImmutableSet.of("stop_id", "stop_name"),
ImmutableSet.of("stop_id"),
+ Set.of(),
container);
assertThat(container.getValidationNotices())
@@ -71,11 +75,27 @@ public void missingRequiredColumnShouldGenerateNotice() {
new CsvHeader(new String[] {"stop_name"}),
ImmutableSet.of("stop_id", "stop_name"),
ImmutableSet.of("stop_id"),
+ Set.of(),
container);
assertThat(container.getValidationNotices())
.containsExactly(new MissingRequiredColumnNotice("stops.txt", "stop_id"));
- assertThat(container.hasValidationErrors()).isTrue();
+ }
+
+ @Test
+ public void missingRecommendedColumnShouldGenerateNotice() {
+ NoticeContainer container = new NoticeContainer();
+ new DefaultTableHeaderValidator()
+ .validate(
+ "stops.txt",
+ new CsvHeader(new String[] {"stop_name"}),
+ Set.of("stop_id", "stop_name"),
+ Set.of(),
+ Set.of("stop_id"),
+ container);
+
+ assertThat(container.getValidationNotices())
+ .containsExactly(new MissingRecommendedColumnNotice("stops.txt", "stop_id"));
}
@Test
@@ -87,6 +107,7 @@ public void duplicatedColumnShouldGenerateNotice() {
new CsvHeader(new String[] {"stop_id", "stop_name", "stop_id"}),
ImmutableSet.of("stop_id", "stop_name"),
ImmutableSet.of("stop_id"),
+ Set.of(),
container);
assertThat(container.getValidationNotices())
@@ -103,6 +124,7 @@ public void emptyFileShouldNotGenerateNotice() {
CsvHeader.EMPTY,
ImmutableSet.of("stop_id", "stop_name"),
ImmutableSet.of("stop_id"),
+ Set.of(),
container);
assertThat(container.getValidationNotices()).isEmpty();
@@ -118,6 +140,7 @@ public void emptyColumnNameShouldGenerateNotice() {
new CsvHeader(new String[] {"stop_id", null, "stop_name", ""}),
ImmutableSet.of("stop_id", "stop_name"),
ImmutableSet.of("stop_id"),
+ Set.of(),
container);
assertThat(container.getValidationNotices())
diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsStopTimeSchema.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsStopTimeSchema.java
index 731479a1c2..f35ce24ff9 100644
--- a/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsStopTimeSchema.java
+++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsStopTimeSchema.java
@@ -29,6 +29,7 @@
import org.mobilitydata.gtfsvalidator.annotation.Index;
import org.mobilitydata.gtfsvalidator.annotation.NonNegative;
import org.mobilitydata.gtfsvalidator.annotation.PrimaryKey;
+import org.mobilitydata.gtfsvalidator.annotation.RecommendedColumn;
import org.mobilitydata.gtfsvalidator.annotation.Required;
import org.mobilitydata.gtfsvalidator.type.GtfsTime;
@@ -77,5 +78,6 @@ public interface GtfsStopTimeSchema extends GtfsEntity {
double shapeDistTraveled();
@DefaultValue("1")
+ @RecommendedColumn
GtfsStopTimeTimepoint timepoint();
}
diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimepointTimeValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimepointTimeValidator.java
index b8db497c69..972a35d8f0 100644
--- a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimepointTimeValidator.java
+++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimepointTimeValidator.java
@@ -38,7 +38,6 @@
* {@link StopTimeTimepointWithoutTimesNotice} - a timepoint does not specifies arrival_time
* or departure_time
* {@link MissingTimepointValueNotice} - value for {@code stop_times.timepoint} is missing
- * {@link MissingTimepointColumnNotice} - field {@code stop_times.timepoint} is missing
*
*/
@GtfsValidator
@@ -55,10 +54,9 @@ public class TimepointTimeValidator extends FileValidator {
public void validate(NoticeContainer noticeContainer) {
if (!stopTimes.hasColumn(GtfsStopTime.TIMEPOINT_FIELD_NAME)) {
// legacy datasets do not use timepoint column in stop_times.txt as a result:
- // - this should be flagged;
+ // - this should be flagged in the header tests.
// - but also no notice regarding the absence of arrival_time or departure_time should be
// generated
- noticeContainer.addValidationNotice(new MissingTimepointColumnNotice());
return;
}
for (GtfsStopTime stopTime : stopTimes.getEntities()) {
@@ -139,19 +137,4 @@ static class MissingTimepointValueNotice extends ValidationNotice {
this.stopSequence = stopTime.stopSequence();
}
}
-
- /** `timepoint` column is missing for a dataset. */
- @GtfsValidationNotice(
- severity = WARNING,
- files = @FileRefs(GtfsStopTimeSchema.class),
- bestPractices = @FileRefs(GtfsStopTimeSchema.class))
- static class MissingTimepointColumnNotice extends ValidationNotice {
-
- /** The name of the affected file. */
- private final String filename;
-
- MissingTimepointColumnNotice() {
- this.filename = GtfsStopTime.FILENAME;
- }
- }
}
diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimepointTimeValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimepointTimeValidatorTest.java
index 9e3a9aed2c..159105a2af 100644
--- a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimepointTimeValidatorTest.java
+++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimepointTimeValidatorTest.java
@@ -39,7 +39,6 @@
import org.mobilitydata.gtfsvalidator.table.GtfsStopTime;
import org.mobilitydata.gtfsvalidator.table.GtfsStopTimeTableContainer;
import org.mobilitydata.gtfsvalidator.type.GtfsTime;
-import org.mobilitydata.gtfsvalidator.validator.TimepointTimeValidator.MissingTimepointColumnNotice;
import org.mobilitydata.gtfsvalidator.validator.TimepointTimeValidator.MissingTimepointValueNotice;
import org.mobilitydata.gtfsvalidator.validator.TimepointTimeValidator.StopTimeTimepointWithoutTimesNotice;
@@ -59,7 +58,8 @@ private static List generateNotices(
return noticeContainer.getValidationNotices();
}
- // using this header will trigger a MissingTimepointColumnNotice
+ // Using this header will trigger a MissingRecommendedColumnNotice since the timepoint column is
+ // missing.
private static CsvHeader createLegacyHeader() {
return new CsvHeader(
new String[] {
@@ -95,58 +95,6 @@ private static CsvHeader createHeaderWithTimepointColumn() {
});
}
- @Test
- public void noTimepointColumn_noTimeProvided_shouldGenerateNotice() {
- // Using createLegacyHeader() that omits the timestamp column will trigger the
- // MissingTimepointColumnNotice.
- // .setTimepoint(null) is used to indicate that no value is provided, although it has no effect
- // in this test.
- List stopTimes = new ArrayList<>();
- stopTimes.add(
- new GtfsStopTime.Builder()
- .setCsvRowNumber(1)
- .setTripId("first trip id")
- .setArrivalTime(null)
- .setDepartureTime(null)
- .setStopId("stop id 0")
- .setStopSequence(2)
- .setTimepoint((Integer) null)
- .build());
- stopTimes.add(
- new GtfsStopTime.Builder()
- .setCsvRowNumber(4)
- .setTripId("second trip id")
- .setArrivalTime(null)
- .setDepartureTime(null)
- .setStopId("stop id 1")
- .setStopSequence(2)
- .setTimepoint((Integer) null)
- .build());
- assertThat(generateNotices(createLegacyHeader(), stopTimes))
- .containsExactly(new MissingTimepointColumnNotice());
- }
-
- @Test
- public void noTimepointColumn_timesProvided_shouldGenerateNotice() {
- // Using createLegacyHeader() that omits the timestamp column will trigger the
- // MissingTimepointColumnNotice.
- // .setTimepoint(null) is used to indicate that no value is provided, although it has no effect
- // in this test.
- List stopTimes = new ArrayList<>();
- stopTimes.add(
- new GtfsStopTime.Builder()
- .setCsvRowNumber(1)
- .setTripId("first trip id")
- .setArrivalTime(GtfsTime.fromSecondsSinceMidnight(450))
- .setDepartureTime(GtfsTime.fromSecondsSinceMidnight(580))
- .setStopId("stop id")
- .setStopSequence(2)
- .setTimepoint((Integer) null)
- .build());
- assertThat(generateNotices(createLegacyHeader(), stopTimes))
- .containsExactly(new MissingTimepointColumnNotice());
- }
-
@Test
public void timepointWithNoTimeShouldGenerateNotices() {
List stopTimes = new ArrayList<>();
diff --git a/model/src/main/java/org/mobilitydata/gtfsvalidator/annotation/RecommendedColumn.java b/model/src/main/java/org/mobilitydata/gtfsvalidator/annotation/RecommendedColumn.java
new file mode 100644
index 0000000000..f292ebd22d
--- /dev/null
+++ b/model/src/main/java/org/mobilitydata/gtfsvalidator/annotation/RecommendedColumn.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.mobilitydata.gtfsvalidator.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Adds a validation that it's recommended that a column be present. A value for the field may be
+ * optional.
+ *
+ * Example.
+ *
+ *
+ * {@literal @}GtfsTable("stop_times.txt")
+ * public interface GtfsStopTimeSchema extends GtfsEntity {
+ *
+ * {@literal @}DefaultValue("1")
+ * {@literal @}RecommendedColumn
+ * GtfsStopTimeTimepoint timepoint();
+ * }
+ *
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.SOURCE)
+public @interface RecommendedColumn {}
diff --git a/processor/src/main/java/org/mobilitydata/gtfsvalidator/processor/Analyser.java b/processor/src/main/java/org/mobilitydata/gtfsvalidator/processor/Analyser.java
index f2d52b825c..0a15d5577b 100644
--- a/processor/src/main/java/org/mobilitydata/gtfsvalidator/processor/Analyser.java
+++ b/processor/src/main/java/org/mobilitydata/gtfsvalidator/processor/Analyser.java
@@ -71,6 +71,7 @@ public GtfsFileDescriptor analyzeGtfsFileType(TypeElement type) {
fieldBuilder.setRecommended(method.getAnnotation(Recommended.class) != null);
fieldBuilder.setColumnRequired(method.getAnnotation(RequiredColumn.class) != null);
fieldBuilder.setValueRequired(method.getAnnotation(Required.class) != null);
+ fieldBuilder.setColumnRecommended(method.getAnnotation(RecommendedColumn.class) != null);
fieldBuilder.setMixedCase(method.getAnnotation(MixedCase.class) != null);
PrimaryKey primaryKey = method.getAnnotation(PrimaryKey.class);
if (primaryKey != null) {
diff --git a/processor/src/main/java/org/mobilitydata/gtfsvalidator/processor/GtfsFieldDescriptor.java b/processor/src/main/java/org/mobilitydata/gtfsvalidator/processor/GtfsFieldDescriptor.java
index d94b62c8ef..c5c6374fac 100644
--- a/processor/src/main/java/org/mobilitydata/gtfsvalidator/processor/GtfsFieldDescriptor.java
+++ b/processor/src/main/java/org/mobilitydata/gtfsvalidator/processor/GtfsFieldDescriptor.java
@@ -59,6 +59,8 @@ public static GtfsFieldDescriptor.Builder builder() {
public abstract boolean columnRequired();
+ public abstract boolean columnRecommended();
+
public boolean isHeaderRequired() {
return columnRequired() || valueRequired();
}
@@ -81,6 +83,8 @@ public abstract static class Builder {
public abstract Builder setColumnRequired(boolean value);
+ public abstract Builder setColumnRecommended(boolean value);
+
public abstract Builder setMixedCase(boolean value);
public abstract Builder setPrimaryKey(PrimaryKey annotation);
diff --git a/processor/src/main/java/org/mobilitydata/gtfsvalidator/processor/TableDescriptorGenerator.java b/processor/src/main/java/org/mobilitydata/gtfsvalidator/processor/TableDescriptorGenerator.java
index 825ff03e2c..304bc93c83 100644
--- a/processor/src/main/java/org/mobilitydata/gtfsvalidator/processor/TableDescriptorGenerator.java
+++ b/processor/src/main/java/org/mobilitydata/gtfsvalidator/processor/TableDescriptorGenerator.java
@@ -186,12 +186,14 @@ private MethodSpec generateGetColumnsMethod() {
"GtfsColumnDescriptor.builder()\n"
+ ".setColumnName($T.$L)\n"
+ ".setHeaderRequired($L)\n"
+ + ".setHeaderRecommended($L)\n"
+ ".setFieldLevel($T.$L)\n"
+ ".setIsMixedCase($L)\n"
+ ".setIsCached($L)\n",
gtfsEntityType,
fieldNameField(field.name()),
field.isHeaderRequired(),
+ field.columnRecommended(),
FieldLevelEnum.class,
getFieldLevel(field),
field.mixedCase(),
diff --git a/processor/tests/src/main/java/org/mobilitydata/gtfsvalidator/processor/tests/RecommendedColumnAnnotationSchema.java b/processor/tests/src/main/java/org/mobilitydata/gtfsvalidator/processor/tests/RecommendedColumnAnnotationSchema.java
new file mode 100644
index 0000000000..dff043ff42
--- /dev/null
+++ b/processor/tests/src/main/java/org/mobilitydata/gtfsvalidator/processor/tests/RecommendedColumnAnnotationSchema.java
@@ -0,0 +1,11 @@
+package org.mobilitydata.gtfsvalidator.processor.tests;
+
+import org.mobilitydata.gtfsvalidator.annotation.GtfsTable;
+import org.mobilitydata.gtfsvalidator.annotation.RecommendedColumn;
+
+@GtfsTable("recommended_column.txt")
+public interface RecommendedColumnAnnotationSchema {
+
+ @RecommendedColumn
+ String columnRecommended();
+}
diff --git a/processor/tests/src/test/java/org/mobilitydata/gtfsvalidator/processor/tests/RecommendedColumnAnnotationSchemaTest.java b/processor/tests/src/test/java/org/mobilitydata/gtfsvalidator/processor/tests/RecommendedColumnAnnotationSchemaTest.java
new file mode 100644
index 0000000000..fd600103e2
--- /dev/null
+++ b/processor/tests/src/test/java/org/mobilitydata/gtfsvalidator/processor/tests/RecommendedColumnAnnotationSchemaTest.java
@@ -0,0 +1,51 @@
+package org.mobilitydata.gtfsvalidator.processor.tests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mobilitydata.gtfsvalidator.notice.MissingRequiredColumnNotice;
+import org.mobilitydata.gtfsvalidator.table.RecommendedColumnAnnotationTableDescriptor;
+import org.mobilitydata.gtfsvalidator.testing.LoadingHelper;
+import org.mobilitydata.gtfsvalidator.validator.ValidatorLoaderException;
+
+@RunWith(JUnit4.class)
+public class RecommendedColumnAnnotationSchemaTest {
+ private RecommendedColumnAnnotationTableDescriptor tableDescriptor;
+ private LoadingHelper helper;
+
+ @Before
+ public void setup() throws ValidatorLoaderException {
+ tableDescriptor = new RecommendedColumnAnnotationTableDescriptor();
+ helper = new LoadingHelper();
+ }
+
+ @Test
+ public void includingRecommendedColumnHeaderWithoutValueShouldNotGenerateNotice()
+ throws ValidatorLoaderException {
+
+ helper.load(tableDescriptor, "some_column,column_recommended", "value,");
+
+ assertThat(
+ !helper
+ .getValidationNotices()
+ .contains(
+ new MissingRequiredColumnNotice("recommended_column.txt", "column_recommended")));
+ }
+
+ @Test
+ public void missingRecommendedColumnHeaderShouldGenerateNotice() throws ValidatorLoaderException {
+
+ helper.load(tableDescriptor, "column", "value");
+ // Since we use an unknown column ("column") we have to expect at least one unknown_column
+ // notice along with the
+ // missing_recommended_column notice.
+ assertThat(
+ helper
+ .getValidationNotices()
+ .contains(
+ new MissingRequiredColumnNotice("recommended_column.txt", "column_recommended")));
+ }
+}