Skip to content

Commit

Permalink
prepare for Rex API docs
Browse files Browse the repository at this point in the history
Took 3 hours 17 minutes
  • Loading branch information
duncpro committed Nov 20, 2021
1 parent 413aa31 commit 8755017
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 36 deletions.
68 changes: 46 additions & 22 deletions .idea/workspace.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,39 @@ All classes are marked with JCIP annotations describing the level of thread-safe
Notably `TreeRouter` is not thread safe for mutations. Consider wrapping it in a `ReadWriteLock` if you wish
to add routes concurrently. In practice route registration typically only happens at startup and the process is quick
enough that multi-threading is unnecessary.
## Docs

## Router Inspection and Summation
In some cases, like when generating API documentation, an application might need to
inspect the state of a router at runtime. This can be accomplished using the `Router.getAllEndpoints()` method.

The following example generates a set of all the endpoints which exist on, and descend from,
the root route. In other words, all endpoints registered with the router.
```java
final Router<T> routers = /* ... */;
final Set<PositionedEndpoint<T>> positionedEndpoints = router.getAllEndpoints(Route.ROOT);
```

### Summing Multiple Routers
For large applications consisting of many modules it can be useful to assign
each module its own `Router`. The following example demonstrates summing two routers.

Assume that we are using some library which generates API documentation for a
given `Router` and returns a new `Router` which holds that documentation...
````java
final Router<T> appRouter = /* ... */;
final Router<T> docRouter = generateApiDocs(appRouter);
final TreeRouter<T> masterRouter = TreeRouter.sum(Set.of(appRouter, docRouter));
````
Now there is a single Router which contains the routes for our application and our application's documentation.
### Prefixing Routers
`TreeRouter.prefix` produces a copy of the original router except all routes begin with an arbitrary prefix `Route`.
Our original documentation example can be extended by storing all API docs under a specific route.
```java
final Router<T> docRouter = TreeRouter.prefix(generateApiDocs(appRouter),
new Route("/docs"));
```
## More Docs
There is a Javadoc for this library [here](https://duncpro.github.io/JRoute).
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = "com.duncpro"
version = "1.0-SNAPSHOT-2"
version = "1.0-SNAPSHOT-3"

repositories {
mavenCentral()
Expand All @@ -18,7 +18,7 @@ dependencies {
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

tasks.getByName<Test>("test") {
tasks.test {
useJUnitPlatform()
}

Expand Down
14 changes: 12 additions & 2 deletions src/main/java/com/duncpro/jroute/route/Route.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
public class Route {
private final List<RouteElement> elements;

protected Route(List<RouteElement> elements) {
this.elements = Collections.unmodifiableList(elements);
public Route(List<RouteElement> elements) {
this.elements = List.copyOf(elements);
}

public Route(String routeString) {
Expand Down Expand Up @@ -115,4 +115,14 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(elements);
}

public static Route concat(Route... routes) {
Route concatted = Route.ROOT;
for (final var route : routes) {
for (final var element : route.getElements()) {
concatted = concatted.withTrailingElement(element);
}
}
return concatted;
}
}
2 changes: 0 additions & 2 deletions src/main/java/com/duncpro/jroute/route/RouteElement.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.duncpro.jroute.route;

import java.util.List;

public class RouteElement {
protected RouteElement() {}
}
31 changes: 31 additions & 0 deletions src/main/java/com/duncpro/jroute/router/PositionedEndpoint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.duncpro.jroute.router;

import com.duncpro.jroute.HttpMethod;
import com.duncpro.jroute.route.Route;

import java.util.Objects;

public class PositionedEndpoint<E> {
public final Route route;
public final HttpMethod method;
public final E endpoint;

public PositionedEndpoint(Route route, HttpMethod method, E endpoint) {
this.route = Objects.requireNonNull(route);
this.method = Objects.requireNonNull(method);
this.endpoint = Objects.requireNonNull(endpoint);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PositionedEndpoint<?> that = (PositionedEndpoint<?>) o;
return route.equals(that.route) && method == that.method && endpoint.equals(that.endpoint);
}

@Override
public int hashCode() {
return Objects.hash(route, method, endpoint);
}
}
28 changes: 27 additions & 1 deletion src/main/java/com/duncpro/jroute/router/RouteTreeNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import net.jcip.annotations.NotThreadSafe;

import java.util.*;
import java.util.stream.Stream;

@NotThreadSafe
class RouteTreeNode<E> {
Expand All @@ -19,6 +20,24 @@ protected RouteTreeNode(RouteTreeNodePosition<E> position) {
this.position = position;
}

/**
* Returns a {@link Stream} containing all endpoints which are defined at this {@link RouteTreeNode}
* as well as all {@link RouteTreeNode}s descending from the given {@link RouteTreeNode}s.
*/
static <E> Set<PositionedEndpoint<E>> getAllEndpoints(RouteTreeNode<E> root) {
final var endpoints = new HashSet<PositionedEndpoint<E>>();

root.endpoints.entrySet().stream()
.map(e -> new PositionedEndpoint<E>(root.position.getRoute(), e.getKey(), e.getValue()))
.forEach(endpoints::add);

root.children.stream()
.flatMap(child -> RouteTreeNode.getAllEndpoints(child).stream())
.forEach(endpoints::add);

return endpoints;
}

private boolean hasGreedyChild() {
return children.size() == 1
&& children.get(0).position.getRouteElement() instanceof WildcardRouteElement;
Expand Down Expand Up @@ -47,6 +66,12 @@ private void addChildRoute(RouteTreeNode<E> childNode) throws RouteConflictExcep
children.add(childNode);
}

Optional<RouteTreeNode<E>> getChildRoute(RouteElement trailingRouteElement) {
return children.stream()
.filter(child -> child.position.getRouteElement().equals(trailingRouteElement))
.findFirst();
}

RouteTreeNode<E> getOrCreateChildRoute(RouteElement trailingRouteElement) {
return children.stream()
.filter(child -> Objects.equals(child.position.getRouteElement(), trailingRouteElement))
Expand All @@ -59,8 +84,9 @@ RouteTreeNode<E> getOrCreateChildRoute(RouteElement trailingRouteElement) {
}

void addEndpoint(HttpMethod method, E endpoint) {
if (endpoint == null) throw new IllegalArgumentException();
final var prev = endpoints.putIfAbsent(method, endpoint);
if (prev != null) throw new IllegalStateException("Endpoint already bound to method: " + method.name());
if (prev != null) throw new IllegalStateException("PositionedEndpoint already bound to method: " + method.name());
}

Optional<RouterResult<E>> getEndpointAsRouterResult(HttpMethod method) {
Expand Down
22 changes: 20 additions & 2 deletions src/main/java/com/duncpro/jroute/router/Router.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
import com.duncpro.jroute.RouteConflictException;
import com.duncpro.jroute.route.Route;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public interface Router<E> {
default Optional<RouterResult<E>> route(HttpMethod method, String pathString) {
Expand All @@ -32,12 +36,26 @@ default Optional<RouterResult<E>> route(HttpMethod method, String pathString) {
* matches any and all path elements for that position.
* @param endpoint the endpoint which is responsible for handling requests made to paths following the form of this
* route. This endpoint will be returned by {@link #route(HttpMethod, String)}.
* @throws RouteConflictException if the given {@code routeString} overlaps another pre-existing route. The route
* will not be overwritten.
* @throws RouteConflictException if the given {@code routeString} overlaps another pre-existing route with the same
* {@link HttpMethod}. The route will not be overwritten.
*/
default void addRoute(HttpMethod method, String routeString, E endpoint) throws RouteConflictException {
addRoute(method, new Route(routeString), endpoint);
}

void addRoute(HttpMethod method, Route route, E endpoint) throws RouteConflictException;

/**
* Returns a set of {@link PositionedEndpoint}s which are accessible via the given {@link Route}.
* The returned collection includes the endpoints defined on the given {@link Route} as well as all endpoints
* which exists on routes that descend from the given {@link Route}.
*/
Set<PositionedEndpoint<E>> getAllEndpoints(Route prefix);

/**
* @throws RouteConflictException if the given endpoint conflicts with a pre-existing endpoint within the router.
*/
default void addRoute(PositionedEndpoint<E> positionedEndpoint) {
addRoute(positionedEndpoint.method, positionedEndpoint.route, positionedEndpoint.endpoint);
}
}
Loading

0 comments on commit 8755017

Please sign in to comment.