Skip to content

Commit c1a5433

Browse files
committed
More documentation
1 parent 0f404c7 commit c1a5433

File tree

3 files changed

+181
-26
lines changed

3 files changed

+181
-26
lines changed

README.md

Lines changed: 179 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,63 @@ Check those annotations with style!
77

88
KlumCast is validator for annotation placement for Groovy based schemas. It allows to conveniently validate AST driving annotations before the actual transformation is performed and thus helps keep the transformation code clean.
99

10+
## For whom is KlumCast?
11+
12+
KlumCast is for you if you are writing a Groovy AST transformation that is driven by annotations.
13+
14+
It is also relevant for extenders of an existing framework, for example a Layer3 approach with KlumDSL can hugely benefit from the usage of KlumCast.
15+
16+
## Basic example
17+
18+
Lets consider a an annotation that creates convenience methods to fill add entries to collection fields:
19+
20+
```groovy
21+
class MyClass {
22+
@AutoAdd
23+
List<String> names
24+
}
25+
```
26+
27+
Based on this setup, the following code would be generated:
28+
29+
```groovy
30+
void addName(String name) {
31+
if (names == null) names = []
32+
names.add(name)
33+
}
34+
35+
void addNames(Collection<String> names) {
36+
if (this.names == null) this.names = []
37+
this.names.addAll(names)
38+
}
39+
40+
void addNames(String... names) {
41+
if (this.names == null) this.names = []
42+
this.names.addAll(names)
43+
}
44+
```
45+
46+
A Groovy AST-Transformation generating the above code would be easy to implement.
47+
However, before actually creating the methods, it must be assured, that the field to be transformed is actually a collection. Otherwise, compilation errors or worse, runtime errors would occur.
48+
49+
Making this check is easy to implement, but it would clutter the transformation code as well as the javadoc of the annotation. KlumCast allows to move this check to a separate class, which is then called by the AST-Transformation. It also includes a couple of preconfigured validations that can be used out of the box.
50+
51+
Note that annotation, checks and transformation can be written in Java as well as in Groovy, only the actual model schema - and of course the model itself - needs to be written in Groovy.
52+
53+
```java
54+
import java.lang.annotation.Retention;
55+
import java.lang.annotation.Target;
56+
57+
@Target(ElementType.FIELD)
58+
@Retention(RetentionPolicy.RUNTIME)
59+
@KlumCastValidated
60+
@NeedsType(Collection)
61+
@interface AutoAdd {}
62+
```
63+
64+
This is a lot cleaner and easier to understand, as well as less effort in implementing the transformation.
65+
66+
1067
# Quick explainer: models, schemas and AST transformations
1168

1269
Since KlumCast derived from Klum-AST, it is a fitting example to explain the key concepts of KlumCast.
@@ -71,7 +128,20 @@ The Annotations in the above example all have to follow additional placement rul
71128

72129
# Usage
73130

74-
In order to use KlumCast on an annotation, the validation to check needs to be annotated with `@KlumCastValidated` as well as the actual validation annotations as in the example above
131+
## Basic usage: use provided validations
132+
133+
### Dependencies
134+
135+
KlumCast ist split into two modules: klum-cast-annotations and klum-cast-compile. The annotations module contains the preconfigured validations as well as the base class for custom validations. The compile module contains the AST-Transformation that is applied to the schema.
136+
137+
The annotations module must be present at runtime (since the validation transformation needs access to the compiled classes of the annotations). However,
138+
the compile module only needs to be present during the compilation of the schema, but not during the compilation of the model itself, i.e. it should be a compileOnly dependency in Gradle or an optional dependency in Maven. Since the compile module contains a global AST-Transformation, it would have a slight impact on the compilation time of the model, so it should be avoided to have it present during the compilation of the model.
139+
140+
If a project is split into the usual three modules (annotations, ast and runtime), the klumcast-annotations module should be a regular dependency of the annotations module (`compile` for Maven, `api` or `implementation` for Gradle), while the klumcast-compile module should be a compileOnly dependency of the ast module (`optional` for Maven, `compileOnly` for Gradle). See KlumAST for an example.
141+
142+
### Declaring validations
143+
144+
In order to use KlumCast on an annotation, the validation to check needs to be annotated with `@KlumCastValidated` as well as the actual validation annotations as in the example above. The `@KlumCastValidated` annotation is only used to mark the annotation as validation target and is not used by the AST-Transformation itself. It allows the validation transformation to easily detect the annotations to be processed.
75145

76146
```java
77147
@Target([ElementType.FIELD, ElementType.METHOD])
@@ -84,70 +154,152 @@ public @interface Field {
84154
}
85155
```
86156

87-
Note that the validations to be annotated can be implemented in Java or Groovy, but place where the annotation is used needs to be Groovy, since the AST transformation is only applied to Groovy code.
157+
Note that the validations to be annotated can be implemented in Java or Groovy, but the place where the annotation is used needs to be Groovy, since the AST transformation is only applied to Groovy code.
88158

