Skip to content

Commit

Permalink
feat: Support injection of arbitrary types via ValidationContext (#1607)
Browse files Browse the repository at this point in the history
ValidationContext is used to inject params to constructors of *Validator
classes. ValidationContext initially supported only CountryCode and
CurrentDateTime, e.g.:

  @Inject
  ExpiredCalendarValidator(
      CurrentDateTime currentDateTime, // Injected from ValidationContext
      GtfsCalendarTableContainer calendarTable,
      GtfsCalendarDateTableContainer calendarDateTable) {...}

This change allows users to inject any types required for custom
validation logic.
  • Loading branch information
aababilov authored Nov 22, 2023
1 parent b01aff1 commit c0cd6be
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public <T extends GtfsEntity> List<SingleEntityValidator<T>> createSingleEntityV
} else {
validators.add((SingleEntityValidator<T>) validatorWithDependencyStatus.validator());
}
} catch (ReflectiveOperationException e) {
} catch (ReflectiveOperationException | ValidatorLoaderException e) {
logger.atSevere().withCause(e).log(
"Cannot instantiate validator %s", validatorClass.getCanonicalName());
}
Expand All @@ -117,7 +117,7 @@ public <T extends GtfsEntity> List<FileValidator> createSingleFileValidators(
} else {
validators.add(validatorWithStatus.validator());
}
} catch (ReflectiveOperationException e) {
} catch (ReflectiveOperationException | ValidatorLoaderException e) {
logger.atSevere().withCause(e).log(
"Cannot instantiate validator %s", validatorClass.getCanonicalName());
}
Expand All @@ -139,7 +139,7 @@ public List<FileValidator> createMultiFileValidators(
} else {
validators.add(validatorWithStatus.validator());
}
} catch (ReflectiveOperationException e) {
} catch (ReflectiveOperationException | ValidatorLoaderException e) {
logger.atSevere().withCause(e).log(
"Cannot instantiate validator %s", validatorClass.getCanonicalName());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,33 @@

package org.mobilitydata.gtfsvalidator.validator;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableMap;
import org.mobilitydata.gtfsvalidator.input.CountryCode;
import org.mobilitydata.gtfsvalidator.input.CurrentDateTime;

/**
* A read-only context passed to particular validator objects. It gives information relevant for
* validation: properties of the feed as a whole, system properties (current time) etc.
*/
@AutoValue
public abstract class ValidationContext {
public class ValidationContext {
public static Builder builder() {
return new AutoValue_ValidationContext.Builder();
return new Builder();
}

private final ImmutableMap<Class<?>, Object> context;

private ValidationContext(ImmutableMap<Class<?>, Object> context) {
this.context = context;
}

/**
* Represents a name of a GTFS feed, such as "nl-openov".
* Represents the country code of a GTFS feed, such as US or NL.
*
* @return the @code{GtfsFeedName} representing the feed's name
* @return the @code{CountryCode} representing the feed's country code
*/
public abstract CountryCode countryCode();
public CountryCode countryCode() {
return get(CountryCode.class);
}

/**
* The time when validation started.
Expand All @@ -50,27 +57,43 @@ public static Builder builder() {
*
* @return The time when validation started as @code{ZonedDateTime}
*/
public abstract CurrentDateTime currentDateTime();
public CurrentDateTime currentDateTime() {
return get(CurrentDateTime.class);
}

/** Returns a member of the context with requested class. */
@SuppressWarnings("unchecked")
public <T> T get(Class<T> clazz) {
if (clazz.isAssignableFrom(CountryCode.class)) {
return (T) countryCode();
}
if (clazz.isAssignableFrom(CurrentDateTime.class)) {
return (T) currentDateTime();
Object o = context.get(clazz);
if (o == null) {
throw new IllegalArgumentException(
"Cannot find " + clazz.getCanonicalName() + " in validation context");
}
throw new IllegalArgumentException(
"Cannot find " + clazz.getCanonicalName() + " in validation context");
return (T) o;
}

@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setCountryCode(CountryCode countryCode);
/** Builder for {@link ValidationContext}. */
public static class Builder {
private final ImmutableMap.Builder<Class<?>, Object> context = ImmutableMap.builder();

public abstract Builder setCurrentDateTime(CurrentDateTime currentDateTime);
/** Sets the country code. */
public Builder setCountryCode(CountryCode countryCode) {
return set(CountryCode.class, countryCode);
}

/** Sets the current time. */
public Builder setCurrentDateTime(CurrentDateTime currentDateTime) {
return set(CurrentDateTime.class, currentDateTime);
}

public abstract ValidationContext build();
/** Sets a member of the context with requested class. */
public <T> Builder set(Class<T> clazz, T obj) {
context.put(clazz, obj);
return this;
}

public ValidationContext build() {
return new ValidationContext(context.buildOrThrow());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@
import java.util.Map.Entry;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.mobilitydata.gtfsvalidator.input.CountryCode;
import org.mobilitydata.gtfsvalidator.input.CurrentDateTime;
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
import org.mobilitydata.gtfsvalidator.table.GtfsEntity;
import org.mobilitydata.gtfsvalidator.table.GtfsFeedContainer;
Expand Down Expand Up @@ -89,16 +87,7 @@ public List<Class<? extends FileValidator>> getMultiFileValidators() {

@SuppressWarnings("unchecked")
private <T extends SingleEntityValidator<?>> void addSingleEntityValidator(
Class<T> validatorClass) throws ValidatorLoaderException {
Constructor<T> constructor = chooseConstructor(validatorClass);
for (Class<?> parameterType : constructor.getParameterTypes()) {
if (!isInjectableFromContext(parameterType)) {
throw new ValidatorLoaderException(
String.format(
"Cannot inject parameter of type %s to %s constructor",
parameterType.getCanonicalName(), validatorClass.getCanonicalName()));
}
}
Class<T> validatorClass) {
for (Method method : validatorClass.getMethods()) {
// A child class of SingleEntityValidator has two `validate' methods:
// 1) the inherited void validate(GtfsEntity entity, NoticeContainer noticeContainer);
Expand Down Expand Up @@ -126,20 +115,13 @@ private <T extends FileValidator> void addFileValidator(Class<T> validatorClass)
// Find out which GTFS tables need to be injected.
List<Class<? extends GtfsTableContainer<?>>> injectedTables = new ArrayList<>();
for (Class<?> parameterType : constructor.getParameterTypes()) {
if (isInjectableFromContext(parameterType)) {
continue;
}
if (GtfsFeedContainer.class.isAssignableFrom(parameterType)) {
injectFeedContainer = true;
continue;
}
if (!GtfsTableContainer.class.isAssignableFrom(parameterType)) {
throw new ValidatorLoaderException(
String.format(
"Cannot inject parameter of type %s to %s constructor",
parameterType.getCanonicalName(), validatorClass.getCanonicalName()));
if (GtfsTableContainer.class.isAssignableFrom(parameterType)) {
injectedTables.add((Class<? extends GtfsTableContainer<?>>) parameterType);
}
injectedTables.add((Class<? extends GtfsTableContainer<?>>) parameterType);
}

if (!injectFeedContainer && injectedTables.size() == 1) {
Expand All @@ -149,11 +131,6 @@ private <T extends FileValidator> void addFileValidator(Class<T> validatorClass)
}
}

private static boolean isInjectableFromContext(Class<?> parameterType) {
return parameterType.isAssignableFrom(CurrentDateTime.class)
|| parameterType.isAssignableFrom(CountryCode.class);
}

/** Chooses the default or injectable constructor. */
@SuppressWarnings("unchecked")
private static <T> Constructor<T> chooseConstructor(Class<T> validatorClass)
Expand All @@ -175,7 +152,8 @@ private static <T> Constructor<T> chooseConstructor(Class<T> validatorClass)
* information in the returned {@link ValidatorWithDependencyStatus}.
*/
private static <T> ValidatorWithDependencyStatus<T> createValidator(
Class<T> clazz, DependencyResolver dependencyResolver) throws ReflectiveOperationException {
Class<T> clazz, DependencyResolver dependencyResolver)
throws ReflectiveOperationException, ValidatorLoaderException {
Constructor<T> chosenConstructor;
try {
chosenConstructor = chooseConstructor(clazz);
Expand All @@ -186,8 +164,16 @@ private static <T> ValidatorWithDependencyStatus<T> createValidator(
// Inject constructor parameters.
Object[] parameters = new Object[chosenConstructor.getParameterCount()];
for (int i = 0; i < parameters.length; ++i) {
parameters[i] =
dependencyResolver.resolveDependency(chosenConstructor.getParameters()[i].getType());
Class<?> parameterType = chosenConstructor.getParameters()[i].getType();
try {
parameters[i] = dependencyResolver.resolveDependency(parameterType);
} catch (IllegalArgumentException e) {
throw new ValidatorLoaderException(
String.format(
"Cannot inject parameter of type %s to %s constructor",
parameterType.getCanonicalName(), clazz.getCanonicalName()),
e);
}
}
chosenConstructor.setAccessible(true);
T validator = chosenConstructor.newInstance(parameters);
Expand All @@ -205,7 +191,8 @@ private static <T> ValidatorWithDependencyStatus<T> createValidator(
*/
public static <T extends SingleEntityValidator>
ValidatorWithDependencyStatus<T> createValidatorWithContext(
Class<T> clazz, ValidationContext validationContext) throws ReflectiveOperationException {
Class<T> clazz, ValidationContext validationContext)
throws ReflectiveOperationException, ValidatorLoaderException {
return (ValidatorWithDependencyStatus<T>)
createValidator(clazz, new DependencyResolver(validationContext, null, null));
}
Expand All @@ -216,7 +203,7 @@ ValidatorWithDependencyStatus<T> createSingleFileValidator(
Class<? extends FileValidator> clazz,
GtfsTableContainer<?> table,
ValidationContext validationContext)
throws ReflectiveOperationException {
throws ReflectiveOperationException, ValidatorLoaderException {
return (ValidatorWithDependencyStatus<T>)
createValidator(clazz, new DependencyResolver(validationContext, table, null));
}
Expand All @@ -225,7 +212,7 @@ ValidatorWithDependencyStatus<T> createSingleFileValidator(
@SuppressWarnings("unchecked")
public static <T extends FileValidator> ValidatorWithDependencyStatus<T> createMultiFileValidator(
Class<T> clazz, GtfsFeedContainer feed, ValidationContext validationContext)
throws ReflectiveOperationException {
throws ReflectiveOperationException, ValidatorLoaderException {
return createValidator(clazz, new DependencyResolver(validationContext, null, feed));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ public void get_unsupported_throws() {
IllegalArgumentException.class, () -> VALIDATION_CONTEXT.get(ChildCurrentDateTime.class));
}

@Test
public void get_extraIntegerObject_successful() {
assertThat(
ValidationContext.builder()
.set(Integer.class, new Integer(10))
.build()
.get(Integer.class))
.isEqualTo(10);
}

private static class ChildCurrentDateTime extends CurrentDateTime {

public ChildCurrentDateTime(ZonedDateTime now) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public class ValidatorLoaderTest {
.build();

@Test
public void createValidatorWithContext_injectsContext() throws ReflectiveOperationException {
public void createValidatorWithContext_injectsContext()
throws ReflectiveOperationException, ValidatorLoaderException {
GtfsTestEntityValidator validator =
ValidatorLoader.createValidatorWithContext(
GtfsTestEntityValidator.class, VALIDATION_CONTEXT)
Expand All @@ -55,7 +56,7 @@ public void createValidatorWithContext_injectsContext() throws ReflectiveOperati

@Test
public void createSingleFileValidator_injectsTableContainerAndContext()
throws ReflectiveOperationException {
throws ReflectiveOperationException, ValidatorLoaderException {
GtfsTestTableContainer table = new GtfsTestTableContainer(TableStatus.EMPTY_FILE);
GtfsTestSingleFileValidator validator =
(GtfsTestSingleFileValidator)
Expand All @@ -70,7 +71,7 @@ public void createSingleFileValidator_injectsTableContainerAndContext()

@Test
public void createMultiFileValidator_injectsFeedContainerAndContext()
throws ReflectiveOperationException {
throws ReflectiveOperationException, ValidatorLoaderException {
GtfsTestTableContainer stopTable =
new GtfsTestTableContainer(TableStatus.PARSABLE_HEADERS_AND_ROWS);
GtfsFeedContainer feedContainer = new GtfsFeedContainer(ImmutableList.of(stopTable));
Expand All @@ -88,7 +89,7 @@ public void createMultiFileValidator_injectsFeedContainerAndContext()

@Test
public void createMultiFileValidator_singleContainer_dependenciesHaveErrors()
throws ReflectiveOperationException {
throws ReflectiveOperationException, ValidatorLoaderException {
GtfsTestTableContainer table = new GtfsTestTableContainer(TableStatus.UNPARSABLE_ROWS);
GtfsFeedContainer feedContainer = new GtfsFeedContainer(ImmutableList.of(table));

Expand Down

0 comments on commit c0cd6be

Please sign in to comment.