Skip to content

Commit 4a04f2d

Browse files
Add description to @Overwrite, log override sources, and generate Markdown reference file
Signed-off-by: Nora <46890129+RainbowDashLabs@users.noreply.github.com>
1 parent c8709f5 commit 4a04f2d

5 files changed

Lines changed: 175 additions & 9 deletions

File tree

build.gradle.kts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ plugins {
1414

1515
publishData {
1616
useEldoNexusRepos(false)
17-
publishingVersion = "2.1.1"
17+
publishingVersion = "2.1.2"
1818
}
1919

2020
group = "dev.chojo"
@@ -119,4 +119,24 @@ tasks {
119119
compileJava {
120120
dependsOn(spotlessApply)
121121
}
122+
123+
register<Copy>("copyOverrideReference") {
124+
description = "Copies the generated override reference file to build/ocular/"
125+
from(fileTree(layout.buildDirectory) {
126+
include("**/META-INF/ocular/overrides.md")
127+
})
128+
eachFile {
129+
relativePath = RelativePath(true, name)
130+
}
131+
includeEmptyDirs = false
132+
into(layout.buildDirectory.dir("ocular"))
133+
}
134+
135+
named("classes") {
136+
finalizedBy("copyOverrideReference")
137+
}
138+
139+
test {
140+
finalizedBy("copyOverrideReference")
141+
}
122142
}

src/main/java/dev/chojo/ocular/override/OverrideApplier.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ public static <V> void applyOverrides(V object, ValueSupplier supplier) {
5454
// override values without the config class needing any special code.
5555
Class<?> clazz = object.getClass();
5656

57+
// Log available overrides with their descriptions
58+
logAvailableOverrides(clazz);
59+
5760
// Walk through every field in the config class (e.g. "private String host")
5861
// and check if the generated supplier has an override value for that field name.
5962
for (Field field : clazz.getDeclaredFields()) {
@@ -178,6 +181,75 @@ private static Field findBackingField(Class<?> clazz, String methodName, Class<?
178181
return bestMatch;
179182
}
180183

184+
/**
185+
* Logs all available overrides for the given configuration class, including their descriptions
186+
* if provided via the {@link Overwrite#description()} attribute.
187+
* Takes {@link OverwritePrefix} into account for deriving default keys and forced prefixes.
188+
*/
189+
private static void logAvailableOverrides(Class<?> clazz) {
190+
String prefix = clazz.getSimpleName();
191+
boolean forcePrefix = false;
192+
OverwritePrefix overwritePrefix = clazz.getAnnotation(OverwritePrefix.class);
193+
if (overwritePrefix != null) {
194+
prefix = overwritePrefix.value();
195+
forcePrefix = overwritePrefix.force();
196+
}
197+
198+
boolean headerLogged = false;
199+
for (Field field : clazz.getDeclaredFields()) {
200+
Overwrite overwrite = field.getAnnotation(Overwrite.class);
201+
if (overwrite != null) {
202+
if (!headerLogged) {
203+
log.info("Available overrides for {}:", clazz.getSimpleName());
204+
headerLogged = true;
205+
}
206+
logOverwriteSources(overwrite, prefix, forcePrefix, field.getName());
207+
}
208+
}
209+
for (Method method : clazz.getDeclaredMethods()) {
210+
Overwrite overwrite = method.getAnnotation(Overwrite.class);
211+
if (overwrite != null) {
212+
if (!headerLogged) {
213+
log.info("Available overrides for {}:", clazz.getSimpleName());
214+
headerLogged = true;
215+
}
216+
logOverwriteSources(overwrite, prefix, forcePrefix, method.getName());
217+
}
218+
}
219+
}
220+
221+
/**
222+
* Logs the property and environment variable sources for a single {@link Overwrite} annotation,
223+
* taking the prefix and force flag into account.
224+
*/
225+
private static void logOverwriteSources(Overwrite overwrite, String prefix, boolean forcePrefix, String memberName) {
226+
String desc = overwrite.description().isEmpty() ? "" : " - " + overwrite.description();
227+
for (Prop prop : overwrite.prop()) {
228+
String propPrefix = prefix.replace("_", ".").toLowerCase();
229+
String key;
230+
if (prop.value().isEmpty()) {
231+
key = propPrefix + "." + memberName;
232+
} else if (forcePrefix) {
233+
key = propPrefix + "." + prop.value();
234+
} else {
235+
key = prop.value();
236+
}
237+
log.info(" Property: {}{}", key, desc);
238+
}
239+
for (Env env : overwrite.env()) {
240+
String envPrefix = prefix.replace(".", "_").toUpperCase();
241+
String key;
242+
if (env.value().isEmpty()) {
243+
key = envPrefix + "_" + memberName.toUpperCase();
244+
} else if (forcePrefix) {
245+
key = envPrefix + "_" + env.value();
246+
} else {
247+
key = env.value();
248+
}
249+
log.info(" Environment: {}{}", key, desc);
250+
}
251+
}
252+
181253
/**
182254
* Converts a string override value to the target field/parameter type.
183255
* Supports all Java primitive types and their boxed equivalents, plus String.

src/main/java/dev/chojo/ocular/override/Overwrite.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* Marks a field or method in a configuration class as overridable at runtime.
1515
* <p>
1616
* When a configuration class contains fields or methods annotated with {@code @Overwrite},
17-
* the annotation processor ({@link dev.chojo.ocular.processor.OcularProcessor}) will generate
17+
* the annotation processor ({@link dev.chojo.ocular.processor.OcularProcessor OcularProcessor}) will generate
1818
* a helper class at compile time that knows how to look up override values from environment
1919
* variables and/or system properties.
2020
* <p>
@@ -39,4 +39,9 @@
3939
* System property sources to check for an override value.
4040
*/
4141
Prop[] prop() default {};
42+
43+
/**
44+
* Description of the override.
45+
*/
46+
String description() default "";
4247
}

src/main/java/dev/chojo/ocular/processor/OcularProcessor.java

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
*/
66
package dev.chojo.ocular.processor;
77

8-
import dev.chojo.ocular.override.OverwritePrefix;
98
import dev.chojo.ocular.override.Overwrite;
9+
import dev.chojo.ocular.override.OverwritePrefix;
1010

1111
import javax.annotation.processing.AbstractProcessor;
1212
import javax.annotation.processing.Filer;
@@ -23,10 +23,14 @@
2323
import javax.lang.model.element.PackageElement;
2424
import javax.lang.model.element.TypeElement;
2525
import javax.tools.Diagnostic;
26+
import javax.tools.FileObject;
2627
import javax.tools.JavaFileObject;
28+
import javax.tools.StandardLocation;
2729
import java.io.IOException;
30+
import java.io.Writer;
2831
import java.util.ArrayList;
2932
import java.util.HashMap;
33+
import java.util.LinkedHashMap;
3034
import java.util.List;
3135
import java.util.Map;
3236
import java.util.Set;
@@ -125,16 +129,30 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
125129
classesToProcess.computeIfAbsent(enclosingClass, k -> new ArrayList<>()).add(element);
126130
}
127131

132+
// Collect override info for the reference file
133+
Map<String, List<OverrideInfo>> allOverrides = new LinkedHashMap<>();
134+
128135
// Generate one override provider class per config class
129136
for (Map.Entry<TypeElement, List<Element>> entry : classesToProcess.entrySet()) {
130137
try {
131-
generateOverrideProvider(entry.getKey(), entry.getValue());
138+
List<OverrideInfo> infos = generateOverrideProvider(entry.getKey(), entry.getValue());
139+
allOverrides.put(entry.getKey().getQualifiedName().toString(), infos);
132140
} catch (IOException e) {
133141
messager.printMessage(Diagnostic.Kind.ERROR,
134142
"Could not generate override provider for " + entry.getKey().getQualifiedName() + ": " + e.getMessage());
135143
}
136144
}
137145

146+
// Generate reference documentation file
147+
if (!allOverrides.isEmpty()) {
148+
try {
149+
generateOverrideDocumentation(allOverrides);
150+
} catch (IOException e) {
151+
messager.printMessage(Diagnostic.Kind.ERROR,
152+
"Could not generate override documentation: " + e.getMessage());
153+
}
154+
}
155+
138156
return true;
139157
}
140158