89-
In order for the validation to be applied, the KlumCast library needs to be present during compile time of schema, but it does not need to be present during the compilation/loading of the model (i.e. the compileOnly in case of Gradle or optional in case of Maven).
159+
### Member annotations
90160

91-
# Validation annotations
161+
KlumCast also supports validation annotations placed on members of the validated annotation, which makes syntax more concise.
92162

93-
The following validation annotations are available:
163+
For example, the `@Validate` annotation of KlumAST can be placed on classes as well as on fields and methods. Depending on the placement, only specific members of the annotation are valid:
164+
165+
```java
166+
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
167+
@Retention(RetentionPolicy.RUNTIME)
168+
@KlumCastValidated
169+
@NumberOfParameters(0)
170+
@Documented
171+
public @interface Validate {
94172

95-
## @ClassNeedsAnnotation
173+
@NotOn({ ElementType.METHOD , ElementType.TYPE })
174+
Class<? extends Closure> value() default GroovyTruth.class;
175+
176+
@NotOn({ ElementType.METHOD , ElementType.TYPE })
177+
String message() default "";
178+
}
179+
```
180+
### Included validations
181+
182+
KlumCast includes a couple of validations that can be used out of the box:
183+
184+
#### @ClassNeedsAnnotation
96185

97186
Checks if the class the annotated element is part of is annotated with the given annotation.
98187

99-
## @NumberOfParameters
188+
#### @NumberOfParameters
100189

101190
Checks that the annotated methods has exactly the given number of parameters. Note that if the annotated element is no method, the validation is ignored.
102191

103-
## @AllowedMembers
192+
#### @MustBeStatic
193+
194+
If the validated annotation is placed on a method, the method must be static. Has no effect if the annotation is placed on any other element.
195+
196+
#### @MutuallyExclusive
197+
198+
Designates that the annotated members are mutually exclusive, i.e. only one of them can be set at the same time.
199+
200+
#### @NeedsReturnType
201+
202+
Checks that the annotated method has the given return type. Note that if the annotated element is no method, the validation is ignored.
203+
204+
#### @ParameterTypes
205+
206+
Forces the parameters of an annotated method to be of the given type. Note that if the annotated element is no method, the validation is ignored.
207+
208+
#### @UniquePerClass
209+
210+
The annotation must only be used once per class.
211+
212+
#### @AlsoNeeds
213+
214+
The AlsoNeeds annotation is used to specify that a certain annotation member should be used together with one or more specific annotation members. It can only be used on annotation members.
215+
216+
#### @NotTogetherWith
217+
218+
The NotTogetherWith annotation is used to specify that a certain annotation member should not be used together with one or more specific annotation members. It can only be used on annotation members.
219+
220+
#### @NotOn
104221

105-
Can be used multiple times for various targets. Checks for the matching target that only the given members of the annotation are set or that some annotations are forbidden.
222+
The NotOn annotation is used to specify that a certain annotation member should not be used on a specific element type. It can only be used on annotation members.
106223

107-
# Custom validations
224+
#### @OnlyOn
108225

109-
Custom validations consist of two elements, the annotation and a validator class. The annotation needs to be annotated with `@KlumCastValidated` and the validator class needs to extends `KlumCastCheck`.
226+
The OnlyOn annotation is used to specify that a certain annotation member should only be used on a specific element type. It can only be used on annotation members.
110227

111-
## The annotation
228+
## Nested annotations
112229

113-
The annotation needs to be of Runtime retention and target only Annotations. It is annotated with `@KlumCastValidator` which points to the classname of the validator class.
230+
Validation annotations can themselves be aggregations of multiple annotations. This is useful if a combination of validations is used multiple times. Or to give
231+
an annotation a domain specific name.
232+
233+
Note that Target and Retention annotations are omitted in the examples below for brevity.
234+
235+
```groovy
236+
@KlumCastValidated
237+
@ClassNeedsAnnotation(DSL)
238+
@interface NeedsDslClass {}
239+
240+
@KlumCastValidated
241+
@NumberOfParameters(1)
242+
@NeedsReturnType(Void)
243+
@interface SetterLike {}
244+
```
245+
## Custom validations
246+
247+
Custom validation can be declared using the `@KlumCastValidator` annotation. This annotation points to the class implementing the validation. The class must extend `KlumCastCheck` and have the actual validator annotation as type parameter. This allows for easy parametrizing of the validator.
248+
249+
So usually, a custom validation consist of two elements, the control annotation (which is eventually placed on the target annotation to mark it as validated) and a validator class. The control annotation needs to be annotated with `@KlumCastValidator` and the validator class needs to extend `KlumCastCheck`.
250+
251+
## The control annotation
252+
253+
The control annotation needs to be of Runtime retention and target only Annotations. It is annotated with `@KlumCastValidator` which points to the classname or type of the validator class. The annotation should have a clear name and contain further members to parametrize the validator.
114254

