Skip to content

Commit

Permalink
Test request parsing and unparsing
Browse files Browse the repository at this point in the history
  • Loading branch information
noelwelsh committed Jul 18, 2024
1 parent eceae42 commit 2017afe
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 34 deletions.
78 changes: 78 additions & 0 deletions core/jvm/src/test/scala/krop/route/RequestEntitySuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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 krop.raise.Raise
import munit.CatsEffectSuite
import org.http4s.Method
import org.http4s.Uri
import org.http4s.implicits.*
import org.http4s.{Request as Http4sRequest}
import org.http4s.{Entity as Http4sEntity}

class RequestEntitySuite extends CatsEffectSuite {
val unitRequest = Request.get(Path.root).withEntity(Entity.unit)
val textRequest = Request.get(Path.root).withEntity(Entity.text)

test("Unit request parses empty entity") {
val request =
Http4sRequest(method = Method.GET, uri = uri"http://example.org/")

unitRequest.parse(request)(using Raise.toOption).map(opt =>
opt match {
case Some(Tuple1(())) => true
case other => fail(s"Not the expected entity: $other")
}
).assert
}

test("Unit request unparses unit") {
val request =
Http4sRequest(method = Method.GET, uri = uri"http://example.org/")

val unparsed = unitRequest.unparse(Tuple1(()))

assertEquals(unparsed.method, request.method)
assertEquals(unparsed.uri.path, request.uri.path)
assertEquals(unparsed.headers, request.headers)
assertEquals(unparsed.entity, request.entity)
}

test("Text request parses string") {
val request =
Http4sRequest(method = Method.GET, uri = uri"http://example.org/", entity = Http4sEntity.utf8String("hello"))

textRequest.parse(request)(using Raise.toOption).map(opt =>
opt match {
case Some(Tuple1("hello")) => true
case other => fail(s"Not the expected entity: $other")
}
)
}

test("Text request unparses text") {
val request =
Http4sRequest(method = Method.GET, uri = uri"http://example.org/", entity = Http4sEntity.utf8String("hello"))

val unparsed = textRequest.unparse(Tuple1("hello"))

assertEquals(unparsed.method, request.method)
assertEquals(unparsed.uri.path, request.uri.path)
assertEquals(unparsed.headers, request.headers)
assertEquals(unparsed.entity, request.entity)
}
}
51 changes: 36 additions & 15 deletions core/jvm/src/test/scala/krop/route/RequestHeaderSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,30 +33,30 @@ class RequestHeaderSuite extends CatsEffectSuite {
uri = uri"http://example.org/",
headers = Headers(jsonContentType)
)
val ensureJsonHeaderRequest = Request
.get(Path.root)
.ensureHeader(`Content-Type`(MediaType.application.json))
val extractContentTypeRequest =
Request.get(Path.root).extractHeader[`Content-Type`]
val extractContentTypeWithDefaultRequest =
Request.get(Path.root).extractHeader(jsonContentType)


test("Ensure header fails if header does not exist") {
val req = Request
.get(Path.root)
.ensureHeader(`Content-Type`(MediaType.application.json))
val request =
Http4sRequest(method = Method.GET, uri = uri"http://example.org/")

req.parse(request)(using Raise.toOption).map(_.isEmpty).assert
ensureJsonHeaderRequest.parse(request)(using Raise.toOption).map(_.isEmpty).assert
}

test("Ensure header succeeds if header does exist") {
val req = Request.get(Path.root).ensureHeader(jsonContentType)

req
ensureJsonHeaderRequest
.parse(jsonRequest)(using Raise.toOption)
.map(opt => assertEquals(opt, Some(EmptyTuple)))
}