@@ -148,7 +166,41 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
148166
* <li>A {@code getValue()} method that looks up a field name in the map.</li>
149167
* </ul>
150168
*/
151-
private void generateOverrideProvider(TypeElement typeElement, List<Element> elements) throws IOException {
169+
/**
170+
* Holds information about a single override for documentation and logging purposes.
171+
*/
172+
private record OverrideInfo(String fieldName, String description, List<String> sources) {
173+
}
174+
175+
/**
176+
* Generates a Markdown reference file at {@code META-INF/ocular/overrides.md} listing all
177+
* overridable properties and environment variables with their descriptions.
178+
*/
179+
private void generateOverrideDocumentation(Map<String, List<OverrideInfo>> allOverrides) throws IOException {
180+
FileObject resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/ocular/overrides.md");
181+
try (Writer writer = resource.openWriter()) {
182+
writer.append("# Ocular Override Reference\n\n");
183+
writer.append("This file is auto-generated by the Ocular annotation processor.\n");
184+
writer.append("It lists all available configuration overrides.\n\n");
185+
186+
for (Map.Entry<String, List<OverrideInfo>> entry : allOverrides.entrySet()) {
187+
writer.append("## ").append(entry.getKey()).append("\n\n");
188+
for (OverrideInfo info : entry.getValue()) {
189+
writer.append("### ").append(info.fieldName()).append("\n\n");
190+
if (!info.description().isEmpty()) {
191+
writer.append(info.description()).append("\n\n");
192+
}
193+
writer.append("Sources (in priority order):\n");
194+
for (String source : info.sources()) {
195+
writer.append("- ").append(source).append("\n");
196+
}
197+
writer.append("\n");
198+
}
199+
}
200+
}
201+
}
202+
203+
private List<OverrideInfo> generateOverrideProvider(TypeElement typeElement, List<Element> elements) throws IOException {
152204
// Extract the package (e.g. "com.example") so we can put the generated file in the same package
153205
String packageName = ((PackageElement) typeElement.getEnclosingElement()).getQualifiedName().toString();
154206
// Full name like "com.example.MyConfig"
@@ -170,6 +222,9 @@ private void generateOverrideProvider(TypeElement typeElement, List<Element> ele
170222

171223
// Ask the compiler to create a new .java source file. The compiler will automatically
172224
// compile this generated file in the same compilation round — we just need to write valid Java into it.
225+
// Collect override info for documentation
226+
List<OverrideInfo> overrideInfos = new ArrayList<>();
227+
173228
JavaFileObject builderFile = filer.createSourceFile(packageName + "." + generatedClassName);
174229
// SourceWriter is a helper that handles indentation so the generated code is readable
175230
try (SourceWriter out = new SourceWriter(builderFile.openWriter())) {
@@ -191,7 +246,10 @@ private void generateOverrideProvider(TypeElement typeElement, List<Element> ele
191246
for (Element element : elements) {
192247
String fieldName = element.getSimpleName().toString();
193248
// Write the lookup code for this field's env/prop sources into the constructor
194-
emitLookupsInOrder(out, element, prefix, forcePrefix, fieldName);
249+
OverrideInfo info = emitLookupsInOrder(out, element, prefix, forcePrefix, fieldName);
250+
if (info != null) {
251+
overrideInfos.add(info);
252+
}
195253
}
196254
out.endBlock();
197255

@@ -203,6 +261,7 @@ private void generateOverrideProvider(TypeElement typeElement, List<Element> ele
203261

204262
out.endBlock();
205263
}
264+
return overrideInfos;
206265
}
207266

208267
/**
@@ -217,12 +276,17 @@ private void generateOverrideProvider(TypeElement typeElement, List<Element> ele
217276
* code structure (mirrors), not the actual runtime annotation objects. Mirrors preserve the
218277
* declaration order of attributes, which is essential for correct precedence.
219278
*/
220-
private void emitLookupsInOrder(SourceWriter out, Element element, String prefix, boolean forcePrefix, String fieldName) throws IOException {
279+
private OverrideInfo emitLookupsInOrder(SourceWriter out, Element element, String prefix, boolean forcePrefix, String fieldName) throws IOException {
221280
// Get the compile-time representation ("mirror") of the @Overwrite annotation on this field.
222281
// We need the mirror (not the annotation object) because at compile time the annotation class
223282
// isn't loaded as a real object — it only exists as metadata in the source code.
224283
AnnotationMirror overwriteMirror = findOverwriteMirror(element);
225-
if (overwriteMirror == null) return;
284+
if (overwriteMirror == null) return null;
285+
286+
// Extract the description from the @Overwrite annotation
287+
String description = extractStringValue(overwriteMirror, "description");
288+
if (description == null) description = "";
289+
List<String> sources = new ArrayList<>();
226290

227291
// Walk through each attribute of @Overwrite in the order the user wrote them.
228292
// For example, in @Overwrite(prop = @Prop(), env = @Env()), we'd iterate:
@@ -232,6 +296,8 @@ private void emitLookupsInOrder(SourceWriter out, Element element, String prefix
232296
// The first source that provides a non-null value wins.
233297
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry :
234298
overwriteMirror.getElementValues().entrySet()) {
299+
// Skip the description value
300+
if (entry.getValue().getValue().getClass() == String.class) continue;
235301
// attrName is either "prop" or "env" — the attribute name from @Overwrite
236302
String attrName = entry.getKey().getSimpleName().toString();
237303
// The value is an array of nested annotations (e.g. the @Prop[] or @Env[] array)
@@ -255,6 +321,7 @@ private void emitLookupsInOrder(SourceWriter out, Element element, String prefix
255321
}
256322
// Write Java code like: String value = System.getProperty("myclass.host");
257323
emitLookup(out, fieldName, "System.getProperty(\"" + key + "\")");
324+
sources.add("Property: `" + key + "`");
258325
}
259326
} else if ("env".equals(attrName)) {
260327
prefix = prefix.replace(".", "_").toUpperCase();
@@ -271,9 +338,11 @@ private void emitLookupsInOrder(SourceWriter out, Element element, String prefix
271338
}
272339
// Write Java code like: String value = System.getenv("MYCLASS_HOST");
273340
emitLookup(out, fieldName, "System.getenv(\"" + key + "\")");
341+
sources.add("Environment: `" + key + "`");
274342
}
275343
}
276344
}
345+
return new OverrideInfo(fieldName, description, sources);
277346
}
278347

279348
/**

src/test/java/dev/chojo/classes/AnnotationConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
public class AnnotationConfig {
1313
// prop is declared first, so it takes precedence over env
14-
@Overwrite(prop = @Prop, env = @Env)
14+
@Overwrite(prop = @Prop, env = @Env, description = "Some description")
1515
public String test;
1616

1717
// explicit keys

0 commit comments

Comments
 (0)