Skip to content

Commit

Permalink
remove Module and replace with Router
Browse files Browse the repository at this point in the history
  • Loading branch information
daviddenton committed May 30, 2017
1 parent fff5143 commit 4195fe1
Show file tree
Hide file tree
Showing 10 changed files with 64 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,19 @@ import org.http4k.core.then
import org.http4k.core.with
import org.http4k.filter.ServerFilters
import org.http4k.lens.Header.X_URI_TEMPLATE
import org.http4k.routing.Module
import org.http4k.routing.Router

class RouteModule private constructor(private val router: ModuleRouter) : Module {
class ContractRouter private constructor(private val router: ModuleRouter) : Router {
override fun match(request: Request): HttpHandler? = router.match(request)

constructor(moduleRoot: BasePath, renderer: ModuleRenderer = NoRenderer, filter: Filter = Filter { it })
: this(ModuleRouter(moduleRoot, renderer, ServerFilters.CatchLensFailure.then(filter)))

override fun toRouter(): Router = router

fun securedBy(new: Security) = RouteModule(router.securedBy(new))
fun withDescriptionPath(fn: (BasePath) -> BasePath) = RouteModule(router.copy(descriptionPath = fn))
fun securedBy(new: Security) = ContractRouter(router.securedBy(new))
fun withDescriptionPath(fn: (BasePath) -> BasePath) = ContractRouter(router.copy(descriptionPath = fn))
fun withRoute(new: ServerRoute) = withRoutes(new)
fun withRoutes(vararg new: ServerRoute) = withRoutes(new.toList())
fun withRoutes(new: Iterable<ServerRoute>) = RouteModule(router.withRoutes(new.toList()))
fun withRoutes(new: Iterable<ServerRoute>) = ContractRouter(router.withRoutes(new.toList()))

companion object {
private data class ModuleRouter(val moduleRoot: BasePath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,16 @@ import org.http4k.core.ResourceLoader
import org.http4k.core.StaticContent
import org.http4k.core.Status.Companion.NOT_FOUND
import org.http4k.core.then
import org.http4k.routing.Module
import org.http4k.routing.Router

class StaticModule(basePath: BasePath,
class StaticRouter(basePath: BasePath,
resourceLoader: ResourceLoader = ResourceLoader.Classpath("/"),
moduleFilter: Filter = Filter { it },
vararg extraPairs: Pair<String, ContentType>) : Module {
filter: Filter = Filter { it },
vararg extraPairs: Pair<String, ContentType>) : Router {

private val staticContent = moduleFilter.then(StaticContent(basePath.toString(), resourceLoader, *extraPairs))
private val staticContent = filter.then(StaticContent(basePath.toString(), resourceLoader, *extraPairs))

override fun match(request: Request): HttpHandler? =
staticContent(request).let { if (it.status != NOT_FOUND) { _: Request -> it } else null }

override fun toRouter(): Router = object : Router {
override fun match(request: Request): HttpHandler? =
staticContent(request).let { if (it.status != NOT_FOUND) { _: Request -> it } else null }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,23 @@ import org.http4k.lens.Path
import org.http4k.lens.Query
import org.junit.Test

class RouteModuleTest {
class ContractRouterTest {

private val header = Header.optional("FILTER")
private val routeModule = RouteModule(Root, SimpleJson(Argo), Filter {
private val contractRouter = ContractRouter(Root, SimpleJson(Argo), Filter {
next -> { next(it.with(header of "true")) }
})

@Test
fun `by default the description lives at the route`() {
val response = routeModule.toHttpHandler()(Request(Method.GET, ""))
val response = contractRouter.toHttpHandler()(Request(Method.GET, ""))
assertThat(response.status, equalTo(OK))
assertThat(response.bodyString(), equalTo("""{"resources":{}}"""))
}

@Test
fun `passes through module filter`() {
val response = routeModule.withRoute(Route("").at(GET) bind {
val response = contractRouter.withRoute(Route("").at(GET) bind {
Response(OK).with(header of header(it))
}).toHttpHandler()(Request(Method.GET, ""))

Expand All @@ -44,7 +44,7 @@ class RouteModuleTest {
@Test
fun `identifies called route using identity header on request`() {

val response = routeModule.withRoute(Route("").at(GET) / Path.fixed("hello") / Path.of("world") bind {
val response = contractRouter.withRoute(Route("").at(GET) / Path.fixed("hello") / Path.of("world") bind {
_, _ ->
{
Response(OK).with(X_URI_TEMPLATE of X_URI_TEMPLATE(it))
Expand All @@ -57,7 +57,7 @@ class RouteModuleTest {

@Test
fun `applies security and responds with a 401 to unauthorized requests`() {
val response = routeModule
val response = contractRouter
.securedBy(ApiKey(Query.required("key"), { it == "bob" }))
.withRoute(Route().at(GET) / "bob" bind { Response(OK)})
.toHttpHandler()(Request(Method.GET, "/bob?key=sue"))
Expand All @@ -66,7 +66,7 @@ class RouteModuleTest {

@Test
fun `applies security and responds with a 200 to authorized requests`() {
val response = routeModule
val response = contractRouter
.securedBy(ApiKey(Query.required("key"), { it == "bob" }))
.withRoute(Route().at(GET) / "bob" bind { Response(OK)})
.toHttpHandler()(Request(Method.GET, "/bob?key=bob"))
Expand All @@ -75,7 +75,7 @@ class RouteModuleTest {

@Test
fun `can change path to description route`() {
val response = routeModule
val response = contractRouter
.withDescriptionPath { it / "docs" / "swagger.json" }
.toHttpHandler()(Request(Method.GET, "/docs/swagger.json"))
assertThat(response.status, equalTo(OK))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ abstract class ModuleRendererContract(private val renderer: ModuleRenderer) {
fun `renders as expected`() {
val customBody = Body.json("the body of the message").toLens()

val module = RouteModule(Root / "basepath", renderer)
val module = ContractRouter(Root / "basepath", renderer)
.securedBy(ApiKey(Query.required("the_api_key"), { true }))
.withRoute(
Route("summary of this route", "some rambling description of what this thing actually does")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import org.http4k.core.Uri.Companion.of
import org.http4k.lens.Header.Common.CONTENT_TYPE
import org.junit.Test

class StaticModuleTest {
class StaticRouterTest {

private val pkg = this::class.java.`package`.name.replace('.', '/')

@Test
fun `looks up contents of existing root file`() {
val router = StaticModule(Root / "svc").toRouter()
val router = StaticRouter(Root / "svc")
val request = Request(GET, of("/svc/mybob.xml"))
val result = router.match(request)!!(request)
assertThat(result.bodyString(), equalTo("<xml>content</xml>"))
Expand All @@ -29,7 +29,7 @@ class StaticModuleTest {

@Test
fun `can register custom mime types`() {
val router = StaticModule(Root / "svc", Classpath(), extraPairs = "myxml" to APPLICATION_XML).toRouter()
val router = StaticRouter(Root / "svc", Classpath(), extraPairs = "myxml" to APPLICATION_XML)
val request = Request(GET, of("/svc/mybob.myxml"))
val result = router.match(request)!!(request)
assertThat(result.status, equalTo(Status.OK))
Expand All @@ -39,12 +39,12 @@ class StaticModuleTest {

@Test
fun `looks up non existent-file`() {
assertThat(StaticModule(Root / "svc", Classpath()).toRouter().match(Request(GET, of("/svc/NotHere.xml"))), absent())
assertThat(StaticRouter(Root / "svc", Classpath()).match(Request(GET, of("/svc/NotHere.xml"))), absent())
}

@Test
fun `can add a filter`() {
val handler = StaticModule(Root / "svc", Classpath(pkg), Filter.Companion {
val handler = StaticRouter(Root / "svc", Classpath(pkg), Filter.Companion {
next ->
{ next(it).header("foo", "bar") }
}).toHttpHandler()
Expand Down
33 changes: 0 additions & 33 deletions http4k-core/src/main/kotlin/org/http4k/routing/Module.kt

This file was deleted.

19 changes: 19 additions & 0 deletions http4k-core/src/main/kotlin/org/http4k/routing/Router.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,26 @@ package org.http4k.routing

import org.http4k.core.HttpHandler
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.lens.LensFailure

interface Router {
fun match(request: Request): HttpHandler?

fun then(that: Router): Router {
val originalMatch = this::match
return object : Router {
override fun match(request: Request): HttpHandler? = originalMatch(request) ?: that.match(request)
}
}

fun toHttpHandler(): HttpHandler =
{ req ->
try {
match(req)?.invoke(req) ?: Response(Status.NOT_FOUND)
} catch (e: LensFailure) {
Response(Status.BAD_REQUEST)
}
}
}
8 changes: 4 additions & 4 deletions http4k-core/src/main/kotlin/org/http4k/routing/Routing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ data class Route(val method: Method, val template: UriTemplate, val handler: Htt

fun routes(vararg routes: Route): RoutingHttpHandler = RoutingHttpHandler(routes.asList())

fun routes(module: Module, vararg then: Module): HttpHandler = then.fold(module) { memo, next -> memo.then(next) }.toHttpHandler()
fun routes(module: Router, vararg then: Router): HttpHandler = then.fold(module) { memo, next -> memo.then(next) }.toHttpHandler()

fun Request.path(name: String): String? = uriTemplate().extract(uri.toString())[name]

infix fun Pair<Method, String>.by(action: HttpHandler): Route = Route(first, from(second), action)

infix fun String.by(router: RoutingHttpHandler): Module = object : Module {
override fun toRouter(): Router = RoutingHttpHandler(router.routes.map { it.copy(template = it.template.prefixedWith(this@by)) })
}
infix fun String.by(router: RoutingHttpHandler): Router = RoutingHttpHandler(
router.routes.map { it.copy(template = it.template.prefixedWith(this@by)) })


data class RoutingHttpHandler(internal val routes: List<Route>, internal val filter: Filter? = null) : Router, HttpHandler {
private val routers = routes.map(Route::asRouter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,43 +13,37 @@ import org.http4k.core.Uri.Companion.of
import org.http4k.lens.LensFailure
import org.junit.Test

class ModuleTest {
class RouterTest {

private val notFoundModule = object : Module {
override fun toRouter(): Router = object : Router {
override fun match(request: Request): HttpHandler? = null
}
private val notFoundRouter = object : Router {
override fun match(request: Request): HttpHandler? = null
}

private val lensFailureModule = object : Module {
override fun toRouter(): Router = object : Router {
override fun match(request: Request): HttpHandler? = throw LensFailure()
}
private val lensFailureRouter = object : Router {
override fun match(request: Request): HttpHandler? = throw LensFailure()
}
private val okModule = object : Module {
override fun toRouter(): Router = object : Router {
private val okRouter = object : Router {
override fun match(request: Request): HttpHandler? = { Response(OK) }
}
}

@Test
fun `can convert module to handler and call it`() {
assertThat(okModule.toHttpHandler()(Request(GET, of("/boo"))), equalTo(Response(OK)))
fun `can convert router to handler and call it`() {
assertThat(okRouter.toHttpHandler()(Request(GET, of("/boo"))), equalTo(Response(OK)))
}

@Test
fun `falls back to 404 response`() {
assertThat(notFoundModule.toHttpHandler()(Request(GET, of("/boo"))), equalTo(Response(NOT_FOUND)))
assertThat(notFoundRouter.toHttpHandler()(Request(GET, of("/boo"))), equalTo(Response(NOT_FOUND)))
}

@Test
fun `lens failure results in 400`() {
assertThat(lensFailureModule.toHttpHandler()(Request(GET, of("/boo"))), equalTo(Response(BAD_REQUEST)))
assertThat(lensFailureRouter.toHttpHandler()(Request(GET, of("/boo"))), equalTo(Response(BAD_REQUEST)))
}

@Test
fun `can combine modules and call them as a handler`() {
assertThat(notFoundModule.then(okModule).toHttpHandler()(Request(GET, of("/boo"))), equalTo(Response(OK)))
fun `can combine routers and call them as a handler`() {
assertThat(notFoundRouter.then(okRouter).toHttpHandler()(Request(GET, of("/boo"))), equalTo(Response(OK)))
}

}
6 changes: 3 additions & 3 deletions src/test/kotlin/cookbook/typesafe_http_contracts.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package cookbook

import org.http4k.contract.ApiInfo
import org.http4k.contract.ApiKey
import org.http4k.contract.ContractRouter
import org.http4k.contract.Root
import org.http4k.contract.Route
import org.http4k.contract.RouteModule
import org.http4k.contract.Swagger
import org.http4k.core.Body
import org.http4k.core.ContentType.Companion.TEXT_PLAIN
Expand Down Expand Up @@ -48,7 +48,7 @@ fun main(args: Array<String>) {
)
}

val handler = RouteModule(Root / "context", Swagger(ApiInfo("my great api", "v1.0"), Argo), ResponseFilters.ReportRouteLatency(Clock.systemUTC(), {
val handler = ContractRouter(Root / "context", Swagger(ApiInfo("my great api", "v1.0"), Argo), ResponseFilters.ReportRouteLatency(Clock.systemUTC(), {
name, latency ->
println(name + " took " + latency)
}))
Expand All @@ -63,4 +63,4 @@ fun main(args: Array<String>) {

// Adding 2 numbers: curl -v "http://localhost:8000/context/add/123/564?apiKey=42"
// API Key enforcement: curl -v "http://localhost:8000/context/add/123/564?apiKey=444"
// Swagger documentation: curl -v "http://localhost:8000/context"
// Swagger documentation: curl -v "http://localhost:8000/context/swagger.json"

0 comments on commit 4195fe1

Please sign in to comment.