Skip to content

Commit

Permalink
Experiment with extracting headers in a Request
Browse files Browse the repository at this point in the history
I think the complexity of type parameters is becoming a bit unmanageable.
  • Loading branch information
noelwelsh committed Feb 13, 2024
1 parent 0c886fc commit 8bfe9fb
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 59 deletions.
100 changes: 57 additions & 43 deletions core/shared/src/main/scala/krop/route/Request.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,59 @@ import org.http4s.Request as Http4sRequest
* The type of values extracted from the URI path.
* @tparam Q
* The type of values extracted from the URI query parameters.
* @tparam H
* The type of values extracted from headers and other parts of the request.
* @tparam E
* The type of values extracted from other parts of the request (e.g. headers
* or entity).
* The type of value extracted from the entity.
* @tparam O
* The type of values that construct the entity. Used when creating a request
* that calls the Route containing this Request.
*/
final class Request[P <: Tuple, Q <: Tuple, E, O](
val method: Method,
val path: Path[P, Q],
val entity: Entity[E, O]
final case class Request[P <: Tuple, Q <: Tuple, H <: Tuple, E, O] private (
method: Method,
path: Path[P, Q],
headers: RequestHeaders[H],
entity: Entity[E, O]
) {
import Request.NormalizedAppend

//
// Combinators ---------------------------------------------------------------
//

/** Create a [[scala.String]] path suitable for embedding in HTML that links
* to the path described by this [[package.Request]]. Use this to create
* hyperlinks or form actions that call a route, without needing to hardcode
* the route in the HTML.
*
* This path will not include settings like the entity or headers that this
* [[package.Request]] may require. It is assumed this will be handled
* elsewhere.
*/
def pathTo(params: P): String =
path.pathTo(params)

/** Produces a human-readable representation of this [[package.Request]]. The
* toString method is used to output the usual programmatic representation.
*/
def describe: String =
s"${method.toString()} ${path.describe} ${entity.encoder.contentType.map(_.mediaType).getOrElse("")}"

def withEntity[E2, O2](entity: Entity[E2, O2]): Request[P, Q, H, E2, O2] =
Request(method, path, headers, entity)

def withMethod(method: Method): Request[P, Q, H, E, O] =
Request(method, path, headers, entity)

def withPath[P2 <: Tuple, Q2 <: Tuple](
path: Path[P2, Q2]
): Request[P2, Q2, H, E, O] =
Request(method, path, headers, entity)

//
// Interpreters --------------------------------------------------------------
//

/** Extract the values that this Request matches from a
* [[org.http4s.Request]], returning [[scala.None]] if the given request
* doesn't match what this is looking for.
Expand All @@ -75,35 +114,6 @@ final class Request[P <: Tuple, Q <: Tuple, E, O](
}
}

/** Create a [[scala.String]] path suitable for embedding in HTML that links
* to the path described by this [[package.Request]]. Use this to create
* hyperlinks or form actions that call a route, without needing to hardcode
* the route in the HTML.
*
* This path will not include settings like the entity or headers that this
* [[package.Request]] may require. It is assumed this will be handled
* elsewhere.
*/
def pathTo(params: P): String =
path.pathTo(params)

/** Produces a human-readable representation of this [[package.Request]]. The
* toString method is used to output the usual programmatic representation.
*/
def describe: String =
s"${method.toString()} ${path.describe} ${entity.encoder.contentType.map(_.mediaType).getOrElse("")}"

def withEntity[E2, O2](entity: Entity[E2, O2]): Request[P, Q, E2, O2] =
new Request(method, path, entity)

def withMethod(method: Method): Request[P, Q, E, O] =
new Request(method, path, entity)

def withPath[P2 <: Tuple, Q2 <: Tuple](
path: Path[P2, Q2]
): Request[P2, Q2, E, O] =
new Request(method, path, entity)

}
object Request {

Expand All @@ -117,34 +127,38 @@ object Request {

def delete[P <: Tuple, Q <: Tuple](
path: Path[P, Q]
): Request[P, Q, Unit, Unit] =
): Request[P, Q, EmptyTuple, Unit, Unit] =
Request.method(Method.DELETE, path)

def get[P <: Tuple, Q <: Tuple](path: Path[P, Q]): Request[P, Q, Unit, Unit] =
def get[P <: Tuple, Q <: Tuple](
path: Path[P, Q]
): Request[P, Q, EmptyTuple, Unit, Unit] =
Request.method(Method.GET, path)

def head[P <: Tuple, Q <: Tuple](
path: Path[P, Q]
): Request[P, Q, Unit, Unit] =
): Request[P, Q, EmptyTuple, Unit, Unit] =
Request.method(Method.HEAD, path)

def patch[P <: Tuple, Q <: Tuple](
path: Path[P, Q]
): Request[P, Q, Unit, Unit] =
): Request[P, Q, EmptyTuple, Unit, Unit] =
Request.method(Method.PATCH, path)