115255
```groovy
116256
@Target(ElementType.ANNOTATION_TYPE)
117257
@Retention(RetentionPolicy.RUNTIME)
118-
@KlumCastValidator("my.MyValidator")
119-
@interface MyAnnotation {
258+
@KlumCastValidator("my.NameMustMatchCheck")
259+
@interface NameMustMatch {
120260
String value()
121261
}
122262
```
123263

124-
## the validator class
264+
## The validator class
265+
266+
The validator class extends `KlumCastCheck` and have the annotation as Type-Parameter. The actual check is usually implemented by the `doCheck` method, which has access to the following information:
267+
268+
- the control annotation (as annotation object, `NameMustMatch` in the example above (if the KlumCastValidatior annotation is placed directly on the annotation to be validated, this can be null)
269+
- the `KlumCastValidator` annotation (which can have an additional String array to further parametrize the validator), in the example above this would be in instance of `@KlumCastValidator("my.NameMustMatch")`
270+
- the annotation target, i.e. the annotated element itself as an `AnnotatedNode` instance
271+
- the annotation to validate, i.e. the annotation that is annotated with the control annotation, as an `AnnotationNode` instance
272+
- if the control annotation is placed on a member of the annotation to validate, that member's name as a String
125273

126-
The validator class extends `KlumCastCheck` and have the annotation as Type-Parameter. The `doCheck` method receives the AST node of the annotated element and the annotation itself. Any exception thrown inside this method is converted to a Compilation Error.
274+
The last to elements are Groovy-Compiler AST-Nodes.
275+
276+
The `doCheck` should perform necessary validations and eventually either return or throw an exception. If an exception is thrown, it is converted to a compilation error.
127277

128278
```groovy
129-
class MyValidator extends KlumCastCheck<MyAnnotation> {
279+
class NameMustMatchCheck extends KlumCastCheck<NameMustMatch> {
130280
@Override
131281
protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) {
132-
if (target.getText().equalsIgnoreCase("foo")) {
133-
throw new IllegalStateException("must not be placed on Foos")
282+
if (!target.getText().startsWith(controlAnnotation.value())) {
283+
throw new IllegalStateException("Target element must start with ${controlAnnotation.value()}")
134284
}
135285
}
136286
}
137287
```
138288

289+
Additionally, the method `isValidFor(AnnotatedNode target)` can be overridden quickly skip the check if necessary.
290+
139291
## Check as inner class
140292

141293
For convenience, the validator class can be implemented as inner class to the annotation itself. In that case, the alternative syntax using `type` instead of `value` is useful.
142294

143295
```groovy
144296
@Target(ElementType.ANNOTATION_TYPE)
145297
@Retention(RetentionPolicy.RUNTIME)
146-
@KlumCastValidator(type = MyAnnotation.Check.class)
147-
@interface MyAnnotation {
298+
@KlumCastValidator(type = NeedsSomething.Check.class)
299+
@interface NeedsSomething
148300
String value()
149301
150-
class Check extends KlumCastCheck<MyAnnotation> {
302+
class Check extends KlumCastCheck<NeedsSomething> {
151303
@Override
152304
protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) {
153305
if (target.getText().equalsIgnoreCase("foo")) {
@@ -157,4 +309,7 @@ For convenience, the validator class can be implemented as inner class to the an
157309
}
158310
159311
}
160-
```
312+
```
313+
314+
Note that annotations and KlumCastValidator annotations can be freely mixed, i.e. a control annotation can have multiple KlumCastValidator annotations as well as multiple control annotations (which themselves can have multiple KlumCastValidator annotations or even more control annotations). Just remember that annotations neither having
315+
`KlumCastValidated` nor `KlumCastValidator` are ignored by the AST-Transformation.

klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/impl/KlumCastCheck.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public void setControlAnnotation(@Nullable T controlAnnotation) {
5353
this.controlAnnotation = controlAnnotation;
5454
}
5555

56-
public void setValidatorAnnotation(@NotNull KlumCastValidator validatorAnnotation) {
56+
public void setKlumCastValidatorAnnotation(@NotNull KlumCastValidator validatorAnnotation) {
5757
this.klumCastValidator = validatorAnnotation;
5858
}
5959

klum-cast-compile/src/main/java/com/blackbuild/klum/cast/validation/ValidationHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ private void executeValidator(KlumCastValidator validator) {
127127
if (!KlumCastCheck.class.isAssignableFrom(type))
128128
throw new IllegalStateException("Class " + validator.value() + " is not a KlumCastCheck.");
129129
KlumCastCheck<Annotation> check = (KlumCastCheck<Annotation>) InvokerHelper.invokeNoArgumentsConstructorOf(type);
130-
check.setValidatorAnnotation(validator);
130+
check.setKlumCastValidatorAnnotation(validator);
131131
check.setControlAnnotation(currentAnnotation);
132132
check.setMemberName(currentMember);
133133
check.check(annotationToValidate, target).ifPresent(errors::add);

0 commit comments

Comments
 (0)