From 5953136d9db96c1c9ccf67d13be8a27a4700ca8b Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Sat, 20 Sep 2025 21:01:12 -0400 Subject: [PATCH 1/3] feat(docs): add @Sample annotation for documentation Marks integration test classes so that markdown pages can be auto generated. --- operator-annotations/pom.xml | 20 ++++++++++++++ .../main/java/io/javaoperatorsdk/Sample.java | 26 +++++++++++++++++++ pom.xml | 1 + 3 files changed, 47 insertions(+) create mode 100644 operator-annotations/pom.xml create mode 100644 operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java diff --git a/operator-annotations/pom.xml b/operator-annotations/pom.xml new file mode 100644 index 0000000000..41e94777ce --- /dev/null +++ b/operator-annotations/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + io.javaoperatorsdk + java-operator-sdk + 5.1.3-SNAPSHOT + + + operator-annotations + + + 22 + 22 + UTF-8 + + + \ No newline at end of file diff --git a/operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java b/operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java new file mode 100644 index 0000000000..49c6d9bc20 --- /dev/null +++ b/operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk; + +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/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 From 8b6f7db2913136b10ee20e9a56bce64409355cfc Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Mon, 29 Sep 2025 02:20:45 -0400 Subject: [PATCH 2/3] refactor: remove unneeded properties in pom.xml refactor: remove unneeded properties in pom.xml --- operator-annotations/pom.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/operator-annotations/pom.xml b/operator-annotations/pom.xml index 41e94777ce..2dd41a32c9 100644 --- a/operator-annotations/pom.xml +++ b/operator-annotations/pom.xml @@ -12,8 +12,6 @@ operator-annotations - 22 - 22 UTF-8 From 7e9be4aa253601b06426fb58383ca062127f57f2 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Mon, 6 Oct 2025 03:13:53 -0400 Subject: [PATCH 3/3] feat: Implement SampleProcessor to generate md files Implemented a sample processor to scan all samples and write the tldr/description to a md file Moved Sample.java to the annotation package --- .../{ => annotation}/Sample.java | 2 +- .../processor/SampleProcessor.java | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) rename operator-annotations/src/main/java/io/javaoperatorsdk/{ => annotation}/Sample.java (94%) create mode 100644 operator-annotations/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java diff --git a/operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java b/operator-annotations/src/main/java/io/javaoperatorsdk/annotation/Sample.java similarity index 94% rename from operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java rename to operator-annotations/src/main/java/io/javaoperatorsdk/annotation/Sample.java index 49c6d9bc20..62a39b0e3b 100644 --- a/operator-annotations/src/main/java/io/javaoperatorsdk/Sample.java +++ b/operator-annotations/src/main/java/io/javaoperatorsdk/annotation/Sample.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk; +package io.javaoperatorsdk.annotation; import java.lang.annotation.*; 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 ""; + } +}