Skip to content

Commit

Permalink
differentiate between error types
Browse files Browse the repository at this point in the history
  • Loading branch information
duncpro committed Aug 13, 2022
1 parent 035f1cb commit d0e32d4
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 72 deletions.
17 changes: 13 additions & 4 deletions .idea/workspace.xml

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

4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<RouterResult<Supplier<Integer>>> result = router.route(HttpMethod.GET, "/users/duncpro/age");
final Optional<Matched<Supplier<Integer>>> 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
Expand Down Expand Up @@ -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).
2 changes: 1 addition & 1 deletion 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-7"
version = "1.0-SNAPSHOT-8"

repositories {
mavenCentral()
Expand Down
13 changes: 2 additions & 11 deletions src/main/java/com/duncpro/jroute/router/RouteTreeNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,8 @@ void addEndpoint(HttpMethod method, E endpoint) {
if (prev != null) throw new IllegalStateException("PositionedEndpoint already bound to method: " + method.name());
}

Optional<RouterResult<E>> 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<E> getEndpoint(HttpMethod method) {
return Optional.ofNullable(endpoints.get(method));
}
}

20 changes: 8 additions & 12 deletions src/main/java/com/duncpro/jroute/router/Router.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<E> {
default Optional<RouterResult<E>> route(HttpMethod method, String pathString) {
default RouterResult<E> 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<RouterResult<E>> route(HttpMethod method, Path path);
RouterResult<E> route(HttpMethod method, Path path);

/**
* Assigns the given endpoint responsibility for requests made with the given {@link HttpMethod}
Expand All @@ -39,11 +35,11 @@ default Optional<RouterResult<E>> 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}.
Expand All @@ -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<E> positionedEndpoint) {
addRoute(positionedEndpoint.method, positionedEndpoint.route, positionedEndpoint.endpoint);
default void add(PositionedEndpoint<E> positionedEndpoint) {
add(positionedEndpoint.method, positionedEndpoint.route, positionedEndpoint.endpoint);
}
}
72 changes: 60 additions & 12 deletions src/main/java/com/duncpro/jroute/router/RouterResult.java
Original file line number Diff line number Diff line change
@@ -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<E> {
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<E> extends RouterResult<E> {
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<E> extends RouterResult<E> {
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<E> extends RouterResult<E> {
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<Matched<E>> asOptional() {
if (this instanceof Matched) return Optional.of((Matched<E>) this);
return Optional.empty();
}
}
9 changes: 6 additions & 3 deletions src/main/java/com/duncpro/jroute/router/TreeRouter.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ public class TreeRouter<E> implements Router<E> {
private final RouteTreeNode<E> rootRoute = new RouteTreeNode<>(RouteTreeNodePosition.root());

@Override
public Optional<RouterResult<E>> route(HttpMethod method, Path path) {
public RouterResult<E> route(HttpMethod method, Path path) {
return findNode(rootRoute, path)
.flatMap(node -> node.getEndpointAsRouterResult(method));
.map(node -> node.getEndpoint(method)
.<RouterResult<E>>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);
}
Expand Down
2 changes: 0 additions & 2 deletions src/test/java/com/duncpro/jroute/PathTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -21,7 +20,7 @@ public void example() {
final Router<BiFunction<Request, List<String>, 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;
Expand All @@ -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<BiFunction<Request, List<String>, 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<BiFunction<Request, List<String>, 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);
Expand Down
Loading

0 comments on commit d0e32d4

Please sign in to comment.