test("Extract header extracts desired header (by type version)") {
val req: Request[?, ?, Tuple1[`Content-Type`], ?] =
Request.get(Path.root).extractHeader[`Content-Type`]

req
extractContentTypeRequest
.parse(jsonRequest)(using Raise.toOption)
.map(h =>
h match {
Expand All @@ -67,10 +67,7 @@ class RequestHeaderSuite extends CatsEffectSuite {
}

test("Extract header extracts desired header (by value version)") {
val req: Request[?, ?, Tuple1[`Content-Type`], ?] =
Request.get(Path.root).extractHeader(jsonContentType)

req
extractContentTypeWithDefaultRequest
.parse(jsonRequest)(using Raise.toOption)
.map(h =>
h match {
Expand All @@ -79,4 +76,28 @@ class RequestHeaderSuite extends CatsEffectSuite {
}
)
}

test("Ensure header unparses with expected header") {
val unparsed = ensureJsonHeaderRequest.unparse(EmptyTuple)

assertEquals(unparsed.method, jsonRequest.method)
assertEquals(unparsed.uri.path, jsonRequest.uri.path)
assertEquals(unparsed.headers, jsonRequest.headers)
}

test("Extract header unparses with expected header") {
val unparsed = extractContentTypeRequest.unparse(Tuple1(jsonContentType))

assertEquals(unparsed.method, jsonRequest.method)
assertEquals(unparsed.uri.path, jsonRequest.uri.path)
assertEquals(unparsed.headers, jsonRequest.headers)
}

test("Extract header with default unparses with expected header") {
val unparsed = extractContentTypeWithDefaultRequest.unparse(EmptyTuple)

assertEquals(unparsed.method, jsonRequest.method)
assertEquals(unparsed.uri.path, jsonRequest.uri.path)
assertEquals(unparsed.headers, jsonRequest.headers)
}
}
43 changes: 24 additions & 19 deletions core/shared/src/main/scala/krop/route/Request.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package krop.route

import cats.effect.IO
import cats.syntax.all.*
import krop.Types.TupleAppend
import krop.Types.TupleConcat
import krop.raise.Raise
import org.http4s.EntityDecoder
Expand Down Expand Up @@ -49,6 +48,16 @@ import org.http4s.Request as Http4sRequest
*/
sealed abstract class Request[P <: Tuple, Q <: Tuple, I <: Tuple, O <: Tuple] {

/** A type alias for the type of values this Request produces when it
* successfully parses an incoming request.
*/
type Parsed = I

/** A type alias for the type of values this Request requires to construct an
* incoming request.
*/
type Unparsed = O

//
// Interpreters --------------------------------------------------------------
//
Expand Down Expand Up @@ -157,14 +166,12 @@ object Request {
TupleConcat[P, Q]
] {

type Result = TupleConcat[P, Q]

export path.pathTo
export path.pathAndQueryTo

def parse[F[_, _]: Raise.Handler](
req: Http4sRequest[IO]
): IO[F[ParseFailure, Result]] = {
): IO[F[ParseFailure, Parsed]] = {
IO.pure(
Raise.handle(
if req.method != method
Expand All @@ -181,7 +188,7 @@ object Request {
)
}

def unparse(params: Result): Http4sRequest[IO] = {
def unparse(params: Unparsed): Http4sRequest[IO] = {
val uri = path.unparse(params)
Http4sRequest(uri = uri)
}
Expand Down Expand Up @@ -267,8 +274,6 @@ object Request {
TupleConcat[TupleConcat[P, Q], I],
TupleConcat[TupleConcat[P, Q], O]
] {
type Result = TupleConcat[TupleConcat[P, Q], I]

import RequestHeaders.Process
import RequestHeaders.failure

Expand All @@ -277,7 +282,7 @@ object Request {

def parse[F[_, _]: Raise.Handler](
req: Http4sRequest[IO]
): IO[F[ParseFailure, Result]] = {
): IO[F[ParseFailure, Parsed]] = {
val ioPQ: IO[F[ParseFailure, TupleConcat[P, Q]]] = path.parse(req)

extension [A](opt: Option[A]) {
Expand Down Expand Up @@ -320,16 +325,14 @@ object Request {
Raise.mapToIO(fPQ)(pq =>
IO.pure(
(pq ++ Tuple.fromArray(e.toArray).asInstanceOf[I])
.asInstanceOf[Result]
.asInstanceOf[Parsed]
)
)
)
}
}

def unparse(
params: TupleConcat[TupleConcat[P, Q], O]
): Http4sRequest[IO] = {
def unparse(params: Unparsed): Http4sRequest[IO] = {
val ps = params.toIArray
val (pqArr, oArr) = ps.splitAt(ps.length - outputCount)

Expand Down Expand Up @@ -453,7 +456,7 @@ object Request {
](
headers: RequestHeaders[P, Q, ?, ?],
entity: Entity[D, E]
) extends Request[P, Q, TupleAppend[I, D], TupleAppend[O, E]] {
) extends Request[P, Q, Tuple.Append[I, D], Tuple.Append[O, E]] {

def pathTo(params: P): String =
headers.pathTo(params)
Expand All @@ -466,17 +469,14 @@ object Request {

def parse[F[_, _]: Raise.Handler](
req: Http4sRequest[IO]
): IO[F[ParseFailure, TupleAppend[I, D]]] = {
): IO[F[ParseFailure, Parsed]] = {
headers.parse(req).flatMap { result =>
Raise.flatMapToIO(result) { i =>
given EntityDecoder[IO, D] = entity.decoder
req
.as[D]
.map { d =>
Raise.succeed((d match {
case () => i
case other => i :* other
}).asInstanceOf[TupleAppend[I, D]])
Raise.succeed((i :* d).asInstanceOf[Parsed])
}
.handleErrorWith(err =>
IO(
Expand All @@ -495,7 +495,12 @@ object Request {
}
}

def unparse(params: TupleAppend[O, E]): Http4sRequest[IO] = ???
def unparse(params: Unparsed): Http4sRequest[IO] = {
val (o, e) = params.splitAt(headers.outputCount)
val request = headers.unparse(o.asInstanceOf[headers.Unparsed])
val encoded = entity.encoder.toEntity(e.asInstanceOf[Tuple1[E]](0))
request.withEntity(encoded)
}

def withEntity[D2, E2](
entity: Entity[D2, E2]
Expand Down

0 comments on commit 2017afe

Please sign in to comment.