|
package com.exmpale.sealedtype; |
|
|
|
import com.exmpale.sealedtype.SealedType; |
|
import com.google.common.base.MoreObjects; |
|
import com.google.common.collect.Sets; |
|
import org.jooq.lambda.Seq; |
|
import org.junit.BeforeClass; |
|
import org.junit.Test; |
|
import org.reflections.Reflections; |
|
import org.reflections.scanners.SubTypesScanner; |
|
|
|
import java.lang.reflect.Constructor; |
|
import java.lang.reflect.Method; |
|
import java.lang.reflect.Modifier; |
|
import java.lang.reflect.ParameterizedType; |
|
import java.util.Arrays; |
|
import java.util.List; |
|
import java.util.Set; |
|
|
|
import static org.hamcrest.MatcherAssert.assertThat; |
|
import static org.hamcrest.Matchers.empty; |
|
import static org.jooq.lambda.Seq.seq; |
|
import static org.mockito.Mockito.mock; |
|
import static org.mockito.Mockito.when; |
|
|
|
public class SealedTypeChecker { |
|
|
|
private static Reflections reflections; |
|
|
|
@BeforeClass |
|
public static void setUp() { |
|
// do this once per test class since it takes considerable amount of time and can be reused anyway |
|
reflections = new Reflections("com.example", new SubTypesScanner()); |
|
} |
|
|
|
@Test |
|
public void checkThatSealedTypesAreAbstract() { |
|
Set<Class<? extends SealedType>> nonAbstractClasses = findSealedTypes() |
|
.filter(clazz -> !Modifier.isAbstract(clazz.getModifiers())) |
|
.toSet(); |
|
|
|
assertThat("Sealed types must be abstract", nonAbstractClasses, empty()); |
|
} |
|
|
|
@Test |
|
public void checkThatSealedTypesHaveOnlyPrivateConstructors() { |
|
Set<Constructor<?>> nonPrivateConstructors = findSealedTypes() |
|
.flatMap(clazz -> Arrays.stream(clazz.getConstructors())) |
|
.filter(constructor -> !Modifier.isPrivate(constructor.getModifiers())) |
|
.toSet(); |
|
|
|
assertThat("Sealed types must only have private constructors", nonPrivateConstructors, empty()); |
|
} |
|
|
|
@Test |
|
public void checkThatSealedTypesHaveASubTypeForEveryEnumValue() { |
|
Set<SealedTypeInfo> sealedTypesWithUnimplementedSubtypes = getSealedTypeInfos() |
|
.filter(sealedTypeInfo -> !Sets.difference(sealedTypeInfo.getDeclaredSubtypes(), sealedTypeInfo.getImplementedSubtypes()).isEmpty()) |
|
.toSet(); |
|
|
|
assertThat("There must be a subtype for every enum value", sealedTypesWithUnimplementedSubtypes, empty()); |
|
} |
|
|
|
@Test |
|
public void checkThatSealedTypesHaveAEnumValueForEverySubtype() { |
|
Set<SealedTypeInfo> sealedTypesWithUnimplementedSubtypes = getSealedTypeInfos() |
|
.filter(sealedTypeInfo -> !Sets.difference(sealedTypeInfo.getImplementedSubtypes(), sealedTypeInfo.getDeclaredSubtypes()).isEmpty()) |
|
.toSet(); |
|
|
|
assertThat("There must be an enum value for every subtype", sealedTypesWithUnimplementedSubtypes, empty()); |
|
} |
|
|
|
@Test |
|
public void checkThatSealedTypesProvideAnApplyMethod() { |
|
Set<Class<? extends SealedType>> sealedTypesWithoutApplyMethod = findSealedTypes() |
|
.filter(sealedType -> !seq(Arrays.stream(sealedType.getDeclaredMethods())) |
|
.findFirst(applyMethod -> applyMethod.getName().equals("apply")).isPresent()) |
|
.toSet(); |
|
|
|
assertThat("There must be an 'apply()' method defined on the sealed type", sealedTypesWithoutApplyMethod, empty()); |
|
} |
|
|
|
@Test |
|
public void checkThatSealedTypesProvideAnApplyMethodWithMatchingNumberOfArguments() { |
|
Set<Class<? extends SealedType>> sealedTypesWithoutAppropriateApplyMethodApplyMethod = findSealedTypes() |
|
.filter(sealedType -> !seq(Arrays.stream(sealedType.getDeclaredMethods())) |
|
.filter(method -> method.getName().equals("apply")) |
|
.findFirst(applyMethod -> { |
|
Set<? extends Class<? extends SealedType>> subtypesDefined = findDirectSubTypes(sealedType); |
|
List<Class<?>> subtypesFoundInApply = Seq.seq(Arrays.stream(applyMethod.getParameterTypes())).toList(); |
|
// we can only check the argument count due to erasure |
|
return subtypesFoundInApply.size() == subtypesDefined.size(); |
|
}) |
|
.isPresent()) |
|
.toSet(); |
|
|
|
assertThat("There must be an 'apply(Function<SubA, T>, Function<SubB, T>, ...)' method taking exactly one lambda function for each possible subtype", |
|
sealedTypesWithoutAppropriateApplyMethodApplyMethod, |
|
empty()); |
|
} |
|
|
|
@Test |
|
public void checkThatSealedTypesProvideAStaticCreateMethod() { |
|
Set<Class<? extends SealedType>> sealedTypesWithoutApplyMethod = findSealedTypes() |
|
.filter(sealedType -> !seq(Arrays.stream(sealedType.getDeclaredMethods())) |
|
.findFirst(this::isStaticCreateMethod).isPresent()) |
|
.toSet(); |
|
|
|
assertThat("There must be a static 'create()' method defined on the sealed type", sealedTypesWithoutApplyMethod, empty()); |
|
} |
|
|
|
@Test |
|
public void checkThatSealedTypesProvideAStaticCreateMethodMethodWithMatchingNumberOfArguments() { |
|
Set<Class<? extends SealedType>> sealedTypesWithoutApplyMethod = findSealedTypes() |
|
.filter(sealedType -> !seq(Arrays.stream(sealedType.getDeclaredMethods())) |
|
.filter(this::isStaticCreateMethod) |
|
.findFirst(createMethod -> { |
|
Set<? extends Class<? extends SealedType>> subtypesDefined = findDirectSubTypes(sealedType); |
|
List<Class<?>> subtypesFoundInApply = Seq.seq(Arrays.stream(createMethod.getParameterTypes())).toList(); |
|
// we can only check the argument count due to erasure |
|
return subtypesFoundInApply.size() == 1 + subtypesDefined.size(); |
|
}) |
|
.isPresent()) |
|
.toSet(); |
|
|
|
assertThat("There must be a static 'create(T, Supplier<SubA, T>, Supplier<SubB, T>, ...)' method taking exactly one lambda function for each possible subtype", |
|
sealedTypesWithoutApplyMethod, |
|
empty()); |
|
} |
|
|
|
private boolean isStaticCreateMethod(Method method) { |
|
return method.getName().equals("create") |
|
&& Modifier.isStatic(method.getModifiers()); |
|
} |
|
|
|
private Seq<Class<? extends SealedType>> findSealedTypes() { |
|
return seq(reflections.getSubTypesOf(SealedType.class)) |
|
.filter(clazz -> Arrays.asList(clazz.getInterfaces()).contains(SealedType.class)); |
|
} |
|
|
|
private Seq<SealedTypeInfo> getSealedTypeInfos() { |
|
return findSealedTypes() |
|
.map(sealedType -> { |
|
Set<? extends Class<? extends SealedType>> subTypes = findDirectSubTypes(sealedType); |
|
|
|
Set<? extends Enum<?>> implementedTypeEnumValues = seq(subTypes) |
|
.map(clazz -> { |
|
SealedType<?> mock = mock(clazz); |
|
when(mock.getType()).thenCallRealMethod(); |
|
return mock.getType(); |
|
}) |
|
.toSet(); |
|
|
|
Set<? extends Enum<?>> declaredEnumValues = getDeclaredEnumValues(sealedType); |
|
|
|
return new SealedTypeInfo(sealedType, declaredEnumValues, implementedTypeEnumValues); |
|
}); |
|
} |
|
|
|
private Set<? extends Class<? extends SealedType>> findDirectSubTypes(Class<? extends SealedType> sealedType) { |
|
return seq(reflections.getSubTypesOf(sealedType)) |
|
.filter(subtype -> subtype.getSuperclass().equals(sealedType)) |
|
.toSet(); |
|
} |
|
|
|
private class SealedTypeInfo { |
|
|
|
private Class<? extends SealedType> sealedType; |
|
|
|
private Set<? extends Enum<?>> declaredSubtypes; |
|
|
|
private Set<? extends Enum<?>> implementedSubtypes; |
|
|
|
public SealedTypeInfo(Class<? extends SealedType> sealedType, Set<? extends Enum<?>> declaredSubtypes, Set<? extends Enum<?>> implementedSubtypes) { |
|
this.sealedType = sealedType; |
|
this.declaredSubtypes = declaredSubtypes; |
|
this.implementedSubtypes = implementedSubtypes; |
|
} |
|
|
|
public Class<? extends SealedType> getSealedType() { |
|
return sealedType; |
|
} |
|
|
|
public Set<? extends Enum<?>> getDeclaredSubtypes() { |
|
return declaredSubtypes; |
|
} |
|
|
|
public Set<? extends Enum<?>> getImplementedSubtypes() { |
|
return implementedSubtypes; |
|
} |
|
|
|
@Override |
|
public String toString() { |
|
return MoreObjects.toStringHelper(this) |
|
.add("sealedType", sealedType) |
|
.add("declaredSubtypes", declaredSubtypes) |
|
.add("implementedSubtypes", implementedSubtypes) |
|
.toString(); |
|
} |
|
|
|
} |
|
|
|
private Set<? extends Enum<?>> getDeclaredEnumValues(Class<? extends SealedType> sealedType) { |
|
Class<? extends Enum<?>> enumType = seq(Arrays.stream(sealedType.getGenericInterfaces())) |
|
.findFirst(genericInterface -> genericInterface.getTypeName().startsWith(SealedType.class.getName() + "<")) |
|
.map(parametrizedSealedType -> (Class<? extends Enum<?>>) ((ParameterizedType) parametrizedSealedType).getActualTypeArguments()[0]) |
|
.get(); |
|
|
|
return seq(Arrays.stream(enumType.getEnumConstants())).toSet(); |
|
} |
|
|
|
} |