diff --git a/operator-annotations/pom.xml b/operator-annotations/pom.xml new file mode 100644 index 0000000000..2dd41a32c9 --- /dev/null +++ b/operator-annotations/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + io.javaoperatorsdk + java-operator-sdk + 5.1.3-SNAPSHOT + + + operator-annotations + + + UTF-8 + + + \ No newline at end of file diff --git a/operator-annotations/src/main/java/io/javaoperatorsdk/annotation/Sample.java b/operator-annotations/src/main/java/io/javaoperatorsdk/annotation/Sample.java new file mode 100644 index 0000000000..62a39b0e3b --- /dev/null +++ b/operator-annotations/src/main/java/io/javaoperatorsdk/annotation/Sample.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.annotation; + +import java.lang.annotation.*; + +/** + * This annotation marks an integration test class as a sample for the documentation. + * Intended for use on test classes only. + * + *

Example: + *

{@code
+ * @Sample(
+ *  tldr="Usage of PrimaryToSecondaryMapper",
+ *  description="Showcases the usage of PrimaryToSecondaryMapper, in what situation it needs to be used and how to optimize typical uses with Informer indexes."
+ * )
+ * class PrimaryToSecondaryIT {
+ *   // details omitted
+ * }
+ * }
+ */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +@Documented +public @interface Sample { + String tldr(); + String description(); +} \ No newline at end of file diff --git a/operator-annotations/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java b/operator-annotations/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java new file mode 100644 index 0000000000..6c7934189d --- /dev/null +++ b/operator-annotations/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java @@ -0,0 +1,96 @@ +package io.javaoperatorsdk.processor; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.lang.model.element.*; +import javax.lang.model.util.Types; +import javax.tools.FileObject; +import javax.tools.StandardLocation; +import java.io.IOException; +import java.io.Writer; +import java.util.*; + +/** + * Annotation processor that generates a markdown file listing all classes annotated with @Sample. + */ +@SupportedAnnotationTypes("io.javaoperatorsdk.annotation.Sample") +public class SampleProcessor extends AbstractProcessor { + + private record SampleInfo(String tldr, String description) {} + private final List samples = new ArrayList<>(); + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + + Types types = processingEnv.getTypeUtils(); + for (TypeElement annotation: annotations) { + // element has details about the class being annotated, but not the values + // ex: String tldr = ..., it knows it has a field called tldr but not what's assigned + for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { + // a mirror gives access to the values assigned to the fields of the annotation + // element.getAnnotation does not work since the retention is SOURCE + AnnotationMirror annotationMirror = element.getAnnotationMirrors().stream() + .filter(am -> types.isSameType(am.getAnnotationType(), annotation.asType())) + .findFirst() + .orElse(null); + + if (annotationMirror != null) { + String tldr = getString(annotationMirror.getElementValues(), "tldr"); + String description = getString(annotationMirror.getElementValues(), "description"); + + samples.add(new SampleInfo(tldr, description) ); + } + } + } + + if (roundEnv.processingOver()) { + // sort to keep the order stable + samples.sort(Comparator.comparing(SampleInfo::tldr, String.CASE_INSENSITIVE_ORDER)); + writeSampleMDFile(samples); + } + return false; + } + + /** + * + */ + private void writeSampleMDFile(List samples) { + try { + FileObject fileObject = processingEnv.getFiler() + .createResource(StandardLocation.CLASS_OUTPUT, "", "samples.md"); + + try(Writer writer = fileObject.openWriter();) { + writer.write("# Integration Test Samples \n"); + + for (SampleInfo sample : samples) { + writer.write("## " + sample.tldr() + "\n"); + writer.write(sample.description() + "\n\n"); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Extracts a string value from the annotation values map. + * @param vals the map of annotation values + * @param name the name of the field to extract + * @return the string value, or empty string if not found + */ + private String getString( + Map vals, String name) { + for (Map.Entry ev : vals.entrySet()) { + if (ev.getKey().getSimpleName().contentEquals(name)) { + Object value = ev.getValue().getValue(); + return value == null ? "" : value.toString(); + } + } + // should not happen since tldr and description are mandatory + return ""; + } +} diff --git a/pom.xml b/pom.xml index 2afcd8448a..69524810b7 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ sample-operators caffeine-bounded-cache-support bootstrapper-maven-plugin + operator-annotations