mirror of
https://github.com/zaaarf/route-cartographer.git
synced 2024-12-05 00:24:53 +01:00
chore: added docs
This commit is contained in:
parent
e858d20172
commit
05f9c98dcd
4 changed files with 165 additions and 36 deletions
|
@ -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.
|
|
@ -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()
|
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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());
|
||||||
|
|
Loading…
Reference in a new issue