chore: added docs

This commit is contained in:
zaaarf 2024-01-19 17:28:19 +01:00
parent e858d20172
commit 05f9c98dcd
No known key found for this signature in database
GPG key ID: C91CFF9E2262BBA1
4 changed files with 165 additions and 36 deletions

View file

@ -1,2 +1,4 @@
# Route Mapper # Route Compass
Just a little program I needed for work. Reads Spring's route mapping annotations in a project and writes down all about them. An annotation processor that reads Spring Web's annotations to write down a map of all the routes in your projects: their paths, parameters, methods...
It's a small program I found myself needing at work. Don't count on it being production-ready.

View file

@ -3,7 +3,7 @@ plugins {
} }
group = 'foo.zaaarf' group = 'foo.zaaarf'
version = '1.0-SNAPSHOT' version = '0.1'
repositories { repositories {
mavenCentral() mavenCentral()
@ -11,10 +11,4 @@ repositories {
dependencies { dependencies {
implementation 'org.springframework:spring-web:5.3.31' implementation 'org.springframework:spring-web:5.3.31'
testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
test {
useJUnitPlatform()
} }

View file

@ -4,18 +4,53 @@ import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
/** /**
* Internal representation of a REST route. * Representation of a REST route.
*/ */
public class Route { public class Route {
public final String route; /**
* The path of the endpoint.
*/
public final String path;
/**
* The supported {@link RequestMethod}s, flattened to a string.
*/
public final String method; public final String method;
/**
* The {@link MediaType} produced by the endpoint.
* May be null if not specified.
*/
public final String produces; public final String produces;
/**
* The {@link MediaType} consumed by the endpoint.
* May be null if not specified.
*/
public final String consumes; public final String consumes;
/**
* Whether the endpoint is deprecated.
*/
public final boolean deprecated; public final boolean deprecated;
/**
* An array of {@link Param}s, representing parameters accepted by the endpoint.
*/
public final Param[] params; public final Param[] params;
public Route(String route, RequestMethod[] methods, MediaType consumes, MediaType produces, boolean deprecated, Param... params) { /**
this.route = route; * The one and only constructor.
* @param path the path of the endpoint
* @param methods the {@link RequestMethod}s accepted by the endpoint
* @param consumes the {@link MediaType} consumed by the endpoint, may be null
* @param produces the {@link MediaType} produced by the endpoint, may be null
* @param deprecated whether the endpoint is deprecated
* @param params {@link Param}s of the endpoint, may be null
*/
public Route(String path, RequestMethod[] methods, MediaType consumes, MediaType produces, boolean deprecated,
Param... params) {
this.path = path;
StringBuilder methodStringBuilder = new StringBuilder("["); StringBuilder methodStringBuilder = new StringBuilder("[");
for(RequestMethod m : methods) for(RequestMethod m : methods)
@ -35,14 +70,36 @@ public class Route {
this.deprecated = deprecated; this.deprecated = deprecated;
this.params = params; if(params != null) this.params = params;
else this.params = new Param[0]; //just in case
} }
/**
* Representation of a parameter of a REST route.
*/
public static class Param { public static class Param {
/**
* The fully-qualified name of the expected type of the parameter.
*/
public final String typeFQN; public final String typeFQN;
/**
* The name of the parameter.
*/
public final String name; public final String name;
/**
* The default value of the parameter.
* May be null, in which case the parameter is required.
*/
public final String defaultValue; public final String defaultValue;
/**
* The one and only constructor.
* @param typeFQN the FQN of the expected type of the parameter
* @param name the name of the parameter
* @param defaultValue the default value of the parameter, may be null if the parameter is required
*/
public Param(String typeFQN, String name, String defaultValue) { public Param(String typeFQN, String name, String defaultValue) {
this.typeFQN = typeFQN; this.typeFQN = typeFQN;
this.name = name; this.name = name;

View file

@ -21,21 +21,42 @@ import java.util.*;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* The main processor class.
*/
@SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedSourceVersion(SourceVersion.RELEASE_8)
public class RouteCompass extends AbstractProcessor { public class RouteCompass extends AbstractProcessor {
private final HashMap<String, List<Route>> foundRoutes = new HashMap<>(); /**
private final HashSet<Class<? extends Annotation>> annotationClasses = new HashSet<>(); * A {@link Map} tying each component class to the routes it contains.
*/
private final Map<String, List<Route>> foundRoutes = new HashMap<>();
/**
* A {@link Set} containing all the supported annotation classes.
*/
private final Set<Class<? extends Annotation>> annotationClasses = new HashSet<>();
/**
* Default constructor, it only initialises {@link #annotationClasses}.
*/
public RouteCompass() { public RouteCompass() {
annotationClasses.add(RequestMapping.class); this.annotationClasses.add(RequestMapping.class);
annotationClasses.add(GetMapping.class); this.annotationClasses.add(GetMapping.class);
annotationClasses.add(PostMapping.class); this.annotationClasses.add(PostMapping.class);
annotationClasses.add(PutMapping.class); this.annotationClasses.add(PutMapping.class);
annotationClasses.add(DeleteMapping.class); this.annotationClasses.add(DeleteMapping.class);
annotationClasses.add(PatchMapping.class); this.annotationClasses.add(PatchMapping.class);
} }
/**
* Processes Spring's annotations, NOT claiming them for itself.
* It builds a {@link Route} object for each route and adds it to {@link #foundRoutes},
* then proceeds to print it to a file.
* @param annotations the annotation types requested to be processed
* @param env environment for information about the current and prior round
* @return false, letting other processor process the annotations again
*/
@Override @Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
for(TypeElement annotationType : annotations) { for(TypeElement annotationType : annotations) {
@ -70,7 +91,7 @@ public class RouteCompass extends AbstractProcessor {
for(Route r : routesInClass) { for(Route r : routesInClass) {
out.print("\t- "); out.print("\t- ");
if(r.deprecated) out.print("[DEPRECATED] "); if(r.deprecated) out.print("[DEPRECATED] ");
out.print(r.method + " " + r.route); out.print(r.method + " " + r.path);
if(r.consumes != null) out.print("(expects: " + r.consumes + ")"); if(r.consumes != null) out.print("(expects: " + r.consumes + ")");
if(r.produces != null) out.print("(returns: " + r.produces + ")"); if(r.produces != null) out.print("(returns: " + r.produces + ")");
out.println(); out.println();
@ -92,6 +113,12 @@ public class RouteCompass extends AbstractProcessor {
return false; //don't claim them, let spring do its job return false; //don't claim them, let spring do its job
} }
/**
* Extracts the route of an element.
* @param annotationType the {@link TypeElement} with the annotation we are processing
* @param element the {@link Element} currently being examined
* @return the full route of the endpoint
*/
private String getFullRoute(TypeElement annotationType, Element element) { private String getFullRoute(TypeElement annotationType, Element element) {
try { try {
String route = this.getAnnotationFieldsValue(annotationType, element, "path", "value"); String route = this.getAnnotationFieldsValue(annotationType, element, "path", "value");
@ -107,6 +134,27 @@ public class RouteCompass extends AbstractProcessor {
} }
} }
/**
* Finds the request methods supported by the endpoint.
* @param annotationType the {@link TypeElement} with the annotation we are processing
* @param element the {@link Element} currently being examined
* @return the {@link RequestMethod}s supported by the endpoint
*/
private RequestMethod[] getRequestMethods(TypeElement annotationType, Element element) {
RequestMethod[] methods = annotationType.getQualifiedName().contentEquals(RequestMapping.class.getName())
? element.getAnnotation(RequestMapping.class).method()
: annotationType.getAnnotation(RequestMapping.class).method();
return methods.length == 0
? this.getParentOrFallback(element, methods, this::getRequestMethods)
: methods;
}
/**
* Finds the media type consumed by an endpoint.
* @param annotationType the {@link TypeElement} with the annotation we are processing
* @param element the {@link Element} currently being examined
* @return the {@link MediaType} consumed by the endpoint
*/
private MediaType getConsumedType(TypeElement annotationType, Element element) { private MediaType getConsumedType(TypeElement annotationType, Element element) {
try { try {
MediaType res = this.getAnnotationFieldsValue(annotationType, element, "consumes"); MediaType res = this.getAnnotationFieldsValue(annotationType, element, "consumes");
@ -118,6 +166,12 @@ public class RouteCompass extends AbstractProcessor {
} }
} }
/**
* Finds the media type consumed by an endpoint.
* @param annotationType the {@link TypeElement} with the annotation we are processing
* @param element the {@link Element} currently being examined
* @return the {@link MediaType} consumed by the endpoint
*/
private MediaType getProducedType(TypeElement annotationType, Element element) { private MediaType getProducedType(TypeElement annotationType, Element element) {
try { try {
MediaType res = this.getAnnotationFieldsValue(annotationType, element, "produces"); MediaType res = this.getAnnotationFieldsValue(annotationType, element, "produces");
@ -129,20 +183,21 @@ public class RouteCompass extends AbstractProcessor {
} }
} }
private RequestMethod[] getRequestMethods(TypeElement annotationType, Element element) { /**
RequestMethod[] methods = annotationType.getQualifiedName().contentEquals(RequestMapping.class.getName()) * Checks whether the endpoint or its parent are deprecated
? element.getAnnotation(RequestMapping.class).method() * @param element the {@link Element} currently being examined
: annotationType.getAnnotation(RequestMapping.class).method(); * @return whether the given endpoint is deprecated
return methods.length == 0 */
? this.getParentOrFallback(element, methods, this::getRequestMethods) private boolean isDeprecated(Element element) {
: methods; return element.getAnnotation(Deprecated.class) != null
} || element.getEnclosingElement().getAnnotation(Deprecated.class) != null;
private boolean isDeprecated(Element elem) {
return elem.getAnnotation(Deprecated.class) != null
|| elem.getEnclosingElement().getAnnotation(Deprecated.class) != null;
} }
/**
* Gets the parameters accepted by a request.
* @param params the {@link VariableElement}s representing the parameters of a request
* @return an array of {@link Route.Param} representing the parameters of the request.
*/
private Route.Param[] getParams(List<? extends VariableElement> params) { private Route.Param[] getParams(List<? extends VariableElement> params) {
return params.stream() return params.stream()
.map(p -> { .map(p -> {
@ -165,6 +220,15 @@ public class RouteCompass extends AbstractProcessor {
}).filter(Objects::nonNull).toArray(Route.Param[]::new); }).filter(Objects::nonNull).toArray(Route.Param[]::new);
} }
/**
* An annotation value.
* @param annotationType the {@link TypeElement} with the annotation we are processing
* @param element the {@link Element} currently being examined
* @param fieldNames the field name(s) to look for; they are tried in order, and the first found is returned
* @return the field value, cast to the expected type
* @param <T> the expected type of the field
* @throws ReflectiveOperationException when given non-existing or inaccessible field names (hopefully never)
*/
@SuppressWarnings({"OptionalGetWithoutIsPresent", "unchecked"}) @SuppressWarnings({"OptionalGetWithoutIsPresent", "unchecked"})
private <T> T getAnnotationFieldsValue(TypeElement annotationType, Element element, String ... fieldNames) private <T> T getAnnotationFieldsValue(TypeElement annotationType, Element element, String ... fieldNames)
throws ReflectiveOperationException { throws ReflectiveOperationException {
@ -183,6 +247,15 @@ public class RouteCompass extends AbstractProcessor {
return result; return result;
} }
/**
* Finds whether the parent of the given element has any supported annotation, then applies the given
* function to both parent and found annotation.
* @param element the {@link Element} currently being examined
* @param fallback the value to return if the parent didn't have any supported annotations
* @param fun the {@link BiFunction} to apply
* @return the output or the function, or the fallback value if the parent didn't have any supported annotation
* @param <T> the type of the expected result
*/
private <T> T getParentOrFallback(Element element, T fallback, BiFunction<TypeElement, Element, T> fun) { private <T> T getParentOrFallback(Element element, T fallback, BiFunction<TypeElement, Element, T> fun) {
List<Class<? extends Annotation>> found = this.annotationClasses.stream() List<Class<? extends Annotation>> found = this.annotationClasses.stream()
.filter(annClass -> element.getEnclosingElement().getAnnotation(annClass) != null) .filter(annClass -> element.getEnclosingElement().getAnnotation(annClass) != null)
@ -194,7 +267,7 @@ public class RouteCompass extends AbstractProcessor {
Diagnostic.Kind.WARNING, Diagnostic.Kind.WARNING,
"Found multiple mapping annotations on " "Found multiple mapping annotations on "
+ element.getSimpleName().toString() + element.getSimpleName().toString()
+ ", only one of the will be considered!" + ", only one of them will be considered!"
); );
return fun.apply( return fun.apply(
@ -204,6 +277,9 @@ public class RouteCompass extends AbstractProcessor {
); );
} }
/**
* @return the types of annotations supported by this processor
*/
@Override @Override
public Set<String> getSupportedAnnotationTypes() { public Set<String> getSupportedAnnotationTypes() {
return annotationClasses.stream().map(Class::getCanonicalName).collect(Collectors.toSet()); return annotationClasses.stream().map(Class::getCanonicalName).collect(Collectors.toSet());