diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 5fd9971..d4738e7 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -6,8 +6,15 @@
+
-
+
+
+
+
+
+
+
@@ -101,7 +108,7 @@
-
+
@@ -211,11 +218,11 @@
+
-
@@ -226,7 +233,9 @@
1660360738130
-
+
+
+
diff --git a/README.md b/README.md
index 67bfceb..1aa784d 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ The third parameter of `addRoute` is the endpoint. For the sake of brevity we wi
number for each request instead of looking up the user's actual age in a database.
### Process Inbound Requests
```java
-final Optional>> result = router.route(HttpMethod.GET, "/users/duncpro/age");
+final Optional>> result = router.route(HttpMethod.GET, "/users/duncpro/age").asOptional();
```
An empty optional is returned if no route matches the given path. In this case there is obviously a match since
we just registered a route for this path above. In practice you should present a 404 page if a path
@@ -74,5 +74,7 @@ assertEquals(Map.of("customerId", "duncan", "orderId", "abc123"), pathArguments)
assertEquals(Route("/customers/*/orders/*"), route);
```
+
+
## More Docs
There is a Javadoc for this library [here](https://duncpro.github.io/JRoute).
diff --git a/build.gradle.kts b/build.gradle.kts
index 348c2cb..09c30c6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -5,7 +5,7 @@ plugins {
}
group = "com.duncpro"
-version = "1.0-SNAPSHOT-7"
+version = "1.0-SNAPSHOT-8"
repositories {
mavenCentral()
diff --git a/src/main/java/com/duncpro/jroute/router/RouteTreeNode.java b/src/main/java/com/duncpro/jroute/router/RouteTreeNode.java
index bcf0f91..a7dd23b 100644
--- a/src/main/java/com/duncpro/jroute/router/RouteTreeNode.java
+++ b/src/main/java/com/duncpro/jroute/router/RouteTreeNode.java
@@ -89,17 +89,8 @@ void addEndpoint(HttpMethod method, E endpoint) {
if (prev != null) throw new IllegalStateException("PositionedEndpoint already bound to method: " + method.name());
}
- Optional> getEndpointAsRouterResult(HttpMethod method) {
- final var endpoint = endpoints.get(method);
-
- if (endpoint == null) return Optional.empty();
-
- final var result = new RouterResult<>(
- endpoint,
- position.getRoute()
- );
-
- return Optional.of(result);
+ Optional getEndpoint(HttpMethod method) {
+ return Optional.ofNullable(endpoints.get(method));
}
}
diff --git a/src/main/java/com/duncpro/jroute/router/Router.java b/src/main/java/com/duncpro/jroute/router/Router.java
index 1b78ec7..88d81bc 100644
--- a/src/main/java/com/duncpro/jroute/router/Router.java
+++ b/src/main/java/com/duncpro/jroute/router/Router.java
@@ -5,24 +5,20 @@
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 {
- default Optional> route(HttpMethod method, String pathString) {
+ default RouterResult route(HttpMethod method, String pathString) {
return route(method, new Path(pathString));
}
/**
* Finds the {@link Route} and endpoint ({@link E}) which has been assigned responsibility for requests made to the
* given {@link Path} using the given {@link HttpMethod}. Routes must be registered with the router in advance using
- * {@link #addRoute(HttpMethod, String, Object)} or similar. If no route exists for the given method and path then
+ * {@link #add(HttpMethod, String, Object)} or similar. If no route exists for the given method and path then
* an empty optional is returned instead.
*/
- Optional> route(HttpMethod method, Path path);
+ RouterResult route(HttpMethod method, Path path);
/**
* Assigns the given endpoint responsibility for requests made with the given {@link HttpMethod}
@@ -39,11 +35,11 @@ default Optional> route(HttpMethod method, String pathString) {
* @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);
+ default void add(HttpMethod method, String routeString, E endpoint) throws RouteConflictException {
+ add(method, new Route(routeString), endpoint);
}
- void addRoute(HttpMethod method, Route route, E endpoint) throws RouteConflictException;
+ void add(HttpMethod method, Route route, E endpoint) throws RouteConflictException;
/**
* Returns a set of {@link PositionedEndpoint}s which are accessible via the given {@link Route}.
@@ -55,7 +51,7 @@ default void addRoute(HttpMethod method, String routeString, E endpoint) throws
/**
* @throws RouteConflictException if the given endpoint conflicts with a pre-existing endpoint within the router.
*/
- default void addRoute(PositionedEndpoint positionedEndpoint) {
- addRoute(positionedEndpoint.method, positionedEndpoint.route, positionedEndpoint.endpoint);
+ default void add(PositionedEndpoint positionedEndpoint) {
+ add(positionedEndpoint.method, positionedEndpoint.route, positionedEndpoint.endpoint);
}
}
diff --git a/src/main/java/com/duncpro/jroute/router/RouterResult.java b/src/main/java/com/duncpro/jroute/router/RouterResult.java
index 0479b04..052d251 100644
--- a/src/main/java/com/duncpro/jroute/router/RouterResult.java
+++ b/src/main/java/com/duncpro/jroute/router/RouterResult.java
@@ -1,27 +1,75 @@
package com.duncpro.jroute.router;
+import com.duncpro.jroute.HttpMethod;
import com.duncpro.jroute.route.Route;
import net.jcip.annotations.Immutable;
-@Immutable
+import java.util.Optional;
+
+/**
+ * There are three district types of {@link RouterResult}.
+ * 1. {@link Matched}, representing a successful match of a request to an endpoint.
+ * 2. {@link ResourceNotFound}, representing a failure to match the path to a resource.
+ * 3. {@link MethodNotAllowed}, representing a successful match of a request to a resource but a failure
+ * to match and the request method (http verb) to an endpoint on the resource.
+ */
+@SuppressWarnings("unused") // The type parameter is used! Quit complaining IntelliJ.
public class RouterResult {
- private final E endpoint;
- private final Route route;
+ /**
+ * {@link RouterResult} which indicates a failure to match the given path with any registered route.
+ * This class represents an HTTP 404 error. See also {@link MethodNotAllowed}, which represents the case
+ * where the route exists but the method does not.
+ */
+ public static final class ResourceNotFound extends RouterResult {
+ ResourceNotFound() {}
+ }
- public RouterResult(E endpoint, Route route) {
- this.endpoint = endpoint;
- this.route = route;
+ /**
+ * {@link RouterResult} representing the case where the given path matched a resource within the
+ * {@link Router}, but the given {@link HttpMethod} is not defined for the resource.
+ * This result is associated with HTTP Status Code 405.
+ */
+ public static final class MethodNotAllowed extends RouterResult {
+ MethodNotAllowed() {}
}
- public E getEndpoint() {
- return endpoint;
+ /**
+ * {@link RouterResult} representing the case where the given patch matched a resource within the
+ * {@link Router}, and the given {@link HttpMethod} is defined on that resource.
+ */
+ @Immutable
+ public static final class Matched extends RouterResult {
+ private final E endpoint;
+ private final Route route;
+
+ Matched(E endpoint, Route route) {
+ this.endpoint = endpoint;
+ this.route = route;
+ }
+
+ public E getEndpoint() {
+ return endpoint;
+ }
+
+ /**
+ * Returns the route matching the path. The returned {@link Route} object can be used to extract path arguments
+ * for route parameters. See {@link Route#extractVariables(String)}.
+ */
+ public Route getRoute() {
+ return route;
+ }
}
+ private RouterResult() {}
+
/**
- * Returns the route matching the path. The returned {@link Route} object can be used to extract path arguments
- * for route parameters. See {@link Route#extractVariables(String)}.
+ * If this {@link RouterResult} is a successful matched (i.e. instanceof {@link Matched}),
+ * then this method returns an optional containing this object but cast to {@link Matched}.
+ * If this {@link RouterResult} is an error type, such as {@link ResourceNotFound} or {@link MethodNotAllowed},
+ * then this method returns an empty optional.
*/
- public Route getRoute() {
- return route;
+ public Optional> asOptional() {
+ if (this instanceof Matched) return Optional.of((Matched) this);
+ return Optional.empty();
}
}
diff --git a/src/main/java/com/duncpro/jroute/router/TreeRouter.java b/src/main/java/com/duncpro/jroute/router/TreeRouter.java
index 1a70f66..0b0f365 100644
--- a/src/main/java/com/duncpro/jroute/router/TreeRouter.java
+++ b/src/main/java/com/duncpro/jroute/router/TreeRouter.java
@@ -14,13 +14,16 @@ public class TreeRouter implements Router {
private final RouteTreeNode rootRoute = new RouteTreeNode<>(RouteTreeNodePosition.root());
@Override
- public Optional> route(HttpMethod method, Path path) {
+ public RouterResult route(HttpMethod method, Path path) {
return findNode(rootRoute, path)
- .flatMap(node -> node.getEndpointAsRouterResult(method));
+ .map(node -> node.getEndpoint(method)
+ .>map(endpoint -> new RouterResult.Matched<>(endpoint, node.position.getRoute()))
+ .orElse(new RouterResult.MethodNotAllowed<>()))
+ .orElse(new RouterResult.ResourceNotFound<>());
}
@Override
- public void addRoute(HttpMethod method, Route route, E endpoint) throws RouteConflictException {
+ public void add(HttpMethod method, Route route, E endpoint) throws RouteConflictException {
final var node = findOrCreateNode(rootRoute, route);
node.addEndpoint(method, endpoint);
}
diff --git a/src/test/java/com/duncpro/jroute/PathTest.java b/src/test/java/com/duncpro/jroute/PathTest.java
index 3042e9d..c53419a 100644
--- a/src/test/java/com/duncpro/jroute/PathTest.java
+++ b/src/test/java/com/duncpro/jroute/PathTest.java
@@ -1,7 +1,5 @@
package com.duncpro.jroute;
-import com.duncpro.jroute.route.Route;
-import com.duncpro.jroute.route.StaticRouteElement;
import org.junit.jupiter.api.Test;
import java.util.List;
diff --git a/src/test/java/com/duncpro/jroute/ExampleUsage.java b/src/test/java/com/duncpro/jroute/SmokeTest.java
similarity index 53%
rename from src/test/java/com/duncpro/jroute/ExampleUsage.java
rename to src/test/java/com/duncpro/jroute/SmokeTest.java
index 7f2229d..5553c6d 100644
--- a/src/test/java/com/duncpro/jroute/ExampleUsage.java
+++ b/src/test/java/com/duncpro/jroute/SmokeTest.java
@@ -2,16 +2,15 @@
import com.duncpro.jroute.router.Router;
import com.duncpro.jroute.router.TreeRouter;
+import com.duncpro.jroute.router.RouterResult;
import org.junit.jupiter.api.Test;
import java.util.List;
-import java.util.function.BiConsumer;
import java.util.function.BiFunction;
-import java.util.function.Consumer;
import static org.junit.jupiter.api.Assertions.assertEquals;
-public class ExampleUsage {
+public class SmokeTest {
private static class Request {
String path;
}
@@ -21,7 +20,7 @@ public void example() {
final Router, Integer>> router = new TreeRouter<>();
// On application startup, register HTTP request handlers with the router.
- router.addRoute(HttpMethod.GET, "/calculator/add/*/*", (request, pathArgs) -> {
+ router.add(HttpMethod.GET, "/calculator/add/*/*", (request, pathArgs) -> {
final var x = Integer.parseInt(pathArgs.get(0));
final var y = Integer.parseInt(pathArgs.get(1));
return x + y;
@@ -32,14 +31,20 @@ public void example() {
fakeIncomingRequest.path = "/calculator/add/6/4";
// Resolve the HTTP request handler based on the path in the URL.
- final var routerResult = router.route(HttpMethod.GET, fakeIncomingRequest.path)
- .orElseThrow();
+ final RouterResult, Integer>> routerResult = router
+ .route(HttpMethod.GET, fakeIncomingRequest.path);
- // Extract the variable path elements so they may be used by the request handler.
- final var pathArgs = routerResult.getRoute().extractVariables(fakeIncomingRequest.path);
+ if (routerResult instanceof RouterResult.MethodNotAllowed) throw new AssertionError();
+ if (routerResult instanceof RouterResult.ResourceNotFound) throw new AssertionError();
+ if (!(routerResult instanceof RouterResult.Matched)) throw new AssertionError();
+
+ final var match = (RouterResult.Matched, Integer>>) routerResult;
+
+ // Extract the variable path elements, so they may be used by the request handler.
+ final var pathArgs = match.getRoute().extractVariables(fakeIncomingRequest.path);
// Invoke the request handler and pass in the variables.
- final var response = routerResult.getEndpoint().apply(fakeIncomingRequest, pathArgs);
+ final var response = match.getEndpoint().apply(fakeIncomingRequest, pathArgs);
// Make sure our calculator endpoint is working properly
assertEquals(10, response);
diff --git a/src/test/java/com/duncpro/jroute/router/TreeRouterTest.java b/src/test/java/com/duncpro/jroute/router/TreeRouterTest.java
index 12c4d77..464525e 100644
--- a/src/test/java/com/duncpro/jroute/router/TreeRouterTest.java
+++ b/src/test/java/com/duncpro/jroute/router/TreeRouterTest.java
@@ -6,8 +6,6 @@
import com.duncpro.jroute.route.StaticRouteElement;
import org.junit.jupiter.api.Test;
-import java.util.Set;
-
import static org.junit.jupiter.api.Assertions.*;
class TreeRouterTest {
@@ -23,9 +21,8 @@ void createAndLookupRoute() {
final var actualEndpoint = tree
.getOrCreateChildRoute(new StaticRouteElement("hello"))
.getOrCreateChildRoute(new StaticRouteElement("world"))
- .getEndpointAsRouterResult(HttpMethod.GET)
- .orElseThrow()
- .getEndpoint();
+ .getEndpoint(HttpMethod.GET)
+ .orElseThrow();
assertEquals(expectedEndpoint, actualEndpoint);
}
@@ -36,9 +33,10 @@ void resolveNormalPath() {
final var expectedEndpoint = 1;
- router.addRoute(HttpMethod.GET, "/hello/world", expectedEndpoint);
+ router.add(HttpMethod.GET, "/hello/world", expectedEndpoint);
final var actualEndpoint = router.route(HttpMethod.GET, "/hello/world")
+ .asOptional()
.orElseThrow()
.getEndpoint();
@@ -51,9 +49,10 @@ void resolveWildcardPath() {
final var expectedEndpoint = 10;
- router.addRoute(HttpMethod.GET, "/users/*/pets/*/age", expectedEndpoint);
+ router.add(HttpMethod.GET, "/users/*/pets/*/age", expectedEndpoint);
final var actualEndpoint = router.route(HttpMethod.GET, "/users/duncan/pets/cocoa/age")
+ .asOptional()
.orElseThrow()
.getEndpoint();
@@ -64,30 +63,40 @@ void resolveWildcardPath() {
void unresolvablePath() {
final var router = new TreeRouter<>();
- router.addRoute(HttpMethod.GET, "/users", 0);
+ router.add(HttpMethod.GET, "/users", 0);
+
+ final var result = router.route(HttpMethod.POST, "/settings");
+
+ assertTrue(result instanceof RouterResult.ResourceNotFound);
+ }
+
+ @Test
+ void unresolvableMethod() {
+ final var router = new TreeRouter<>();
- final var endpoint = router.route(HttpMethod.POST, "/settings");
+ router.add(HttpMethod.GET, "/users", 0);
- assertTrue(endpoint.isEmpty());
+ final var result = router.route(HttpMethod.POST, "/users");
+ assertTrue(result instanceof RouterResult.MethodNotAllowed);
}
@Test
void conflictWildcardAfterStaticRoute() {
final var router = new TreeRouter<>();
- router.addRoute(HttpMethod.GET, "/users/duncan", new Object());
+ router.add(HttpMethod.GET, "/users/duncan", new Object());
assertThrows(RouteConflictException.class, () -> {
- router.addRoute(HttpMethod.GET, "/users/*", new Object());
+ router.add(HttpMethod.GET, "/users/*", new Object());
});
}
@Test
void conflictStaticRouteAfterWildcard() {
final var router = new TreeRouter<>();
- router.addRoute(HttpMethod.GET, "/users/*", new Object());
+ router.add(HttpMethod.GET, "/users/*", new Object());
assertThrows(RouteConflictException.class, () -> {
- router.addRoute(HttpMethod.GET, "/users/duncan", new Object());
+ router.add(HttpMethod.GET, "/users/duncan", new Object());
});
}
@@ -96,9 +105,10 @@ void routeResultHasCorrectRoute() {
final var router = new TreeRouter<>();
final var expected = new Route("/users/*/devices");
- router.addRoute(HttpMethod.GET, expected.toString(), new Object());
+ router.add(HttpMethod.GET, expected.toString(), new Object());
final var actual = router.route(HttpMethod.GET, "/users/duncan/devices")
+ .asOptional()
.orElseThrow()
.getRoute();
@@ -112,8 +122,8 @@ void getAllEndpoints() {
final var expectedEndpoint = new PositionedEndpoint<>(new Route("/hello"), HttpMethod.GET, 1);
final var expectedNestedEndpoint = new PositionedEndpoint<>(new Route("/hello/world"), HttpMethod.GET, 2);
- router.addRoute(expectedEndpoint);
- router.addRoute(expectedNestedEndpoint);
+ router.add(expectedEndpoint);
+ router.add(expectedNestedEndpoint);
{
final var resolvedEndpoints = router.getAllEndpoints(new Route("/hello/world"));