def post[P <: Tuple, Q <: Tuple](
path: Path[P, Q]
): Request[P, Q, Unit, Unit] =
): Request[P, Q, EmptyTuple, Unit, Unit] =
Request.method(Method.POST, path)

def put[P <: Tuple, Q <: Tuple](path: Path[P, Q]): Request[P, Q, Unit, Unit] =
def put[P <: Tuple, Q <: Tuple](
path: Path[P, Q]
): Request[P, Q, EmptyTuple, Unit, Unit] =
Request.method(Method.PUT, path)

def method[P <: Tuple, Q <: Tuple](
method: Method,
path: Path[P, Q]
): Request[P, Q, Unit, Unit] =
new Request(method, path, Entity.unit)
): Request[P, Q, EmptyTuple, Unit, Unit] =
new Request(method, path, RequestHeaders.empty, Entity.unit)

}
34 changes: 34 additions & 0 deletions core/shared/src/main/scala/krop/route/RequestHeaders.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2023 Creative Scala
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package krop.route

import org.http4s.Header

final case class RequestHeaders[H <: Tuple](headers: Vector[Header[?, ?]]) {

/** Add the given header to the headers extracted from the request. */
def withHeader[A](header: A)(using
h: Header[A, ?]
): RequestHeaders[Tuple.Append[H, A]] =
this.copy(headers = headers :+ h)
}
object RequestHeaders {
val empty: RequestHeaders[EmptyTuple] = RequestHeaders(Vector.empty)

def apply[A](header: A)(using h: Header[A, ?]): RequestHeaders[Tuple1[A]] =
empty.withHeader(header)
}
27 changes: 14 additions & 13 deletions core/shared/src/main/scala/krop/route/Route.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ import org.http4s.HttpRoutes
/** Type alias for a [[package.Route]] that has extracts no [[package.Entity]]
* from the request.
*/
type PathRoute[P <: Tuple, R] = Route[P, EmptyTuple, Unit, Unit, R]
type PathRoute[P <: Tuple, R] = Route[P, EmptyTuple, EmptyTuple, Unit, Unit, R]

/** Type alias for a [[package.Route]] that has extracts no [[package.Path]] or
* [[package.Entity]]] parameters from the request.
*/
type SimpleRoute[P, R] = Route[EmptyTuple, EmptyTuple, Unit, Unit, R]
type SimpleRoute[P, R] =
Route[EmptyTuple, EmptyTuple, EmptyTuple, Unit, Unit, R]

