Search code examples
archunit

AnyOf-Predicate in ArchUnit


I'd like to test that all Hibernate association annotations (@ManyToOne, @OneToMany, @OneToOne, @ManyToMany) are using fetch = FetchType.LAZY. This is what works:

private static final Set<Class<? extends Annotation>> associations =
         Set.of(ManyToOne.class, OneToMany.class, OneToOne.class, ManyToMany.class);

@ArchTest
public static final ArchRule allEntityRelationsShouldBeLazy = fields().that()
      .areDeclaredInClassesThat().areAnnotatedWith(Entity.class)
      .and()
      .areAnnotatedWith(ManyToOne.class)
      .or().areAnnotatedWith(OneToMany.class)
      .or().areAnnotatedWith(OneToOne.class)
      .or().areAnnotatedWith(ManyToMany.class)
      // what I'd like: .areAnnotatedWith(Any.of(associations))
      .should()
      .notBeAnnotatedWith(new DescribedPredicate<>("FetchType.EAGER or undefined FetchType") {
         @Override
         public boolean apply(JavaAnnotation<?> input) {
            JavaClass rawType = input.getRawType();
            if (!rawType.isEquivalentTo(OneToOne.class)
            // what I'd like: if (!Any.of(associations).apply(input)) {
                  && !rawType.isEquivalentTo(OneToMany.class)
                  && !rawType.isEquivalentTo(ManyToOne.class)
                  && !rawType.isEquivalentTo(ManyToMany.class)) {
               // Filter again, because a field can contain multiple annotations.
               return false;
            }
            return input.get("fetch")
                  .transform(JavaEnumConstant.class::cast)
                  .transform(fetchType -> !FetchType.LAZY.name().equals(fetchType.name()))
                  .or(true);
         }
      });

I have to filter twice because a field can have several annotations like so:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "otherEntity", referencedColumnName = "id")
private OtherEntity otherEntity;

What I don't like is that I have to write the Annotations (ManyToOne…) twice. Why isn't there a "anyOf"-Predicate in ArchUnit? Or is there any other way I could avoid repeating them?


Solution

  • Using a custom condition for the entity fields, you can directly operate on the relevant annotations for associations and don't have to repeat them at all:

    @ArchTest
    ArchRule allEntityRelationsShouldBeLazy = fields()
        .that().areDeclaredInClassesThat().areAnnotatedWith(Entity.class)
        .should(new ArchCondition<JavaField>("be annotated with FetchType.LAZY if an association") {
            @Override
            public void check(JavaField field, ConditionEvents events) {
                field.getAnnotations().stream()
                    .filter(annotation -> associations.stream().anyMatch(annotation.getRawType()::isEquivalentTo))
                    .forEach(association -> {
                        JavaEnumConstant fetchType = (JavaEnumConstant) association.get("fetch").get();
                        if (!FetchType.LAZY.name().equals(fetchType.name())) {
                            String message = field.getDescription() + " is not LAZY in " + field.getSourceCodeLocation();
                            events.add(SimpleConditionEvent.violated(field, message));
                        }
                    });
            }
        });