Skip to content

Commit

Permalink
Merge pull request #20 from klum-dsl/feature/11-filter-annotations
Browse files Browse the repository at this point in the history
Feature/11 filter annotations
  • Loading branch information
pauxus authored Nov 5, 2023
2 parents 8518f16 + f1c2ebf commit e6f65d7
Show file tree
Hide file tree
Showing 16 changed files with 338 additions and 58 deletions.
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ class HueBase extends BaseStation {

The Annotations in the above example all have to follow additional placement rules that need to be checked by the AST-Transformation consuming them:

- Field.members is only valid on Collections and Maps
- Key and Owner annotations are only valid on classes annotated with @DSL
- @Key is only valid on String fields
- @Owner is only valid on fields or on single argument methods
- @DSL.stripSuffix is only valid on non-final classes
- `@Field.members` is only valid on Collections and Maps
- `@Key` and `@Owner` annotations are only valid on classes annotated with `@DSL`
- `@Key` is only valid on String fields
- `@Owner` is only valid on fields or on single argument methods
- `@DSL.stripSuffix` is only valid on non-final classes


# Usage
Expand Down Expand Up @@ -285,8 +285,26 @@ class NameMustMatchCheck extends KlumCastCheck<NameMustMatch> {
}
}
```
### Filtering Checks

Additionally, the method `isValidFor(AnnotatedNode target)` can be overridden quickly skip the check if necessary.
Filters can be set to determine that a check is only valid in certain condition. This can be done in three ways:

#### KlumCastCheck.isValidFor(AnnotatedNode target)
By overriding the `isValidFor(AnnotatedNode target)` method, custom checks can be implemented directly in the check implementation.

### KlumCastValidator.validFor()

The KlumCastValidator Annotation has a member `validFor` of type `ElementType[]`. Only if the annotated target is one of the listed types here, the Check is executed.

### @Filter annotation-members on annotations

By annotation a member of an annotation with `@Filter`, that member becomes a filter for its annotation chain. Filter members can either be

- An `ElementType[]`, in which case the filter behaves exactly as `KlumCastValidator.validFor`
- A Class object containing a subclass of `Filter.Function` which acts as a custom filter
- A String containing the fully qualified Class-name of the filter implementation

Note that in order for a check to be executed, all checks of the annotation chain must match.

## Check as inner class

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2023 Stephan Pauxberger
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.blackbuild.klum.cast;

import org.codehaus.groovy.ast.AnnotatedNode;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
* Designates a member as a filter. Filters are used to filter the list of possible targets for a
* check. The check is only performed if the filter method returns true. Usually, this will be done using
* a default method.
* <p>The filter can be of the following types:</p>
* <ul>
* <li>{@link ElementType[]}: The check is executed if the annotated element is one of the given types</li>
* <li>{@link Filter.Function}: the type of a filter implementation</li>
* <li>{@link String}: The fully qualified classname of the filter implementation</li>
* </ul>
*
* <p>In order for a check to be executed, <b>all</b> Filter checks must pass.</p>
*/
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Filter {

abstract class Function {
protected Filter annotation;

public abstract boolean isValidFor(AnnotatedNode target);

public void setAnnotation(Filter annotation) {
this.annotation = annotation;
}
}

/** Default filter that always matches */
class All extends Function {
@Override
public boolean isValidFor(AnnotatedNode target) {
return true;
}
}

/** Filter that matches if the annotated element is a method */
class Methods extends Function {
@Override
public boolean isValidFor(AnnotatedNode target) {
return target instanceof org.codehaus.groovy.ast.MethodNode;
}
}

/** Filter that matches if the annotated element is a field */
class Fields extends Function {
@Override
public boolean isValidFor(AnnotatedNode target) {
return target instanceof org.codehaus.groovy.ast.FieldNode;
}
}
/** Filter that matches if the annotated element is a Class */
class Classes extends Function {
@Override
public boolean isValidFor(AnnotatedNode target) {
return target instanceof org.codehaus.groovy.ast.ClassNode;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
/**
* Meta-Annotation that defines the validator to validate the usage of the annotated annotation.
* Either value or type must be set, but not both.
* <p>
* The validFor parameter can be used to restrict the usage of the validator to certain elements.
* </p>
*/
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
Expand All @@ -53,6 +56,12 @@
*/
String[] parameters() default {};

/**
* The elements the validator is valid for.
* @return the elements the validator is valid for.
*/
@Filter Class<? extends Filter.Function> validFor() default Filter.All.class;

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface List {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package com.blackbuild.klum.cast.checks;

import com.blackbuild.klum.cast.Filter;
import com.blackbuild.klum.cast.KlumCastValidator;
import com.blackbuild.klum.cast.checks.impl.KlumCastCheck;
import org.codehaus.groovy.ast.AnnotatedNode;
Expand All @@ -40,21 +41,15 @@
*/
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@KlumCastValidator(type = MustBeStatic.Check.class)
@KlumCastValidator(type = MustBeStatic.Check.class, validFor = Filter.Methods.class)
public @interface MustBeStatic {

class Check extends KlumCastCheck<MustBeStatic> {

@Override
protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) {
if (!((MethodNode) target).isStatic())
throw new IllegalStateException("Annotation " + annotationToCheck.getClassNode().getName() + " must be placed on a static method.");
}

@Override
protected boolean isValidFor(AnnotatedNode target) {
return target instanceof MethodNode;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package com.blackbuild.klum.cast.checks;

import com.blackbuild.klum.cast.Filter;
import com.blackbuild.klum.cast.KlumCastValidator;

import java.lang.annotation.*;
Expand All @@ -44,7 +45,7 @@
/**
* If set, the annotation is only checked when placed on one of the given types.
*/
ElementType[] whenOn() default {};
@Filter ElementType[] whenOn() default {};

/**
* If set to true, exactly one of the given members must be set. If set to false, more than one member can be set.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package com.blackbuild.klum.cast.checks;

import com.blackbuild.klum.cast.Filter;
import com.blackbuild.klum.cast.KlumCastValidator;

import java.lang.annotation.ElementType;
Expand All @@ -36,7 +37,7 @@
*/
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@KlumCastValidator("com.blackbuild.klum.cast.checks.impl.NeedsReturnTypeCheck")
@KlumCastValidator(value = "com.blackbuild.klum.cast.checks.impl.NeedsReturnTypeCheck", validFor = Filter.Methods.class)
public @interface NeedsReturnType {
Class<?>[] value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package com.blackbuild.klum.cast.checks;

import com.blackbuild.klum.cast.Filter;
import com.blackbuild.klum.cast.KlumCastValidator;
import com.blackbuild.klum.cast.checks.impl.KlumCastCheck;
import org.codehaus.groovy.ast.AnnotatedNode;
Expand All @@ -39,7 +40,7 @@
*/
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@KlumCastValidator(type = NumberOfParameters.Check.class)
@KlumCastValidator(type = NumberOfParameters.Check.class, validFor = Filter.Methods.class)
public @interface NumberOfParameters {
/**
* The number of parameters the annotated method must have.
Expand All @@ -51,11 +52,9 @@ class Check extends KlumCastCheck<NumberOfParameters> {

@Override
protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) {
if (target instanceof MethodNode) {
MethodNode methodNode = (MethodNode) target;
if (methodNode.getParameters().length != controlAnnotation.value())
throw new RuntimeException("Method " + methodNode.getName() + " must have " + controlAnnotation.value() + " parameters.");
}
MethodNode methodNode = (MethodNode) target;
if (methodNode.getParameters().length != controlAnnotation.value())
throw new RuntimeException("Method " + methodNode.getName() + " must have " + controlAnnotation.value() + " parameters.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package com.blackbuild.klum.cast.checks;

import com.blackbuild.klum.cast.Filter;
import com.blackbuild.klum.cast.KlumCastValidator;

import java.lang.annotation.ElementType;
Expand All @@ -37,7 +38,7 @@
*/
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@KlumCastValidator("com.blackbuild.klum.cast.checks.impl.ParameterTypesCheck")
@KlumCastValidator(value = "com.blackbuild.klum.cast.checks.impl.ParameterTypesCheck", validFor = Filter.Methods.class)
public @interface ParameterTypes {
Class<?>[] value();
boolean strict() default false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2023 Stephan Pauxberger
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.blackbuild.klum.cast.checks.impl;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.function.Predicate;
import java.util.stream.Stream;

public class AnnotationHelper {
private AnnotationHelper() {}


public static <T extends Annotation> Stream<Object> getValuesOfMembersAnnotatedWith(Annotation annotation, Class<T> memberAnnotationType) {
return getValuesOfMembersAnnotatedWith(annotation, memberAnnotationType, it -> true);
}

public static <T extends Annotation> Stream<Object> getValuesOfMembersAnnotatedWith(Annotation annotation, Class<T> memberAnnotationType, Predicate<T> filter) {
return Stream.of(annotation.annotationType().getDeclaredMethods())
.filter(m -> hasMatchingAnnotation(m, memberAnnotationType, filter))
.map(m -> invokeMemberMethod(annotation, m));
}

private static <T extends Annotation> boolean hasMatchingAnnotation(Method member, Class<T> annotationType, Predicate<T> filter) {
T memberAnnotation = member.getAnnotation(annotationType);
return memberAnnotation != null && filter.test(memberAnnotation);
}

private static Object invokeMemberMethod(Annotation annotation, Method m) {
try {
return m.invoke(annotation);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.jetbrains.annotations.Nullable;

import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Optional;

/**
Expand All @@ -48,13 +49,12 @@ public abstract class KlumCastCheck<T extends Annotation> {
@Nullable protected T controlAnnotation;
@Nullable protected String memberName;
@NotNull protected KlumCastValidator klumCastValidator;
@NotNull protected List<T> annotationStack;

public void setControlAnnotation(@Nullable T controlAnnotation) {
this.controlAnnotation = controlAnnotation;
}

public void setKlumCastValidatorAnnotation(@NotNull KlumCastValidator validatorAnnotation) {
this.klumCastValidator = validatorAnnotation;
public void setAnnotationStack(List<T> annotationStack) {
this.annotationStack = annotationStack.subList(0, annotationStack.size() - 1);
klumCastValidator = (KlumCastValidator) annotationStack.get(annotationStack.size() - 1);
controlAnnotation = annotationStack.size() > 1 ? annotationStack.get(annotationStack.size() - 2) : null;
}

public void setMemberName(@Nullable String memberName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
package com.blackbuild.klum.cast.checks.impl;

import com.blackbuild.klum.cast.checks.NeedsOneOf;
import com.blackbuild.klum.cast.validation.AstSupport;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;

Expand All @@ -41,9 +40,4 @@ protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) {
if (!controlAnnotation.exclusive() && matchingMembers.isEmpty())
throw new RuntimeException("At least one of " + Arrays.asList(controlAnnotation.value()) + " must be set");
}

@Override
protected boolean isValidFor(AnnotatedNode target) {
return AstSupport.matchesOneOf(controlAnnotation.whenOn(), target);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,4 @@ protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) {
if (Arrays.stream(controlAnnotation.value()).map(ClassHelper::make).noneMatch(r -> AstSupport.isAssignable(actualReturnType, r)))
throw new IllegalStateException("Method " + ((MethodNode) target).getName() + " must return one of " + Arrays.toString(controlAnnotation.value()) + ".");
}

@Override
protected boolean isValidFor(AnnotatedNode target) {
return target instanceof MethodNode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,4 @@ protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) {
));
}
}

@Override
protected boolean isValidFor(AnnotatedNode target) {
return target instanceof MethodNode;
}

}
Loading

0 comments on commit e6f65d7

Please sign in to comment.