/** Type alias for a [[package.Route]] that has extracts no [[package.Entity]]
* from the request and extracts a single parameter from the [[package.Path]].
Expand All @@ -56,8 +57,8 @@ type Path1Route[P, R] = PathRoute[Tuple1[P], R]
* @tparam R
* The type of the parameters used to build the [[package.Response]].
*/
final class Route[P <: Tuple, Q <: Tuple, E, O, R](
val request: Request[P, Q, E, O],
final class Route[P <: Tuple, Q <: Tuple, H <: Tuple, E, O, R](
val request: Request[P, Q, H, E, O],
val response: Response[R],
val handler: Request.NormalizedAppend[Tuple.Concat[P, Q], E] => IO[R]
) {
Expand All @@ -71,7 +72,7 @@ final class Route[P <: Tuple, Q <: Tuple, E, O, R](
/** Try this Route. If it fails to match, pass control to the given
* [[package.Route]].
*/
def orElse(that: Route[?, ?, ?, ?, ?]): Routes =
def orElse(that: Route[?, ?, ?, ?, ?, ?]): Routes =
this.orElse(that.toRoutes)

/** Try this Route. If it fails to match, pass control to the
Expand Down Expand Up @@ -141,32 +142,32 @@ final class Route[P <: Tuple, Q <: Tuple, E, O, R](
object Route {
import Request.NormalizedAppend

def apply[P <: Tuple, Q <: Tuple, E, O, R](
request: Request[P, Q, E, O],
def apply[P <: Tuple, Q <: Tuple, H <: Tuple, E, O, R](
request: Request[P, Q, H, E, O],
response: Response[R]
)(using
ta: TupleApply[NormalizedAppend[Tuple.Concat[P, Q], E], R],
taIO: TupleApply[NormalizedAppend[Tuple.Concat[P, Q], E], IO[R]]
): RouteBuilder[P, Q, E, O, ta.Fun, taIO.Fun, R] =
): RouteBuilder[P, Q, H, E, O, ta.Fun, taIO.Fun, R] =
RouteBuilder(request, response, ta, taIO)

final class RouteBuilder[P <: Tuple, Q <: Tuple, E, O, F, FIO, R](
request: Request[P, Q, E, O],
final class RouteBuilder[P <: Tuple, Q <: Tuple, H <: Tuple, E, O, F, FIO, R](
request: Request[P, Q, H, E, O],
response: Response[R],
ta: TupleApply.Aux[NormalizedAppend[Tuple.Concat[P, Q], E], F, R],
taIO: TupleApply.Aux[NormalizedAppend[Tuple.Concat[P, Q], E], FIO, IO[
R
]]
) {
def handle(f: F): Route[P, Q, E, O, R] =
def handle(f: F): Route[P, Q, H, E, O, R] =
new Route(request, response, i => IO.pure(ta.tuple(f)(i)))

def handleIO[A](f: FIO): Route[P, Q, E, O, R] =
def handleIO[A](f: FIO): Route[P, Q, H, E, O, R] =
new Route(request, response, taIO.tuple(f))

def passthrough(using
pb: PassthroughBuilder[NormalizedAppend[Tuple.Concat[P, Q], E], R]
): Route[P, Q, E, O, R] =
): Route[P, Q, H, E, O, R] =
new Route(request, response, pb.build)
}
}
6 changes: 3 additions & 3 deletions core/shared/src/main/scala/krop/route/Routes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ import krop.tool.NotFound
import org.http4s.HttpRoutes

/** [[package.Routes]] are a collection of zero or more [[package.Route]]. */
final class Routes(val routes: Chain[Route[?, ?, ?, ?, ?]]) {
final class Routes(val routes: Chain[Route[?, ?, ?, ?, ?, ?]]) {

/** Create a [[package.Routes]] that tries first these routes, and if they
* fail to match, the route in the given parameter.
*/
def orElse(that: Route[?, ?, ?, ?, ?]): Routes =
def orElse(that: Route[?, ?, ?, ?, ?, ?]): Routes =
Routes(this.routes :+ that)

/** Create a [[package.Routes]] that tries first these routes, and if they
Expand Down Expand Up @@ -63,5 +63,5 @@ final class Routes(val routes: Chain[Route[?, ?, ?, ?, ?]]) {
object Routes {

/** The empty [[package.Routes]], which don't match any request. */
val empty: Routes = new Routes(Chain.empty[Route[?, ?, ?, ?, ?]])
val empty: Routes = new Routes(Chain.empty[Route[?, ?, ?, ?, ?, ?]])
}

0 comments on commit 8bfe9fb

Please sign in to comment.