From 49a4e5200e1571e80ed7dd2932e8526787e620df Mon Sep 17 00:00:00 2001 From: "John A. De Goes" Date: Wed, 19 Jan 2022 17:45:03 +0000 Subject: [PATCH] ZIO 2: Latest changes from main (#858) * Doc: Add outline (#815) * doc: add categories * refactor: doc structure refactored * fix: package-lock.json removed * refactor: create outline sub-directories * refactor: rename test to testing * Docs: Update Basic Examples (#814) * doc(Getting started): updated examples * docs: updated basic examples * docs: update advanced examples (#816) * maintenance: semanticdb revision usage (#832) * maintenance: workflow scala version (#833) * Fix: HasHeader bug (#835) * #834 - fix and test for the bug. * applied suggested change * refactor: fix naming for Http operators (#839) * Update sbt-bloop to 1.4.12 (#810) * Update sbt-bloop to 1.4.12 * Update sbt-bloop to 1.4.12 * Update scala-library to 2.13.8 (#801) * Update scala-library to 2.13.8 * Regenerate workflow with sbt-github-actions * Add configuration builder methods to zhttp.service.Server (#768) * Add configuration builder methods to zhttp.service.Server * Update zio-http/src/main/scala/zhttp/service/Server.scala Co-authored-by: Tushar Mathur * Update zio-http/src/main/scala/zhttp/service/Server.scala Co-authored-by: Tushar Mathur * Update zio-http/src/main/scala/zhttp/service/Server.scala Co-authored-by: Tushar Mathur * Change some server with* builder methods (enable parameter) * Server withAcceptContinue(enabled) Co-authored-by: Tushar Mathur * maintenance: html template for internal server error string (#851) Closes #842 * Performance: Improve benchmarking code (#731) * wip: try server codec without validation * wip: remove flush consolidator * wip: use wrapped buffer * wip: add flush consolidator * perf: make response encoding checks faster * use encoder and decoder (#733) * wip: remove Request creation * use encoder and decoder Co-authored-by: Tushar Mathur * disable object aggregator * disable object aggregator * revert disable object aggregator * doc: update scala doc * perf: freeze the HttpResponse Co-authored-by: Amit Kumar Singh Co-authored-by: amitsingh * Refactor: Support middlewares on Http (#773) * refactor: remove type-params from Response * chore: self review * refactor: rename Middleware to HttpMiddleware * refactor: add `@@@` to Http as an alternative to `@@`. * feature: add new Middleware API * feature: add `flatten` and `ifThenElse` * feature: add `ifThenElseZIO` * refactor: fix type params for `identity` * feature: add `when` * feature: add `make` constructor * refactor: make middleware methods final * refactor: git remains * refactor: implement HttpMiddleware as Middleware * scala3 fix * Refactor CORS middleware (#788) * Refactor/merge middleware and http middleware (#790) * Refactor move cors and timeout * move some httpMiddlewares to Middleware * move some AuthMiddleware to Middleware * move remaining AuthMiddleware to Middleware * move Middlewares to middleware package * scaladoc * codec example * move Middleware to http package * named alias for `@@` * rename Auth to AuthMiddlewares * rename CORSMiddleware to CorsMiddlewares * rename CSRF to CsrfMiddlewares * make primitives private * rename MiddlewareExtensions to HttpMiddlewares * rename operators in HttpMiddlewares * scalaDoc * arg rename * doc update and general refactor * simplify cors middleware * rename CorsConfig * renames * Make middlewares package private MiddlewareRequest * Introduce MiddlewareRequest (#798) * Introduce MiddlewareRequest * PR review comments * Refactor move runAfter to Middleware * refactor: add `UMiddleware` * feature: add `contramapZIO` * refactor: move cors config to Cors file * refactor: rename files * refactor: remove AuthSpec from WebSpec * refactor: fix naming for Http operators * refactor: add partial type suport for contraMapZIO * Refactor: Codec (#841) * Add Run Before (#840) * Add Run Before * Add Run Before and After * use renamed operator * refactor: add partial type suport for contraMap * Implement missing operators in Middleware (#807) * Implement missing operators in Middleware * fix as operator * headers Middleware changes * sign cookie * extend with HeaderExtensions * rename suite * PR comments * refactor: use `Request` instead of `MiddlewareRequest` * refactor: rename methods * refactor: resolve fix me issue Co-authored-by: amitsingh Co-authored-by: Amit Kumar Singh * ZIO 2 support (#809) * upgrade to zio 2.0.0-RC1 * bump timeout for failing test * other fixes * increase timeout * rejigger broken test * try, try again * get compiling & tests passing Co-authored-by: Kit Langton * Delete commented websocket tests (#826) * Backmerge main 14 jan 2021 (#831) * Doc: Add outline (#815) * doc: add categories * refactor: doc structure refactored * fix: package-lock.json removed * refactor: create outline sub-directories * refactor: rename test to testing * Update netty-all to 4.1.73.Final (#811) * Feature: API to modify headers (#824) * feat(Headers):added new api to update headers * renamed api * Feature: Signed Cookie (#751) * feat(cookie): added secret in cookie * feat(cookie): added signcookie middleware * feat(cookie): scalafmt * fix(cookie): sign cookie while encoding * scalafmt * fix(Cookie): added unsign method for cookie * fix(cookie): minor changes * fix(signCookieMiddleware: simplified signCookies * fix(cookie): removed try catch from signContent * cookie: throw error in verify * cookie: throw error in verify * verify method changes * fixed test cases * fix: removed decodeResponseSignedCookie * fix: middlewareSpec * added modifyheaders in middleware * removed unwanted changes * scalafmt * refactoring * refactoring * build fix * build fix * fix: decodeResponseCookie * added modify * Update sbt-scalafix to 0.9.34 (#805) * Fix: Echo streaming (#828) * Failing test * Fix echo streaming * Pr Comments * ZIO 2 support (#809) * upgrade to zio 2.0.0-RC1 * bump timeout for failing test * other fixes * increase timeout * rejigger broken test * try, try again * get compiling & tests passing Co-authored-by: Kit Langton * ZIO2 changes * scalafmt * renamed Co-authored-by: Shubham Girdhar Co-authored-by: Shruti Verma <62893271+ShrutiVerma97@users.noreply.github.com> Co-authored-by: John A. De Goes Co-authored-by: Kit Langton * Merge Co-authored-by: Shubham Girdhar Co-authored-by: Shruti Verma <62893271+ShrutiVerma97@users.noreply.github.com> Co-authored-by: Gabriel Ciuloaica <95849448+gciuloaica@users.noreply.github.com> Co-authored-by: Tushar Mathur Co-authored-by: Amit Kumar Singh Co-authored-by: Javier Goday Co-authored-by: Tushar Mathur Co-authored-by: amitsingh Co-authored-by: Kit Langton --- .github/workflows/ci.yml | 22 +- .../advanced-examples/authentication.md | 31 +- .../advanced-examples/concrete-entity.md | 13 +- .../docs/examples/advanced-examples/cors.md | 22 - .../advanced-examples/hello-world-advanced.md | 13 +- .../advanced-examples/sticky-threads.md | 48 -- .../examples/advanced-examples/stream-file.md | 27 - .../advanced-examples/stream-response.md | 22 +- .../advanced-examples/web-socket-advanced.md | 36 +- .../zio-http-basic-examples/hello-world.md | 2 +- .../zio-http-basic-examples/https-client.md | 22 +- .../zio-http-basic-examples/https-server.md | 30 - .../zio-http-basic-examples/simple-client.md | 27 - .../zio-http-basic-examples/web-socket.md | 9 +- docs/website/docs/getting-started.md | 133 ++++ docs/website/docs/index.md | 148 +---- .../scala/example/HelloWorldWithCORS.scala | 7 +- .../example/HelloWorldWithMiddlewares.scala | 12 +- .../src/main/scala/example/SignCookies.scala | 3 +- project/BenchmarkWorkFlow.scala | 2 + project/BuildHelper.scala | 6 +- project/ScoverageWorkFlow.scala | 8 +- project/plugins.sbt | 2 +- .../zhttp.benchmarks/HttpRouteTextPerf.scala | 2 +- .../src/main/scala/zhttp/test/test.scala | 2 +- zio-http/src/main/scala/zhttp/core/Util.scala | 19 +- .../scala/zhttp/endpoint/CanConstruct.scala | 2 +- zio-http/src/main/scala/zhttp/http/CORS.scala | 19 - .../src/main/scala/zhttp/http/HExit.scala | 12 +- zio-http/src/main/scala/zhttp/http/Http.scala | 127 ++-- .../main/scala/zhttp/http/Middleware.scala | 578 +++++++----------- .../src/main/scala/zhttp/http/Response.scala | 2 +- .../zhttp/http/headers/HeaderChecks.scala | 2 +- .../scala/zhttp/http/middleware/Auth.scala | 38 ++ .../scala/zhttp/http/middleware/Cors.scala | 76 +++ .../scala/zhttp/http/middleware/Csrf.scala | 44 ++ .../scala/zhttp/http/middleware/Web.scala | 183 ++++++ .../scala/zhttp/http/middleware/package.scala | 5 + .../src/main/scala/zhttp/http/package.scala | 15 +- .../src/main/scala/zhttp/service/Server.scala | 132 +++- .../server/ServerChannelInitializer.scala | 24 +- .../service/server/WebSocketUpgrade.scala | 2 +- .../src/main/scala/zhttp/socket/Socket.scala | 8 +- .../src/test/scala/zhttp/http/HExitSpec.scala | 8 +- .../test/scala/zhttp/http/HeaderSpec.scala | 6 + .../src/test/scala/zhttp/http/HttpSpec.scala | 38 +- .../scala/zhttp/http/MiddlewareSpec.scala | 155 +++++ .../zhttp/http/middleware/AuthSpec.scala | 29 + .../zhttp/http/middleware/CorsSpec.scala | 54 ++ .../zhttp/http/middleware/CsrfSpec.scala | 39 ++ .../scala/zhttp/http/middleware/WebSpec.scala | 179 ++++++ .../zhttp/middleware/MiddlewareSpec.scala | 243 -------- 52 files changed, 1525 insertions(+), 1163 deletions(-) delete mode 100644 docs/website/docs/examples/advanced-examples/cors.md delete mode 100644 docs/website/docs/examples/advanced-examples/sticky-threads.md delete mode 100644 docs/website/docs/examples/advanced-examples/stream-file.md delete mode 100644 docs/website/docs/examples/zio-http-basic-examples/https-server.md delete mode 100644 docs/website/docs/examples/zio-http-basic-examples/simple-client.md create mode 100644 docs/website/docs/getting-started.md delete mode 100644 zio-http/src/main/scala/zhttp/http/CORS.scala create mode 100644 zio-http/src/main/scala/zhttp/http/middleware/Auth.scala create mode 100644 zio-http/src/main/scala/zhttp/http/middleware/Cors.scala create mode 100644 zio-http/src/main/scala/zhttp/http/middleware/Csrf.scala create mode 100644 zio-http/src/main/scala/zhttp/http/middleware/Web.scala create mode 100644 zio-http/src/main/scala/zhttp/http/middleware/package.scala create mode 100644 zio-http/src/test/scala/zhttp/http/MiddlewareSpec.scala create mode 100644 zio-http/src/test/scala/zhttp/http/middleware/AuthSpec.scala create mode 100644 zio-http/src/test/scala/zhttp/http/middleware/CorsSpec.scala create mode 100644 zio-http/src/test/scala/zhttp/http/middleware/CsrfSpec.scala create mode 100644 zio-http/src/test/scala/zhttp/http/middleware/WebSpec.scala delete mode 100644 zio-http/src/test/scala/zhttp/middleware/MiddlewareSpec.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4ffdcacfc..696f707267 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.15, 2.13.7, 3.1.0] + scala: [2.12.15, 2.13.8, 3.1.0] java: [graal_21.1.0@11, temurin@8] runs-on: ${{ matrix.os }} steps: @@ -60,7 +60,7 @@ jobs: key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} - name: Check formatting - run: sbt ++2.13.7 fmtCheck + run: sbt ++2.13.8 fmtCheck - name: Check that workflows are up to date run: sbt ++${{ matrix.scala }} githubWorkflowCheck @@ -70,7 +70,7 @@ jobs: - name: Check doc generation if: ${{ github.event_name == 'pull_request' }} - run: sbt ++2.13.7 doc + run: sbt ++2.13.8 doc - name: Compress target directories run: tar cf targets.tar target zio-http-test/target zio-http/target zio-http-benchmarks/target example/target project/target @@ -88,7 +88,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.7] + scala: [2.13.8] java: [graal_21.1.0@11] runs-on: ${{ matrix.os }} steps: @@ -133,12 +133,12 @@ jobs: tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.7) + - name: Download target directories (2.13.8) uses: actions/download-artifact@v2 with: - name: target-${{ matrix.os }}-2.13.7-${{ matrix.java }} + name: target-${{ matrix.os }}-2.13.8-${{ matrix.java }} - - name: Inflate target directories (2.13.7) + - name: Inflate target directories (2.13.8) run: | tar xf targets.tar rm targets.tar @@ -201,7 +201,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.6] + scala: [2.13.8] java: [temurin@11] runs-on: ${{ matrix.os }} steps: @@ -212,7 +212,7 @@ jobs: - name: Add Scoverage id: add_plugin - run: sed -i -e '$aaddSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.2")' project/plugins.sbt + run: sed -i -e '$aaddSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3")' project/plugins.sbt - name: Update Build Definition id: update_build_definition @@ -222,7 +222,7 @@ jobs: - name: Run Coverage id: run_coverage - run: sbt ++2.13.7 coverage 'project zhttp;test' coverageReport + run: sbt ++${{ matrix.scala }} 'coverage; project zhttp; test; coverageReport' - name: Push Codecov id: push_codecov @@ -234,7 +234,7 @@ jobs: strategy: matrix: os: [centos] - scala: [2.13.6] + scala: [2.13.8] java: [temurin@11] runs-on: [ "${{ matrix.os }}", zio-http ] steps: diff --git a/docs/website/docs/examples/advanced-examples/authentication.md b/docs/website/docs/examples/advanced-examples/authentication.md index 32f2167d32..1aa65022a7 100644 --- a/docs/website/docs/examples/advanced-examples/authentication.md +++ b/docs/website/docs/examples/advanced-examples/authentication.md @@ -2,7 +2,7 @@ ```scala import pdi.jwt.{Jwt, JwtAlgorithm, JwtClaim} -import zhttp.http.{Method, _} +import zhttp.http._ import zhttp.service.Server import zio._ @@ -27,36 +27,33 @@ object Authentication extends ZIOAppDefault { } // Authentication middleware - // Takes in a Failing HttpApp and a Succeed HttpApp which are - // called based on Authentication success or failure + // Takes in a Failing HttpApp and a Succeed HttpApp which are called based on Authentication success or failure // For each request tries to read the `X-ACCESS-TOKEN` header // Validates JWT Claim - def authenticate[R, E](fail: HttpApp[R, E], success: JwtClaim => HttpApp[R, E]): HttpApp[R, E] = - Http.flatten { - Http.fromFunction[Request] { + def authenticate[R, E](fail: HttpApp[R, E], success: JwtClaim => HttpApp[R, E]): HttpApp[R, E] = + Http + .fromFunction[Request] { _.getHeader("X-ACCESS-TOKEN") - .flatMap(header => jwtDecode(header.value.toString)) - .fold[HttpApp[R, E]](fail)(success) - } - } + .flatMap(header => jwtDecode(header._2.toString)) + .fold[HttpApp[R, E]](fail)(success) + } + .flatten // Http app that requires a JWT claim - def user(claim: JwtClaim): UHttpApp = Http.collect { - case Method.GET -> !! / "user" / name / "greet" => - Response.text(s"Welcome to the ZIO party! ${name}") - case Method.GET -> !! / "user" / "expiration" => - Response.text(s"Expires in: ${claim.expiration.getOrElse(-1L)}") + def user(claim: JwtClaim): UHttpApp = Http.collect[Request] { + case Method.GET -> !! / "user" / name / "greet" => Response.text(s"Welcome to the ZIO party! ${name}") + case Method.GET -> !! / "user" / "expiration" => Response.text(s"Expires in: ${claim.expiration.getOrElse(-1L)}") } // App that let's the user login // Login is successful only if the password is the reverse of the username - def login: UHttpApp = Http.collect { case Method.GET -> !! / "login" / username / password => + def login: UHttpApp = Http.collect[Request] { case Method.GET -> !! / "login" / username / password => if (password.reverse == username) Response.text(jwtEncode(username)) else Response.fromHttpError(HttpError.Unauthorized("Invalid username of password\n")) } // Composing all the HttpApps together - val app: UHttpApp = login +++ authenticate(Http.forbidden("Not allowed!"), user) + val app: UHttpApp = login ++ authenticate(Http.forbidden("Not allowed!"), user) // Run it like any simple app val run = diff --git a/docs/website/docs/examples/advanced-examples/concrete-entity.md b/docs/website/docs/examples/advanced-examples/concrete-entity.md index 320a407b4a..82b81c4f41 100644 --- a/docs/website/docs/examples/advanced-examples/concrete-entity.md +++ b/docs/website/docs/examples/advanced-examples/concrete-entity.md @@ -8,10 +8,10 @@ import zio._ * Example to build app on concrete entity */ object ConcreteEntity extends ZIOAppDefault { - //Request + // Request case class CreateUser(name: String) - //Response + // Response case class UserCreated(id: Long) val user: Http[Any, Nothing, CreateUser, UserCreated] = @@ -19,11 +19,10 @@ object ConcreteEntity extends ZIOAppDefault { UserCreated(2) } - val app: Http[Any, Nothing, Request, Response[Any, Nothing]] = user - .contramap[Request](req => CreateUser(req.endpoint._2.toString)) - //Http[Any, Nothing, Request, UserCreated] - .map(userCreated => Response.text(userCreated.id.toString)) - //Http[Any, Nothing, Request, Response] + val app: HttpApp[Any, Nothing] = + user + .contramap[Request](req => CreateUser(req.path.toString)) // Http[Any, Nothing, Request, UserCreated] + .map(userCreated => Response.text(userCreated.id.toString)) // Http[Any, Nothing, Request, Response] // Run it like any simple app val run = diff --git a/docs/website/docs/examples/advanced-examples/cors.md b/docs/website/docs/examples/advanced-examples/cors.md deleted file mode 100644 index 2d28565ca2..0000000000 --- a/docs/website/docs/examples/advanced-examples/cors.md +++ /dev/null @@ -1,22 +0,0 @@ -# CORS Handling - -```scala -import zhttp.http._ -import zhttp.service.Server -import zio._ - -object HelloWorldWithCORS extends ZIOAppDefault { - // Create HTTP route with CORS enabled - val app: HttpApp[Any, Nothing] = CORS( - Http.collect[Request] { - case Method.GET -> !! / "text" => Response.text("Hello World!") - case Method.GET -> !! / "json" => Response.jsonString("""{"greetings": "Hello World!"}""") - }, - config = CORSConfig(anyOrigin = true), - ) - - // Run it like any simple app - val run = - Server.start(8090, app.silent) -} -``` \ No newline at end of file diff --git a/docs/website/docs/examples/advanced-examples/hello-world-advanced.md b/docs/website/docs/examples/advanced-examples/hello-world-advanced.md index 1ba8f2c67e..0af5eca4f7 100644 --- a/docs/website/docs/examples/advanced-examples/hello-world-advanced.md +++ b/docs/website/docs/examples/advanced-examples/hello-world-advanced.md @@ -10,22 +10,22 @@ import scala.util.Try object HelloWorldAdvanced extends ZIOAppDefault { // Set a port - private val PORT = 8090 + private val PORT = 0 private val fooBar: HttpApp[Any, Nothing] = Http.collect[Request] { case Method.GET -> !! / "foo" => Response.text("bar") case Method.GET -> !! / "bar" => Response.text("foo") } - private val app = Http.collectM[Request] { - case Method.GET -> !! / "random" => random.nextString(10).map(Response.text) + private val app = Http.collectZIO[Request] { + case Method.GET -> !! / "random" => random.nextString(10).map(Response.text(_)) case Method.GET -> !! / "utc" => clock.currentDateTime.map(s => Response.text(s.toString)) } private val server = Server.port(PORT) ++ // Setup port Server.paranoidLeakDetection ++ // Paranoid leak detection (affects performance) - Server.app(fooBar +++ app) // Setup the Http app + Server.app(fooBar ++ app) // Setup the Http app override val run = { // Configure thread count using CLI @@ -33,9 +33,9 @@ object HelloWorldAdvanced extends ZIOAppDefault { // Create a new server server.make - .use(_ => + .use(start => // Waiting for the server to start - console.putStrLn(s"Server started on port $PORT") + console.putStrLn(s"Server started on port ${start.port}") // Ensures the server doesn't die after printing *> ZIO.never, @@ -43,4 +43,5 @@ object HelloWorldAdvanced extends ZIOAppDefault { .provideCustomLayer(ServerChannelFactory.auto ++ EventLoopGroup.auto(nThreads)) } } + ``` \ No newline at end of file diff --git a/docs/website/docs/examples/advanced-examples/sticky-threads.md b/docs/website/docs/examples/advanced-examples/sticky-threads.md deleted file mode 100644 index 455d83a117..0000000000 --- a/docs/website/docs/examples/advanced-examples/sticky-threads.md +++ /dev/null @@ -1,48 +0,0 @@ -# Sticky Threads - -```scala -import zhttp.http._ -import zhttp.service.Server -import zio._ -import zio.duration._ - -/** - * The following example depicts thread stickiness. The way it works is — once a - * request is received on the server, a thread is associated with it permanently. - * Any ZIO execution within the context of that request is guaranteed to be done - * on the same thread. This level of thread stickiness improves the performance - * characteristics of the server dramatically. - */ -object StickyThread extends ZIOAppDefault { - - /** - * A simple utility function that prints the fiber with the current thread. - */ - private def printThread(tag: String): ZIO[Any, Nothing, Unit] = { - for { - id <- ZIO.fiberId - _ <- UIO(println(s"${tag.padTo(6, ' ')}: - Fiber(${id.seqNumber}) Thread(${Thread.currentThread().getName})")) - } yield () - } - - /** - * The expected behaviour is that all the `printThread` output different fiber ids - * with the same thread name. - */ - val app = Http.collectM[Request] { case Method.GET -> !! / "text" => - for { - - _ <- printThread("Start") - f1 <- ZIO.sleep(1 second).zipLeft(printThread("First")).fork - f2 <- ZIO.sleep(1 second).zipLeft(printThread("Second")).fork - _ <- f1.join <*> f2.join - } yield Response.text("Hello World!") - } - - // Run it like any simple app - val run = - Server.start(8090, app.silent) -} - -``` \ No newline at end of file diff --git a/docs/website/docs/examples/advanced-examples/stream-file.md b/docs/website/docs/examples/advanced-examples/stream-file.md deleted file mode 100644 index 0841c73f3a..0000000000 --- a/docs/website/docs/examples/advanced-examples/stream-file.md +++ /dev/null @@ -1,27 +0,0 @@ -# Streaming File -```scala -import zhttp.http._ -import zhttp.service._ -import zio._ -import zio.stream._ - -import java.nio.file.Paths - -object FileStreaming extends ZIOAppDefault { - // Read the file as ZStream - val content = HttpData.fromStream { - ZStream.fromFile(Paths.get("README.md")) - } - - // Create HTTP route - val app = Http.collect[Request] { - case Method.GET -> !! / "health" => Response.ok - case Method.GET -> !! / "file" => Response.http(content = content) - } - - // Run it like any simple app - val run = - Server.start(8090, app.silent) -} - -``` \ No newline at end of file diff --git a/docs/website/docs/examples/advanced-examples/stream-response.md b/docs/website/docs/examples/advanced-examples/stream-response.md index 0e1460f78f..6c255d12ce 100644 --- a/docs/website/docs/examples/advanced-examples/stream-response.md +++ b/docs/website/docs/examples/advanced-examples/stream-response.md @@ -3,16 +3,18 @@ ```scala import zhttp.http._ import zhttp.service.Server -import zio._ import zio.stream.ZStream +import zio._ /** * Example to encode content using a ZStream */ object StreamingResponse extends ZIOAppDefault { - // Create a message as a Chunk[Byte] - val message = Chunk.fromArray("Hello world !\r\n".getBytes(HTTP_CHARSET)) + // Starting the server (for more advanced startup configuration checkout `HelloWorldAdvanced`) + override def run = Server.start(8090, app.silent) + // Create a message as a Chunk[Byte] + val message = Chunk.fromArray("Hello world !\r\n".getBytes(HTTP_CHARSET)) // Use `Http.collect` to match on route val app: HttpApp[Any, Nothing] = Http.collect[Request] { @@ -21,20 +23,12 @@ object StreamingResponse extends ZIOAppDefault { // ZStream powered response case Method.GET -> !! / "stream" => - Response.http( + Response( status = Status.OK, - headers = List(Header.contentLength(message.length.toLong)), - content = HttpData.fromStream(ZStream.fromChunk(message)), - // Encoding content using a ZStream + headers = Headers.contentLength(message.length.toLong), + data = HttpData.fromStream(ZStream.fromChunk(message)), // Encoding content using a ZStream ) } - val run = { - - // Starting the server (for more advanced startup - // configuration checkout `HelloWorldAdvanced`) - Server.start(8090, app.silent) - } } - ``` \ No newline at end of file diff --git a/docs/website/docs/examples/advanced-examples/web-socket-advanced.md b/docs/website/docs/examples/advanced-examples/web-socket-advanced.md index e3d020401f..54f9d193cc 100644 --- a/docs/website/docs/examples/advanced-examples/web-socket-advanced.md +++ b/docs/website/docs/examples/advanced-examples/web-socket-advanced.md @@ -1,7 +1,7 @@ # Web Socket Server ```scala import zhttp.http._ -import zhttp.service._ +import zhttp.service.Server import zhttp.socket._ import zio._ import zio.duration._ @@ -27,20 +27,30 @@ object WebSocketAdvanced extends ZIOAppDefault { private val decoder = SocketDecoder.allowExtensions // Combine all channel handlers together - private val socketApp = - SocketApp.open(open) ++ // Called after the request is successfully upgraded to websocket - SocketApp.message(echo merge fooBar) ++ // Called after each message being received on the channel - SocketApp.close(_ => console.putStrLn("Closed!").ignore) ++ // Called after the connection is closed - SocketApp.error(_ => - console.putStrLn("Error!").ignore, - ) ++ // Called whenever there is an error on the socket channel - SocketApp.decoder(decoder) ++ - SocketApp.protocol(protocol) + private val socketApp = { + + SocketApp(echo merge fooBar) // Called after each message being received on the channel + + // Called after the request is successfully upgraded to websocket + .onOpen(open) + + // Called after the connection is closed + .onClose(_ => console.putStrLn("Closed!").ignore) + + // Called whenever there is an error on the socket channel + .onError(_ => console.putStrLn("Error!").ignore) + + // Setup websocket decoder config + .withDecoder(decoder) + + // Setup websocket protocol config + .withProtocol(protocol) + } private val app = - Http.collect[Request] { - case Method.GET -> !! / "greet" / name => Response.text(s"Greetings ${name}!") - case Method.GET -> !! / "subscriptions" => socketApp + Http.collectZIO[Request] { + case Method.GET -> !! / "greet" / name => Response.text(s"Greetings ${name}!").wrapZIO + case Method.GET -> !! / "subscriptions" => socketApp.toResponse } val run = diff --git a/docs/website/docs/examples/zio-http-basic-examples/hello-world.md b/docs/website/docs/examples/zio-http-basic-examples/hello-world.md index cff3dabe8b..867c76f299 100644 --- a/docs/website/docs/examples/zio-http-basic-examples/hello-world.md +++ b/docs/website/docs/examples/zio-http-basic-examples/hello-world.md @@ -10,7 +10,7 @@ object HelloWorld extends ZIOAppDefault { // Create HTTP route val app: HttpApp[Any, Nothing] = Http.collect[Request] { case Method.GET -> !! / "text" => Response.text("Hello World!") - case Method.GET -> !! / "json" => Response.jsonString("""{"greetings": "Hello World!"}""") + case Method.GET -> !! / "json" => Response.json("""{"greetings": "Hello World!"}""") } // Run it like any simple app diff --git a/docs/website/docs/examples/zio-http-basic-examples/https-client.md b/docs/website/docs/examples/zio-http-basic-examples/https-client.md index 111ef05234..3d818d5cc9 100644 --- a/docs/website/docs/examples/zio-http-basic-examples/https-client.md +++ b/docs/website/docs/examples/zio-http-basic-examples/https-client.md @@ -2,7 +2,7 @@ ```scala import io.netty.handler.ssl.SslContextBuilder -import zhttp.http.{Header, HttpData} +import zhttp.http.Headers import zhttp.service.client.ClientSSLHandler.ClientSSLOptions import zhttp.service.{ChannelFactory, Client, EventLoopGroup} import zio._ @@ -14,11 +14,11 @@ import javax.net.ssl.TrustManagerFactory object HttpsClient extends ZIOAppDefault { val env = ChannelFactory.auto ++ EventLoopGroup.auto() val url = "https://sports.api.decathlon.com/groups/water-aerobics" - val headers = List(Header.host("sports.api.decathlon.com")) + val headers = Headers.host("sports.api.decathlon.com") - //Configuring Truststore for https(optional) + // Configuring Truststore for https(optional) val trustStore: KeyStore = KeyStore.getInstance("JKS") - val trustStorePath: InputStream = getClass.getResourceAsStream("truststore.jks") + val trustStorePath: InputStream = getClass.getClassLoader.getResourceAsStream("truststore.jks") val trustStorePassword: String = "changeit" val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) @@ -27,18 +27,12 @@ object HttpsClient extends ZIOAppDefault { trustManagerFactory.init(trustStore) val sslOption: ClientSSLOptions = - ClientSSLOptions - .CustomSSL(SslContextBuilder.forClient().trustManager(trustManagerFactory).build()) + ClientSSLOptions.CustomSSL(SslContextBuilder.forClient().trustManager(trustManagerFactory).build()) val program = for { - res <- Client.request(url, headers, sslOption) - _ <- console.putStrLn { - res.content match { - case HttpData.CompleteData(data) => data.map(_.toChar).mkString - case HttpData.StreamData(_) => "" - case HttpData.Empty => "" - } - } + res <- Client.request(url, headers, sslOption) + data <- res.getBodyAsString + _ <- console.putStrLn { data } } yield () override val run = diff --git a/docs/website/docs/examples/zio-http-basic-examples/https-server.md b/docs/website/docs/examples/zio-http-basic-examples/https-server.md deleted file mode 100644 index ade2bf00b5..0000000000 --- a/docs/website/docs/examples/zio-http-basic-examples/https-server.md +++ /dev/null @@ -1,30 +0,0 @@ -# HTTPS Server -```scala -import zhttp.http._ -import zhttp.service.server.ServerChannelFactory -import zhttp.service.server.ServerSSLHandler.{ServerSSLOptions, ctxFromKeystore} -import zhttp.service.{EventLoopGroup, Server} -import zio._ - -object HttpsHelloWorld extends ZIOAppDefault { - - // Create HTTP route - val app: HttpApp[Any, Nothing] = Http.collect[Request] { - case Method.GET -> !! / "text" => Response.text("Hello World!") - case Method.GET -> !! / "json" => Response.jsonString("""{"greetings": "Hello World!"}""") - } - - /** - * sslcontext can be created using SslContexBuilder. - * In this example an inbuilt API using keystore is used - */ - val sslctx = ctxFromKeystore(getClass.getResourceAsStream("keystore.jks"), "password", "password") - - private val server = - Server.port(8090) ++ Server.app(app) ++ Server.ssl(ServerSSLOptions(sslctx)) - - val run = - server.make.useForever - .provideCustom(ServerChannelFactory.auto, EventLoopGroup.auto(0)) -} -``` \ No newline at end of file diff --git a/docs/website/docs/examples/zio-http-basic-examples/simple-client.md b/docs/website/docs/examples/zio-http-basic-examples/simple-client.md deleted file mode 100644 index 7dadccb092..0000000000 --- a/docs/website/docs/examples/zio-http-basic-examples/simple-client.md +++ /dev/null @@ -1,27 +0,0 @@ -# Simple HTTP Client -```scala -import zhttp.http.{Header, HttpData} -import zhttp.service.{ChannelFactory, Client, EventLoopGroup} -import zio._ - -object SimpleClient extends ZIOAppDefault { - val env = ChannelFactory.auto ++ EventLoopGroup.auto() - val url = "http://sports.api.decathlon.com/groups/water-aerobics" - val headers = List(Header.host("sports.api.decathlon.com")) - - val program = for { - res <- Client.request(url, headers) - _ <- console.putStrLn { - res.content match { - case HttpData.CompleteData(data) => data.map(_.toChar).mkString - case HttpData.StreamData(_) => "" - case HttpData.Empty => "" - } - } - } yield () - - override val run = - program.provide(env) - -} -``` \ No newline at end of file diff --git a/docs/website/docs/examples/zio-http-basic-examples/web-socket.md b/docs/website/docs/examples/zio-http-basic-examples/web-socket.md index fb780be375..e3fc9a7f5a 100644 --- a/docs/website/docs/examples/zio-http-basic-examples/web-socket.md +++ b/docs/website/docs/examples/zio-http-basic-examples/web-socket.md @@ -15,14 +15,13 @@ object WebSocketEcho extends ZIOAppDefault { case WebSocketFrame.Text("BAR") => ZStream.succeed(WebSocketFrame.text("FOO")) case WebSocketFrame.Ping => ZStream.succeed(WebSocketFrame.pong) case WebSocketFrame.Pong => ZStream.succeed(WebSocketFrame.ping) - case fr @ WebSocketFrame.Text(_) => ZStream.repeat(fr) - .schedule(Schedule.spaced(1 second)).take(10) + case fr @ WebSocketFrame.Text(_) => ZStream.repeat(fr).schedule(Schedule.spaced(1 second)).take(10) } private val app = - Http.collect[Request] { - case Method.GET -> !! / "greet" / name => Response.text(s"Greetings {$name}!") - case Method.GET -> !! / "subscriptions" => Response.socket(socket) + Http.collectZIO[Request] { + case Method.GET -> !! / "greet" / name => Response.text(s"Greetings {$name}!").wrapZIO + case Method.GET -> !! / "subscriptions" => socket.toResponse } override val run = diff --git a/docs/website/docs/getting-started.md b/docs/website/docs/getting-started.md new file mode 100644 index 0000000000..e3a09048b8 --- /dev/null +++ b/docs/website/docs/getting-started.md @@ -0,0 +1,133 @@ +--- +sidebar_position: 2 +--- + +# Getting Started + +## Http + +### Creating a "_Hello World_" app + +```scala +import zhttp.http._ + +val app = Http.text("Hello World!") +``` + +An application can be made using any of the available operators on `zhttp.Http`. In the above program for any Http request, the response is always `"Hello World!"`. + +### Routing + +```scala +import zhttp.http._ + +val app = Http.collect[Request] { + case Method.GET -> !! / "fruits" / "a" => Response.text("Apple") + case Method.GET -> !! / "fruits" / "b" => Response.text("Banana") +} +``` + +Pattern matching on route is supported by the framework + +### Composition + +```scala +import zhttp.http._ + +val a = Http.collect[Request] { case Method.GET -> !! / "a" => Response.ok } +val b = Http.collect[Request] { case Method.GET -> !! / "b" => Response.ok } + +val app = a <> b +``` + +Apps can be composed using the `<>` operator. The way it works is, if none of the routes match in `a` , or a `NotFound` error is thrown from `a`, and then the control is passed on to the `b` app. + +### ZIO Integration + +```scala +val app = Http.collectZIO[Request] { + case Method.GET -> !! / "hello" => Response.text("Hello World").wrapZIO +} +``` + +`Http.collectZIO` allow routes to return a ZIO effect value. + +### Accessing the Request + +```scala +import zhttp.http._ + +val app = Http.collectZIO[Request] { + case req @ Method.GET -> !! / "fruits" / "a" => + Response.text("URL:" + req.url.path.asString + " Headers: " + req.getHeaders).wrapZIO + case req @ Method.POST -> !! / "fruits" / "a" => + req.getBodyAsString.map(Response.text(_)) + } +``` + +### Testing + +zhttp provides a `zhttp-test` package for use in unit tests. You can utilize it as follows: + +```scala +import zio.test._ +import zhttp.test._ +import zhttp.http._ + +object Spec extends DefaultRunnableSpec { + + def spec = suite("http")( + testM("should be ok") { + val app = Http.ok + val req = Request() + assertM(app(req))(equalTo(Response.ok)) // an apply method is added via `zhttp.test` package + } + ) +} +``` + +## Socket + +### Creating a socket app + +```scala +import zhttp.socket._ + +private val socket = Socket.collect[WebSocketFrame] { case WebSocketFrame.Text("FOO") => + ZStream.succeed(WebSocketFrame.text("BAR")) + } + + private val app = Http.collectZIO[Request] { + case Method.GET -> !! / "greet" / name => Response.text(s"Greetings {$name}!").wrapZIO + case Method.GET -> !! / "ws" => socket.toResponse + } +``` + +## Server + +### Starting an Http App + +```scala +import zhttp.http._ +import zhttp.service.Server +import zio._ + +object HelloWorld extends App { + val app = Http.ok + + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + Server.start(8090, app).exitCode +} +``` + +A simple Http app that responds with empty content and a `200` status code is deployed on port `8090` using `Server.start`. + +## Examples + +- [Simple Server](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/HelloWorld.scala) +- [Advanced Server](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/HelloWorldAdvanced.scala) +- [WebSocket Server](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/SocketEchoServer.scala) +- [Streaming Response](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/StreamingResponse.scala) +- [Simple Client](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/SimpleClient.scala) +- [File Streaming](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/FileStreaming.scala) +- [Authentication](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/Authentication.scala) diff --git a/docs/website/docs/index.md b/docs/website/docs/index.md index 52e71dd6e9..38dbc946ff 100644 --- a/docs/website/docs/index.md +++ b/docs/website/docs/index.md @@ -3,149 +3,5 @@ sidebar_position: 1 sidebar_label: "Setup" --- -# Getting Started - -## Http - -### Creating a "_Hello World_" app - -```scala -import zhttp.http._ - -val app = Http.text("Hello World!") -``` - -An application can be made using any of the available operators on `zhttp.Http`. In the above program for any Http request, the response is always `"Hello World!"`. - -### Routing - -```scala -import zhttp.http._ - -val app = Http.collect[Request] { - case Method.GET -> Root / "fruits" / "a" => Response.text("Apple") - case Method.GET -> Root / "fruits" / "b" => Response.text("Banana") -} -``` - -Pattern matching on route is supported by the framework - -### Composition - -```scala -import zhttp.http._ - -val a = Http.collect[Request] { case Method.GET -> Root / "a" => Response.ok } -val b = Http.collect[Request] { case Method.GET -> Root / "b" => Response.ok } - -val app = a ++ b -``` - -Apps can be composed using the `<>` operator. The way it works is, if none of the routes match in `a` , or a `NotFound` error is thrown from `a`, and then the control is passed on to the `b` app. - -### ZIO Integration - -```scala -val app = Http.collectM[Request] { - case Method.GET -> Root / "hello" => ZIO.succeed(Response.text("Hello World")) -} -``` - -`Http.collectM` allow routes to return a ZIO effect value. - -### Accessing the Request - -```scala -import zhttp.http._ - -val app = Http.collect[Request] { - case req @ Method.GET -> Root / "fruits" / "a" => - Response.text("URL:" + req.url.path.asString + " Headers: " + r.headers) - case req @ Method.POST -> Root / "fruits" / "a" => - Response.text(req.getBodyAsString.getOrElse("No body!")) -} -``` - -### Testing - -zhttp provides a `zhttp-test` package for use in unit tests. You can utilize it as follows: - -```scala -import zio.test._ -import zhttp.test._ -import zhttp.http._ - -object Spec extends DefaultRunnableSpec { - val app = Http.collect[Request] { - case Method.GET -> Root / "text" => Response.text("Hello World!") - } - - def spec = suite("http") ( - test("should be ok") { - val req = ??? - val expectedRes = resp => resp.status.toJHttpStatus.code() == Status.OK - assertM(app(req))(expectedRes) // an apply method is added via `zhttp.test` package - } - ) -} -``` - -```scala -import zhttp.http._ - -val app = Http.collect[Request] { - case req @ Method.GET -> Root / "fruits" / "a" => - Response.text("URL:" + req.url.path.asString + " Headers: " + r.headers) - case req @ Method.POST -> Root / "fruits" / "a" => - Response.text(req.getBodyAsString.getOrElse("No body!")) -} -``` - -## Socket - -### Creating a socket app - -```scala -import zhttp.socket._ - -private val socket = Socket.collect[WebSocketFrame] { - case WebSocketFrame.Text("FOO") => ZStream.succeed(WebSocketFrame.text("BAR")) -} - -private val app = Http.collect[Request] { - case Method.GET -> Root / "greet" / name => Response.text(s"Greetings {$name}!") - case Method.GET -> Root / "ws" => Response.socket(socket) -} -``` - -## Server - -### Starting an Http App - -```scala -import zhttp.http._ -import zhttp.service.Server -import zio._ - -object HelloWorld extends ZIOAppDefault { - val app = Http.ok - - val run = - Server.start(8090, app) -} -``` - -A simple Http app that responds with empty content and a `200` status code is deployed on port `8090` using `Server.start`. - -## Examples - -- [Simple Server](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/HelloWorld.scala) -- [Advanced Server](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/HelloWorldAdvanced.scala) -- [WebSocket Server](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/SocketEchoServer.scala) -- [Streaming Response](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/StreamingResponse.scala) -- [Simple Client](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/SimpleClient.scala) -- [File Streaming](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/FileStreaming.scala) -- [Authentication](https://github.com/dream11/zio-http/blob/main/example/src/main/scala/Authentication.scala) - - - +# Setup +Work in progress diff --git a/example/src/main/scala/example/HelloWorldWithCORS.scala b/example/src/main/scala/example/HelloWorldWithCORS.scala index a91a7b917b..f2d432db67 100644 --- a/example/src/main/scala/example/HelloWorldWithCORS.scala +++ b/example/src/main/scala/example/HelloWorldWithCORS.scala @@ -1,15 +1,16 @@ package example import zhttp.http.Middleware.cors -import zhttp.http.{CORSConfig, HttpApp, Method, Response, _} +import zhttp.http._ +import zhttp.http.middleware.Cors.CorsConfig import zhttp.service.Server import zio._ object HelloWorldWithCORS extends ZIOAppDefault { // Create CORS configuration - val config: CORSConfig = - CORSConfig(allowedOrigins = _ == "dev", allowedMethods = Some(Set(Method.PUT, Method.DELETE))) + val config: CorsConfig = + CorsConfig(allowedOrigins = _ == "dev", allowedMethods = Some(Set(Method.PUT, Method.DELETE))) // Create HTTP route with CORS enabled val app: HttpApp[Any, Nothing] = diff --git a/example/src/main/scala/example/HelloWorldWithMiddlewares.scala b/example/src/main/scala/example/HelloWorldWithMiddlewares.scala index c662d28a32..77ace0a4cf 100644 --- a/example/src/main/scala/example/HelloWorldWithMiddlewares.scala +++ b/example/src/main/scala/example/HelloWorldWithMiddlewares.scala @@ -1,7 +1,7 @@ package example -import zhttp.http.Middleware.{addHeader, debug, patchZIO, timeout} import zhttp.http._ +import zhttp.http.middleware.HttpMiddleware import zhttp.service.Server import zio._ @@ -17,20 +17,20 @@ object HelloWorldWithMiddlewares extends ZIOAppDefault { case Method.GET -> !! / "long-running" => ZIO.succeed(Response.text("Hello World!")).delay(5 seconds) } - val serverTime: Middleware[Clock, Nothing] = patchZIO((_, _) => + val serverTime: HttpMiddleware[Clock, Nothing] = Middleware.patchZIO(_ => for { currentMilliseconds <- Clock.currentTime(TimeUnit.MILLISECONDS) withHeader = Patch.addHeader("X-Time", currentMilliseconds.toString) } yield withHeader, ) - val middlewares: Middleware[Console with Clock, IOException] = + val middlewares: HttpMiddleware[Console with Clock, IOException] = // print debug info about request and response - debug ++ + Middleware.debug ++ // close connection if request takes more than 3 seconds - timeout(3 seconds) ++ + Middleware.timeout(3 seconds) ++ // add static header - addHeader("X-Environment", "Dev") ++ + Middleware.addHeader("X-Environment", "Dev") ++ // add dynamic header serverTime diff --git a/example/src/main/scala/example/SignCookies.scala b/example/src/main/scala/example/SignCookies.scala index 4de03d5978..49b20719cd 100644 --- a/example/src/main/scala/example/SignCookies.scala +++ b/example/src/main/scala/example/SignCookies.scala @@ -17,6 +17,5 @@ object SignCookies extends ZIOAppDefault { } // Run it like any simple app - val run = - Server.start(8090, app).exitCode + val run = Server.start(8090, app) } diff --git a/project/BenchmarkWorkFlow.scala b/project/BenchmarkWorkFlow.scala index acfd278b00..a2757abda1 100644 --- a/project/BenchmarkWorkFlow.scala +++ b/project/BenchmarkWorkFlow.scala @@ -1,3 +1,4 @@ +import BuildHelper.Scala213 import sbtghactions.GenerativePlugin.autoImport.{UseRef, WorkflowJob, WorkflowStep} object BenchmarkWorkFlow { @@ -10,6 +11,7 @@ object BenchmarkWorkFlow { cond = Some( "${{ github.event_name == 'pull_request'}}", ), + scalas = List(Scala213), steps = List( WorkflowStep.Run( env = Map("GITHUB_TOKEN" -> "${{secrets.ACTIONS_PAT}}"), diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index d2bf8554f4..01153b0415 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -5,9 +5,9 @@ import xerial.sbt.Sonatype.autoImport._ object BuildHelper extends ScalaSettings { val Scala212 = "2.12.15" - val Scala213 = "2.13.7" + val Scala213 = "2.13.8" val ScalaDotty = "3.1.0" - val ScoverageVersion = "1.9.2" + val ScoverageVersion = "1.9.3" private val stdOptions = Seq( "-deprecation", @@ -82,7 +82,7 @@ object BuildHelper extends ScalaSettings { ThisBuild / crossScalaVersions := Seq(Scala212, Scala213, ScalaDotty), ThisBuild / scalaVersion := Scala213, scalacOptions := stdOptions ++ extraOptions(scalaVersion.value, optimize = !isSnapshot.value), - semanticdbVersion := scalafixSemanticdb.withRevision("4.4.30").revision, // use Scalafix compatible version + semanticdbVersion := scalafixSemanticdb.revision, // use Scalafix compatible version ThisBuild / scalafixScalaBinaryVersion := CrossVersion.binaryScalaVersion(scalaVersion.value), ThisBuild / scalafixDependencies ++= List( diff --git a/project/ScoverageWorkFlow.scala b/project/ScoverageWorkFlow.scala index 61dd8f5210..d88efbde86 100644 --- a/project/ScoverageWorkFlow.scala +++ b/project/ScoverageWorkFlow.scala @@ -1,5 +1,6 @@ +import BuildHelper.{Scala213, ScoverageVersion} import sbtghactions.GenerativePlugin.autoImport.{WorkflowJob, WorkflowStep} -import BuildHelper.{ScoverageVersion, Scala213} + object ScoverageWorkFlow { // TODO move plugins to plugins.sbt after scoverage's support for Scala 3 val scoveragePlugin = s"""addSbtPlugin("org.scoverage" % "sbt-scoverage" % "${ScoverageVersion}")""" @@ -12,6 +13,7 @@ object ScoverageWorkFlow { WorkflowJob( id = "unsafeRunScoverage", name = "Unsafe Scoverage", + scalas = List(Scala213), steps = List( WorkflowStep.CheckoutFull, WorkflowStep.Run( @@ -26,8 +28,8 @@ object ScoverageWorkFlow { id = Some("update_build_definition"), name = Some("Update Build Definition"), ), - WorkflowStep.Run( - commands = List(s"sbt ++${Scala213} coverage 'project zhttp;test' coverageReport"), + WorkflowStep.Sbt( + commands = List(s"coverage; project zhttp; test; coverageReport"), id = Some("run_coverage"), name = Some("Run Coverage"), ), diff --git a/project/plugins.sbt b/project/plugins.sbt index a3a4d503f6..440aa80ac0 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.11") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.12") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.34") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") diff --git a/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/HttpRouteTextPerf.scala b/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/HttpRouteTextPerf.scala index e748e87e7e..c2b26e6fec 100644 --- a/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/HttpRouteTextPerf.scala +++ b/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/HttpRouteTextPerf.scala @@ -16,7 +16,7 @@ class HttpRouteTextPerf { private val res = Response.text("HELLO WORLD") private val app = Http.succeed(res) private val req: Request = Request(Method.GET, URL(!!)) - private val httpProgram = ZIO.foreachDiscard(0 to 1000) { _ => app.execute(req).toEffect } + private val httpProgram = ZIO.foreachDiscard(0 to 1000) { _ => app.execute(req).toZIO } private val UIOProgram = ZIO.foreachDiscard(0 to 1000) { _ => UIO(res) } @Benchmark diff --git a/zio-http-test/src/main/scala/zhttp/test/test.scala b/zio-http-test/src/main/scala/zhttp/test/test.scala index e32ecd7d7c..94b25c6d79 100644 --- a/zio-http-test/src/main/scala/zhttp/test/test.scala +++ b/zio-http-test/src/main/scala/zhttp/test/test.scala @@ -5,6 +5,6 @@ import zio.ZIO package object test { implicit class HttpWithTest[R, E, A, B](http: Http[R, E, A, B]) { - def apply(req: A): ZIO[R, Option[E], B] = http.execute(req).toEffect + def apply(req: A): ZIO[R, Option[E], B] = http.execute(req).toZIO } } diff --git a/zio-http/src/main/scala/zhttp/core/Util.scala b/zio-http/src/main/scala/zhttp/core/Util.scala index ce62f6d5b6..b507578e83 100644 --- a/zio-http/src/main/scala/zhttp/core/Util.scala +++ b/zio-http/src/main/scala/zhttp/core/Util.scala @@ -1,5 +1,7 @@ package zhttp.core +import zhttp.html._ + import java.io.{PrintWriter, StringWriter} object Util { @@ -10,15 +12,12 @@ object Util { } def prettyPrintHtml(throwable: Throwable): String = { - s""" - | - | - | - | - |

Internal Server Error

- |
${prettyPrint(throwable).split("\n").map(str => s"
${str}
").mkString("")}
- | - | - |""".stripMargin + html( + head(), + body( + h1("Internal Server Error"), + pre(div(prettyPrint(throwable).split("\n").mkString("\n"))), + ), + ).encode } } diff --git a/zio-http/src/main/scala/zhttp/endpoint/CanConstruct.scala b/zio-http/src/main/scala/zhttp/endpoint/CanConstruct.scala index 81d148ff29..c3e276b826 100644 --- a/zio-http/src/main/scala/zhttp/endpoint/CanConstruct.scala +++ b/zio-http/src/main/scala/zhttp/endpoint/CanConstruct.scala @@ -44,7 +44,7 @@ object CanConstruct { Http .collectHttp[Request] { case req => route.extract(req) match { - case Some(value) => Http.fromEffect(f(Request.ParameterizedRequest(req, value))) + case Some(value) => Http.fromZIO(f(Request.ParameterizedRequest(req, value))) case None => Http.empty } } diff --git a/zio-http/src/main/scala/zhttp/http/CORS.scala b/zio-http/src/main/scala/zhttp/http/CORS.scala deleted file mode 100644 index ff2b80059e..0000000000 --- a/zio-http/src/main/scala/zhttp/http/CORS.scala +++ /dev/null @@ -1,19 +0,0 @@ -package zhttp.http - -import io.netty.handler.codec.http.HttpHeaderNames - -final case class CORSConfig( - anyOrigin: Boolean = false, - anyMethod: Boolean = true, - allowCredentials: Boolean = false, - allowedOrigins: String => Boolean = _ => false, - allowedMethods: Option[Set[Method]] = None, - allowedHeaders: Option[Set[String]] = Some( - Set(HttpHeaderNames.CONTENT_TYPE.toString, HttpHeaderNames.AUTHORIZATION.toString, "*"), - ), - exposedHeaders: Option[Set[String]] = Some(Set("*")), -) - -object CORS { - def DefaultCORSConfig = CORSConfig(anyOrigin = true, allowCredentials = true) -} diff --git a/zio-http/src/main/scala/zhttp/http/HExit.scala b/zio-http/src/main/scala/zhttp/http/HExit.scala index 9cd3491ecc..80388deff8 100644 --- a/zio-http/src/main/scala/zhttp/http/HExit.scala +++ b/zio-http/src/main/scala/zhttp/http/HExit.scala @@ -45,10 +45,10 @@ private[zhttp] sealed trait HExit[-R, +E, +A] { self => Effect( zio.foldZIO( { - case Some(error) => ee(error).toEffect - case None => dd.toEffect + case Some(error) => ee(error).toZIO + case None => dd.toZIO }, - a => aa(a).toEffect, + a => aa(a).toZIO, ), ) case HExit.Empty => dd @@ -59,7 +59,7 @@ private[zhttp] sealed trait HExit[-R, +E, +A] { self => def orElse[R1 <: R, E1, A1 >: A](other: HExit[R1, E1, A1]): HExit[R1, E1, A1] = self.foldExit(_ => other, HExit.succeed, HExit.empty) - def toEffect: ZIO[R, Option[E], A] = self match { + def toZIO: ZIO[R, Option[E], A] = self match { case HExit.Success(a) => ZIO.succeed(a) case HExit.Failure(e) => ZIO.fail(Option(e)) case HExit.Empty => ZIO.fail(None) @@ -68,12 +68,12 @@ private[zhttp] sealed trait HExit[-R, +E, +A] { self => } object HExit { - def effect[R, E, A](z: ZIO[R, E, A]): HExit[R, E, A] = Effect(z.mapError(Option(_))) - def empty: HExit[Any, Nothing, Nothing] = Empty def fail[E](e: E): HExit[Any, E, Nothing] = Failure(e) + def fromZIO[R, E, A](z: ZIO[R, E, A]): HExit[R, E, A] = Effect(z.mapError(Option(_))) + def succeed[A](a: A): HExit[Any, Nothing, A] = Success(a) def unit: HExit[Any, Nothing, Unit] = HExit.succeed(()) diff --git a/zio-http/src/main/scala/zhttp/http/Http.scala b/zio-http/src/main/scala/zhttp/http/Http.scala index baff86f4ee..63be73b261 100644 --- a/zio-http/src/main/scala/zhttp/http/Http.scala +++ b/zio-http/src/main/scala/zhttp/http/Http.scala @@ -17,6 +17,13 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => import Http._ + /** + * Attaches the provided middleware to the Http app + */ + final def @@[R1 <: R, E1 >: E, A1 <: A, B1 >: B, A2, B2]( + mid: Middleware[R1, E1, A1, B1, A2, B2], + ): Http[R1, E1, A2, B2] = mid(self) + /** * Alias for flatmap */ @@ -62,7 +69,7 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => /** * Consumes the input and executes the Http. */ - final def apply(a: A): ZIO[R, Option[E], B] = execute(a).toEffect + final def apply(a: A): ZIO[R, Option[E], B] = execute(a).toZIO /** * Makes the app resolve with a constant value @@ -101,7 +108,7 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => /** * Transforms the input of the http before passing it on to the current Http */ - final def contraFlatMap[X]: MkContraFlatMap[R, E, A, B, X] = MkContraFlatMap[R, E, A, B, X](self) + final def contraFlatMap[X]: PartialContraFlatMap[R, E, A, B, X] = PartialContraFlatMap[R, E, A, B, X](self) /** * Transforms the input of the http before passing it on to the current Http @@ -112,7 +119,7 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => * Transforms the input of the http before giving it effectfully */ final def contramapZIO[R1 <: R, E1 >: E, X](xa: X => ZIO[R1, E1, A]): Http[R1, E1, X, B] = - Http.fromEffectFunction[X](xa) >>> self + Http.fromFunctionZIO[X](xa) >>> self /** * Named alias for `++` @@ -176,7 +183,14 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => * Transforms the output of the http effectfully */ final def mapZIO[R1 <: R, E1 >: E, C](bFc: B => ZIO[R1, E1, C]): Http[R1, E1, A, C] = - self >>> Http.fromEffectFunction(bFc) + self >>> Http.fromFunctionZIO(bFc) + + /** + * Named alias for @@ + */ + final def middleware[R1 <: R, E1 >: E, A1 <: A, B1 >: B, A2, B2]( + mid: Middleware[R1, E1, A1, B1, A2, B2], + ): Http[R1, E1, A2, B2] = mid(self) /** * Named alias for `<>` @@ -263,9 +277,9 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => h: ZIO[R1, E1, Any], ): Http[R1, E1, A, B] = tapAll( - e => Http.fromEffect(f(e)), - x => Http.fromEffect(g(x)), - Http.fromEffect(h), + e => Http.fromZIO(f(e)), + x => Http.fromZIO(g(x)), + Http.fromZIO(h), ) /** @@ -282,19 +296,19 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => * Returns an Http that effectfully peeks at the failure of this Http. */ final def tapErrorZIO[R1 <: R, E1 >: E](f: E => ZIO[R1, E1, Any]): Http[R1, E1, A, B] = - self.tapError(e => Http.fromEffect(f(e))) + self.tapError(e => Http.fromZIO(f(e))) /** * Returns an Http that effectfully peeks at the success of this Http. */ final def tapZIO[R1 <: R, E1 >: E](f: B => ZIO[R1, E1, Any]): Http[R1, E1, A, B] = - self.tap(v => Http.fromEffect(f(v))) + self.tap(v => Http.fromZIO(f(v))) /** * Unwraps an Http that returns a ZIO of Http */ final def unwrap[R1 <: R, E1 >: E, C](implicit ev: B <:< ZIO[R1, E1, C]): Http[R1, E1, A, C] = - self.flatMap(Http.fromEffect(_)) + self.flatMap(Http.fromZIO(_)) /** * Widens the type of the output @@ -317,14 +331,14 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => */ final private[zhttp] def execute(a: A): HExit[R, E, B] = self match { - case Http.Empty => HExit.empty - case Http.Identity => HExit.succeed(a.asInstanceOf[B]) - case Succeed(b) => HExit.succeed(b) - case Fail(e) => HExit.fail(e) - case FromEffectFunction(f) => HExit.effect(f(a)) - case Collect(pf) => if (pf.isDefinedAt(a)) HExit.succeed(pf(a)) else HExit.empty - case Chain(self, other) => self.execute(a).flatMap(b => other.execute(b)) - case Race(self, other) => + case Http.Empty => HExit.empty + case Http.Identity => HExit.succeed(a.asInstanceOf[B]) + case Succeed(b) => HExit.succeed(b) + case Fail(e) => HExit.fail(e) + case FromFunctionZIO(f) => HExit.fromZIO(f(a)) + case Collect(pf) => if (pf.isDefinedAt(a)) HExit.succeed(pf(a)) else HExit.empty + case Chain(self, other) => self.execute(a).flatMap(b => other.execute(b)) + case Race(self, other) => (self.execute(a), other.execute(a)) match { case (HExit.Effect(self), HExit.Effect(other)) => Http.fromOptionFunction[Any](_ => self.raceFirst(other)).execute(a) @@ -342,16 +356,6 @@ object Http { implicit final class HttpAppSyntax[-R, +E](val http: HttpApp[R, E]) extends HeaderModifier[HttpApp[R, E]] { self => - /** - * Attaches the provided middleware to the HttpApp - */ - def @@[R1 <: R, E1 >: E](mid: Middleware[R1, E1]): HttpApp[R1, E1] = middleware(mid) - - /** - * Attaches the provided middleware to the HttpApp - */ - def middleware[R1 <: R, E1 >: E](mid: Middleware[R1, E1]): HttpApp[R1, E1] = mid(http) - /** * Patches the response produced by the app */ @@ -406,14 +410,14 @@ object Http { /** * Creates an HTTP app which accepts a request and produces response. */ - def collect[A]: Http.MakeCollect[A] = Http.MakeCollect(()) + def collect[A]: Http.PartialCollect[A] = Http.PartialCollect(()) - def collectHttp[A]: Http.MakeCollectHttp[A] = Http.MakeCollectHttp(()) + def collectHttp[A]: Http.PartialCollectHttp[A] = Http.PartialCollectHttp(()) /** * Creates an HTTP app which accepts a request and produces response effectfully. */ - def collectZIO[A]: Http.MakeCollectZIO[A] = Http.MakeCollectZIO(()) + def collectZIO[A]: Http.PartialCollectZIO[A] = Http.PartialCollectZIO(()) /** * Combines multiple Http apps into one @@ -451,7 +455,7 @@ object Http { * Flattens an Http app of an that returns an effectful response */ def flattenZIO[R, E, A, B](http: Http[R, E, A, ZIO[R, E, B]]): Http[R, E, A, B] = - http.flatMap(Http.fromEffect) + http.flatMap(Http.fromZIO) /** * Creates an Http app that responds with 403 - Forbidden status code @@ -463,16 +467,6 @@ object Http { */ def fromData(data: HttpData): HttpApp[Any, Nothing] = response(Response(data = data)) - /** - * Converts a ZIO to an Http type - */ - def fromEffect[R, E, B](effect: ZIO[R, E, B]): Http[R, E, Any, B] = Http.fromEffectFunction(_ => effect) - - /** - * Creates an Http app from a function that returns a ZIO - */ - def fromEffectFunction[A]: Http.MakeFromEffectFunction[A] = Http.MakeFromEffectFunction(()) - /* * Creates an Http app from the contents of a file */ @@ -481,36 +475,37 @@ object Http { /** * Creates a Http from a pure function */ - def fromFunction[A]: FromFunction[A] = new FromFunction[A](()) + def fromFunction[A]: PartialFromFunction[A] = new PartialFromFunction[A](()) /** * Creates a Http from an effectful pure function */ - def fromFunctionZIO[A]: FromFunctionZIO[A] = new FromFunctionZIO[A](()) + def fromFunctionZIO[A]: PartialFromFunctionZIO[A] = new PartialFromFunctionZIO[A](()) /** * Creates an `Http` from a function that takes a value of type `A` and returns with a `ZIO[R, Option[E], B]`. The * returned effect can fail with a `None` to signal "not found" to the backend. */ - def fromOptionFunction[A]: FromOptionFunction[A] = new FromOptionFunction(()) + def fromOptionFunction[A]: PartialFromOptionFunction[A] = new PartialFromOptionFunction(()) /** * Creates a Http that always succeeds with a 200 status code and the provided ZStream as the body */ def fromStream[R](stream: ZStream[R, Throwable, String], charset: Charset = HTTP_CHARSET): HttpApp[R, Nothing] = Http - .fromEffect( - ZIO.environment[R].map(r => Http.fromData(HttpData.fromStream(stream.provideEnvironment(r), charset))), - ) + .fromZIO(ZIO.environment[R].map(r => Http.fromData(HttpData.fromStream(stream.provideEnvironment(r), charset)))) .flatten /** * Creates a Http that always succeeds with a 200 status code and the provided ZStream as the body */ def fromStream[R](stream: ZStream[R, Throwable, Byte]): HttpApp[R, Nothing] = - Http - .fromEffect(ZIO.environment[R].map(r => Http.fromData(HttpData.fromStream(stream.provideEnvironment(r))))) - .flatten + Http.fromZIO(ZIO.environment[R].map(r => Http.fromData(HttpData.fromStream(stream.provideEnvironment(r))))).flatten + + /** + * Converts a ZIO to an Http type + */ + def fromZIO[R, E, B](effect: ZIO[R, E, B]): Http[R, E, Any, B] = Http.fromFunctionZIO(_ => effect) /** * Creates an HTTP app which always responds with the provided Html page. @@ -541,12 +536,12 @@ object Http { /** * Converts a ZIO to an Http app type */ - def responseZIO[R, E](res: ZIO[R, E, Response]): HttpApp[R, E] = Http.fromEffect(res) + def responseZIO[R, E](res: ZIO[R, E, Response]): HttpApp[R, E] = Http.fromZIO(res) /** * Creates an Http that delegates to other Https. */ - def route[A]: Http.MakeRoute[A] = Http.MakeRoute(()) + def route[A]: Http.PartialRoute[A] = Http.PartialRoute(()) /** * Creates an HTTP app which always responds with the same status code and empty data. @@ -575,35 +570,31 @@ object Http { def tooLarge: HttpApp[Any, Nothing] = Http.status(Status.REQUEST_ENTITY_TOO_LARGE) // Ctor Help - final case class MakeCollectZIO[A](unit: Unit) extends AnyVal { + final case class PartialCollectZIO[A](unit: Unit) extends AnyVal { def apply[R, E, B](pf: PartialFunction[A, ZIO[R, E, B]]): Http[R, E, A, B] = - Http.collect[A] { case a if pf.isDefinedAt(a) => Http.fromEffect(pf(a)) }.flatten + Http.collect[A] { case a if pf.isDefinedAt(a) => Http.fromZIO(pf(a)) }.flatten } - final case class MakeCollect[A](unit: Unit) extends AnyVal { + final case class PartialCollect[A](unit: Unit) extends AnyVal { def apply[B](pf: PartialFunction[A, B]): Http[Any, Nothing, A, B] = Collect(pf) } - final case class MakeCollectHttp[A](unit: Unit) extends AnyVal { + final case class PartialCollectHttp[A](unit: Unit) extends AnyVal { def apply[R, E, B](pf: PartialFunction[A, Http[R, E, A, B]]): Http[R, E, A, B] = Http.collect[A](pf).flatten } - final case class MakeFromEffectFunction[A](unit: Unit) extends AnyVal { - def apply[R, E, B](f: A => ZIO[R, E, B]): Http[R, E, A, B] = Http.FromEffectFunction(f) - } - - final case class MakeRoute[A](unit: Unit) extends AnyVal { + final case class PartialRoute[A](unit: Unit) extends AnyVal { def apply[R, E, B](pf: PartialFunction[A, Http[R, E, A, B]]): Http[R, E, A, B] = Http.collect[A] { case r if pf.isDefinedAt(r) => pf(r) }.flatten } - final case class MkContraFlatMap[-R, +E, -A, +B, X](self: Http[R, E, A, B]) extends AnyVal { + final case class PartialContraFlatMap[-R, +E, -A, +B, X](self: Http[R, E, A, B]) extends AnyVal { def apply[R1 <: R, E1 >: E](xa: X => Http[R1, E1, Any, A]): Http[R1, E1, X, B] = Http.identity[X].flatMap(xa) >>> self } - final class FromOptionFunction[A](val unit: Unit) extends AnyVal { + final class PartialFromOptionFunction[A](val unit: Unit) extends AnyVal { def apply[R, E, B](f: A => ZIO[R, Option[E], B]): Http[R, E, A, B] = Http .collectZIO[A] { case a => f(a).map(Http.succeed(_)).catchAll { @@ -614,12 +605,12 @@ object Http { .flatten } - final class FromFunction[A](val unit: Unit) extends AnyVal { + final class PartialFromFunction[A](val unit: Unit) extends AnyVal { def apply[B](f: A => B): Http[Any, Nothing, A, B] = Http.identity[A].map(f) } - final class FromFunctionZIO[A](val unit: Unit) extends AnyVal { - def apply[R, E, B](f: A => ZIO[R, E, B]): Http[R, E, A, B] = Http.identity[A].mapZIO(f) + final class PartialFromFunctionZIO[A](val unit: Unit) extends AnyVal { + def apply[R, E, B](f: A => ZIO[R, E, B]): Http[R, E, A, B] = FromFunctionZIO(f) } private final case class Succeed[B](b: B) extends Http[Any, Nothing, Any, B] @@ -628,7 +619,7 @@ object Http { private final case class Fail[E](e: E) extends Http[Any, E, Any, Nothing] - private final case class FromEffectFunction[R, E, A, B](f: A => ZIO[R, E, B]) extends Http[R, E, A, B] + private final case class FromFunctionZIO[R, E, A, B](f: A => ZIO[R, E, B]) extends Http[R, E, A, B] private final case class Collect[R, E, A, B](ab: PartialFunction[A, B]) extends Http[R, E, A, B] diff --git a/zio-http/src/main/scala/zhttp/http/Middleware.scala b/zio-http/src/main/scala/zhttp/http/Middleware.scala index cafb42a70a..7a595df4ac 100644 --- a/zio-http/src/main/scala/zhttp/http/Middleware.scala +++ b/zio-http/src/main/scala/zhttp/http/Middleware.scala @@ -1,465 +1,361 @@ package zhttp.http -import io.netty.handler.codec.http.HttpHeaderNames -import io.netty.util.AsciiString.contentEqualsIgnoreCase -import zhttp.http.CORS.DefaultCORSConfig -import zhttp.http.Headers.BasicSchemeName -import zhttp.http.Middleware.{Flag, RequestP} +import zhttp.http.middleware.Web import zio._ -import java.io.IOException -import java.util.UUID - /** - * Middlewares for Http. + * Middlewares are essentially transformations that one can apply on any Http to produce a new one. They can modify + * requests and responses and also transform them into more concrete domain entities. + * + * You can think of middlewares as a functions — + * + * {{{ + * type Middleware[R, E, AIn, BIn, AOut, BOut] = Http[R, E, AIn, BIn] => Http[R, E, AOut, BOut] + * }}} + * + * The `AIn` and `BIn` type params represent the type params of the input Http. The `AOut` and `BOut` type params + * represent the type params of the output Http. */ -sealed trait Middleware[-R, +E] { self => - final def <>[R1 <: R, E1](other: Middleware[R1, E1]): Middleware[R1, E1] = - self orElse other - - final def ++[R1 <: R, E1 >: E](other: Middleware[R1, E1]): Middleware[R1, E1] = - self combine other - - final def apply[R1 <: R, E1 >: E](app: HttpApp[R1, E1]): HttpApp[R1, E1] = self.execute(app, Middleware.Flag()) - - final def as[R1 <: R, E1 >: E](app: HttpApp[R1, E1]): Middleware[R1, E1] = - Middleware.Constant(app) - - final def combine[R1 <: R, E1 >: E](other: Middleware[R1, E1]): Middleware[R1, E1] = - Middleware.Combine(self, other) - - final def delay(duration: Duration): Middleware[R with Clock, E] = { - self.modifyZIO((_, _, _) => UIO(self).delay(duration)) - } - - final def execute[R1 <: R, E1 >: E](app: HttpApp[R1, E1], flags: Flag): HttpApp[R1, E1] = - Middleware.execute(self, app, flags) - - final def modify[R1 <: R, E1 >: E](f: RequestP[Middleware[R1, E1]]): Middleware[R1, E1] = - Middleware.fromMiddlewareFunction((m, u, h) => f(m, u, h)) - - final def modifyZIO[R1 <: R, E1 >: E]( - f: RequestP[ZIO[R1, Option[E1], Middleware[R1, E1]]], - ): Middleware[R1, E1] = - Middleware.fromMiddlewareFunctionZIO((m, u, h) => f(m, u, h)) - - final def modifyHeaders(f: PartialFunction[Header, Header]): Middleware[R, E] = Middleware.modifyHeaders(f) - - final def orElse[R1 <: R, E1](other: Middleware[R1, E1]): Middleware[R1, E1] = - Middleware.OrElse(self, other) - - final def race[R1 <: R, E1 >: E](other: Middleware[R1, E1]): Middleware[R1, E1] = - Middleware.Race(self, other) - - final def setEmpty(flag: Boolean): Middleware[R, E] = Middleware.EmptyFlag(self, flag) - - final def when(f: RequestP[Boolean]): Middleware[R, E] = - modify((m, u, h) => if (f(m, u, h)) self else Middleware.identity) - - final def withEmpty: Middleware[R, E] = self.setEmpty(true) - - final def withoutEmpty: Middleware[R, E] = self.setEmpty(false) -} - -object Middleware { - - type RequestP[+A] = (Method, URL, Headers) => A +sealed trait Middleware[-R, +E, +AIn, -BIn, -AOut, +BOut] { self => /** - * Sets cookie in response headers + * Creates a new middleware that passes the output Http of the current middleware as the input to the provided + * middleware. */ - def addCookie(cookie: Cookie): Middleware[Any, Nothing] = - Middleware.addHeader(Headers.setCookie(cookie)) + final def >>>[R1 <: R, E1 >: E, AIn1 <: AOut, BIn1 >: BOut, AOut1, BOut1]( + other: Middleware[R1, E1, AIn1, BIn1, AOut1, BOut1], + ): Middleware[R1, E1, AIn, BIn, AOut1, BOut1] = self andThen other /** - * Adds the provided header and value to the response + * Applies self but if it fails, applies other. */ - def addHeader(name: String, value: String): Middleware[Any, Nothing] = - patch((_, _) => Patch.addHeader(name, value)) + final def <>[R1 <: R, E1, AIn0 >: AIn, BIn0 <: BIn, AOut0 <: AOut, BOut0 >: BOut]( + other: Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0], + ): Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0] = self orElse other /** - * Adds the provided header to the response + * Combines two middleware that don't modify the input and output types. */ - def addHeader(header: Headers): Middleware[Any, Nothing] = - patch((_, _) => Patch.addHeader(header)) + final def ++[R1 <: R, E1 >: E, A0 >: AIn <: AOut, B0 >: BOut <: BIn]( + other: Middleware[R1, E1, A0, B0, A0, B0], + ): Middleware[R1, E1, A0, B0, A0, B0] = + self combine other /** - * Adds the provided list of headers to the response + * Composes one middleware with another. */ - def addHeaders(headers: Headers): Middleware[Any, Nothing] = - patch((_, _) => Patch.addHeader(headers)) + final def andThen[R1 <: R, E1 >: E, AIn1 <: AOut, BIn1 >: BOut, AOut1, BOut1]( + other: Middleware[R1, E1, AIn1, BIn1, AOut1, BOut1], + ): Middleware[R1, E1, AIn, BIn, AOut1, BOut1] = Middleware.AndThen(self, other) /** - * Modifies the provided list of headers to the updated list of headers + * Applies middleware on Http and returns new Http. */ - def modifyHeaders(f: PartialFunction[Header, Header]): Middleware[Any, Nothing] = - patch((_, _) => Patch.updateHeaders(_.modify(f))) + final def apply[R1 <: R, E1 >: E](http: Http[R1, E1, AIn, BIn]): Http[R1, E1, AOut, BOut] = execute(http) /** - * Creates an authentication middleware that only allows authenticated requests to be passed on to the app. + * Makes the middleware resolve with a constant Middleware */ - def auth(verify: Headers => Boolean, responseHeaders: Headers = Headers.empty): Middleware[Any, Nothing] = - ifThenElse((_, _, h) => verify(h))( - Middleware.identity, - Middleware.Constant(Http.status(Status.FORBIDDEN).addHeaders(responseHeaders)), - ) + final def as[BOut0]( + bout: BOut0, + ): Middleware[R, E, AIn, BIn, AOut, BOut0] = + self.map(_ => bout) /** - * Creates a middleware for basic authentication + * Combines two middleware that operate on the same input and output types, into one. */ - def basicAuth(f: Header => Boolean): Middleware[Any, Nothing] = - auth( - _.getBasicAuthorizationCredentials match { - case Some(header) => f(header) - case None => false - }, - Headers(HttpHeaderNames.WWW_AUTHENTICATE, BasicSchemeName), - ) + final def combine[R1 <: R, E1 >: E, A0 >: AIn <: AOut, B0 >: BOut <: BIn]( + other: Middleware[R1, E1, A0, B0, A0, B0], + ): Middleware[R1, E1, A0, B0, A0, B0] = + self andThen other /** - * Creates a middleware for basic authentication that checks if the credentials are same as the ones given + * Preprocesses the incoming value for the outgoing Http. */ - def basicAuth(u: String, p: String): Middleware[Any, Nothing] = - basicAuth { case (user, password) => (user == u) && (password == p) } - - def addCookieM[R, E](cookie: ZIO[R, E, Cookie]): Middleware[R, E] = - patchZIO((_, _) => cookie.mapBoth(Option(_), c => Patch.addHeader(Headers.setCookie(c)))) + final def contramap[AOut0](f: AOut0 => AOut): Middleware[R, E, AIn, BIn, AOut0, BOut] = + self.contramapZIO[AOut0](a => UIO(f(a))) /** - * CSRF middlewares : To prevent Cross-site request forgery attacks. This middleware is modeled after the double - * submit cookie pattern. - * @see - * [[Middleware#csrfGenerate]] - Sets cookie with CSRF token - * @see - * [[Middleware#csrfValidate]] - Validate token value in request headers against value in cookies - * @see - * https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie + * Preprocesses the incoming value using a ZIO, for the outgoing Http. */ - - def csrfGenerate[R, E]( - tokenName: String = "x-csrf-token", - tokenGen: ZIO[R, Nothing, String] = UIO(UUID.randomUUID.toString), - ): Middleware[R, E] = - addCookieM(tokenGen.map(Cookie(tokenName, _))) - - def csrfValidate(tokenName: String = "x-csrf-token"): Middleware[Any, Nothing] = { - whenHeader( - headers => { - (headers.getHeaderValue(tokenName), headers.getCookieValue(tokenName)) match { - case (Some(headerValue), Some(cookieValue)) => headerValue != cookieValue - case _ => true - } - }, - Middleware.Constant(Http.status(Status.FORBIDDEN)), - ) - } + final def contramapZIO[AOut0]: Middleware.PartialContraMapZIO[R, E, AIn, BIn, AOut, BOut, AOut0] = + new Middleware.PartialContraMapZIO(self) /** - * Creates a middleware for Cross-Origin Resource Sharing (CORS). - * @see - * https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS + * Delays the production of Http output for the specified duration */ - def cors[R, E](config: CORSConfig = DefaultCORSConfig): Middleware[R, E] = { - def allowCORS(origin: Header, acrm: Method): Boolean = - (config.anyOrigin, config.anyMethod, origin._2.toString, acrm) match { - case (true, true, _, _) => true - case (true, false, _, acrm) => - config.allowedMethods.exists(_.contains(acrm)) - case (false, true, origin, _) => config.allowedOrigins(origin) - case (false, false, origin, acrm) => - config.allowedMethods.exists(_.contains(acrm)) && - config.allowedOrigins(origin) - } - def corsHeaders(origin: Header, method: Method, isPreflight: Boolean): Headers = { - Headers.ifThenElse(isPreflight)( - onTrue = config.allowedHeaders.fold(Headers.empty) { h => - Headers(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS.toString(), h.mkString(",")) - }, - onFalse = config.exposedHeaders.fold(Headers.empty) { h => - Headers(HttpHeaderNames.ACCESS_CONTROL_EXPOSE_HEADERS.toString(), h.mkString(",")) - }, - ) ++ - Headers(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.toString(), origin._2) ++ - Headers( - HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS.toString(), - config.allowedMethods.fold(method.toString())(m => m.map(m => m.toString()).mkString(",")), - ) ++ - Headers.when(config.allowCredentials) { - Headers(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, config.allowCredentials.toString) - } - } - - val existingRoutesWithHeaders = Middleware.make((method, _, headers) => { - ( - method, - headers.getHeader(HttpHeaderNames.ORIGIN), - ) match { - case (_, Some(origin)) if allowCORS(origin, method) => (Some(origin), method) - case _ => (None, method) - } - })((_, _, s) => { - s match { - case (Some(origin), method) => - Patch.addHeader(corsHeaders(origin, method, isPreflight = false)) - case _ => Patch.empty - } - }) - - val optionsHeaders = fromMiddlewareFunction { case (method, _, headers) => - ( - method, - headers.getHeader(HttpHeaderNames.ORIGIN), - headers.getHeader(HttpHeaderNames.ACCESS_CONTROL_REQUEST_METHOD), - ) match { - case (Method.OPTIONS, Some(origin), Some(acrm)) if allowCORS(origin, Method.fromString(acrm._2.toString)) => - fromApp( - ( - Http.succeed( - Response( - Status.NO_CONTENT, - headers = corsHeaders(origin, Method.fromString(acrm._2.toString), isPreflight = true), - ), - ), - ), - ) - case _ => identity - } - } - - existingRoutesWithHeaders orElse optionsHeaders - } + final def delay(duration: Duration): Middleware[R with Clock, E, AIn, BIn, AOut, BOut] = + self.mapZIO(b => UIO(b).delay(duration)) /** - * Add log status, method, url and time taken from req to res + * Creates a new Middleware from another */ - def debug: Middleware[Console with Clock, IOException] = - Middleware.makeZIO((method, url, _) => Clock.nanoTime.map(start => (method, url, start))) { - case (status, _, (method, url, start)) => - for { - end <- Clock.nanoTime - _ <- Console - .printLine(s"${status.asJava.code()} ${method} ${url.asString} ${(end - start) / 1000000}ms") - .mapError(Option(_)) - } yield Patch.empty - } + final def flatMap[R1 <: R, E1 >: E, AIn0 >: AIn, BIn0 <: BIn, AOut0 <: AOut, BOut0]( + f: BOut => Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0], + ): Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0] = + Middleware.FlatMap(self, f) /** - * Creates a middleware for signing cookies + * Flattens an Middleware of a Middleware */ - def signCookies(secret: String): Middleware[Any, Nothing] = - modifyHeaders { - case h if contentEqualsIgnoreCase(h._1, HeaderNames.setCookie) => - (HeaderNames.setCookie, Cookie.decodeResponseCookie(h._2.toString).get.sign(secret).encode) - } + final def flatten[R1 <: R, E1 >: E, AIn0 >: AIn, BIn0 <: BIn, AOut0 <: AOut, BOut0](implicit + ev: BOut <:< Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0], + ): Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0] = + flatMap(identity(_)) /** - * Creates a new constants middleware that always executes the app provided, independent of where the middleware is - * applied + * Transforms the output type of the current middleware. */ - def fromApp[R, E](app: HttpApp[R, E]): Middleware[R, E] = Middleware.Constant(app) + final def map[BOut0](f: BOut => BOut0): Middleware[R, E, AIn, BIn, AOut, BOut0] = + self.flatMap(b => Middleware.succeed(f(b))) /** - * Creates a new middleware using a function from request parameters to a HttpMiddleware + * Transforms the output type of the current middleware using effect function. */ - def fromMiddlewareFunction[R, E](f: RequestP[Middleware[R, E]]): Middleware[R, E] = - fromMiddlewareFunctionZIO((method, url, headers) => UIO(f(method, url, headers))) + final def mapZIO[R1 <: R, E1 >: E, BOut0](f: BOut => ZIO[R1, E1, BOut0]): Middleware[R1, E1, AIn, BIn, AOut, BOut0] = + self.flatMap(b => Middleware.fromHttp(Http.fromZIO(f(b)))) /** - * Creates a new middleware using a function from request parameters to a ZIO of HttpMiddleware + * Applies self but if it fails, applies other. */ - def fromMiddlewareFunctionZIO[R, E](f: RequestP[ZIO[R, Option[E], Middleware[R, E]]]): Middleware[R, E] = - Middleware.FromFunctionZIO(f) + final def orElse[R1 <: R, E1, AIn0 >: AIn, BIn0 <: BIn, AOut0 <: AOut, BOut0 >: BOut]( + other: Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0], + ): Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0] = + Middleware.OrElse(self, other) /** - * An empty middleware that doesn't do anything + * Race between current and other, cancels other when execution of one completes */ - def identity: Middleware[Any, Nothing] = Identity + final def race[R1 <: R, E1 >: E, AIn1 >: AIn, BIn1 <: BIn, AOut1 <: AOut, BOut1 >: BOut]( + other: Middleware[R1, E1, AIn1, BIn1, AOut1, BOut1], + ): Middleware[R1, E1, AIn1, BIn1, AOut1, BOut1] = + Middleware.Race(self, other) - /** - * Logical operator to decide which middleware to select based on the header - */ - def ifHeader[R, E](cond: Headers => Boolean)(left: Middleware[R, E], right: Middleware[R, E]): Middleware[R, E] = - ifThenElse((_, _, headers) => cond(headers))(left, right) + final def runAfter[R1 <: R, E1 >: E](effect: ZIO[R1, E1, Any]): Middleware[R1, E1, AIn, BIn, AOut, BOut] = + self.mapZIO(bOut => effect.as(bOut)) + + final def runBefore[R1 <: R, E1 >: E](effect: ZIO[R1, E1, Any]): Middleware[R1, E1, AIn, BIn, AOut, BOut] = + self.contramapZIO(b => effect.as(b)) /** - * Logical operator to decide which middleware to select based on the predicate. + * Applies Middleware based only if the condition function evaluates to true */ - def ifThenElse[R, E]( - cond: RequestP[Boolean], - )(left: Middleware[R, E], right: Middleware[R, E]): Middleware[R, E] = - Middleware.FromFunctionZIO((method, url, headers) => UIO(if (cond(method, url, headers)) left else right)) + final def when[AOut0 <: AOut](cond: AOut0 => Boolean): Middleware[R, E, AIn, BIn, AOut0, BOut] = + whenZIO(a => UIO(cond(a))) /** - * Logical operator to decide which middleware to select based on the predicate. + * Applies Middleware based only if the condition effectful function evaluates to true */ - def ifThenElseZIO[R, E]( - cond: RequestP[ZIO[R, E, Boolean]], - )(left: Middleware[R, E], right: Middleware[R, E]): Middleware[R, E] = - Middleware.FromFunctionZIO((method, url, headers) => - cond(method, url, headers).mapBoth( - Option(_), - { - case true => left - case false => right - }, - ), + final def whenZIO[R1 <: R, E1 >: E, AOut0 <: AOut]( + cond: AOut0 => ZIO[R1, E1, Boolean], + ): Middleware[R1, E1, AIn, BIn, AOut0, BOut] = + Middleware.ifThenElseZIO[AOut0](cond(_))( + isTrue = _ => self, + isFalse = _ => Middleware.identity, ) /** - * Creates a new middleware using transformation functions + * Applies Middleware and returns a transformed Http app */ - def make[S](req: (Method, URL, Headers) => S): PartiallyAppliedMake[S] = PartiallyAppliedMake(req) + private[zhttp] final def execute[R1 <: R, E1 >: E](http: Http[R1, E1, AIn, BIn]): Http[R1, E1, AOut, BOut] = + Middleware.execute(http, self) +} - /** - * Creates a new middleware using effectful transformation functions - */ - def makeZIO[R, E, S](req: (Method, URL, Headers) => ZIO[R, Option[E], S]): PartiallyAppliedMakeZIO[R, E, S] = - PartiallyAppliedMakeZIO(req) +object Middleware extends Web { /** - * Creates a middleware that produces a Patch for the Response + * Creates a middleware using specified encoder and decoder */ - def patch[R, E](f: (Status, Headers) => Patch): Middleware[R, E] = - Middleware.make((_, _, _) => ())((status, headers, _) => f(status, headers)) + def codec[A, B]: PartialCodec[A, B] = new PartialCodec[A, B](()) /** - * Creates a middleware that produces a Patch for the Response effectfully. + * Creates a middleware using specified effectful encoder and decoder */ - def patchZIO[R, E](f: (Status, Headers) => ZIO[R, Option[E], Patch]): Middleware[R, E] = - Middleware.makeZIO((_, _, _) => ZIO.unit)((status, headers, _) => f(status, headers)) + def codecZIO[A, B]: PartialCodecZIO[A, B] = new PartialCodecZIO[A, B](()) /** - * Removes the header by name + * Creates a middleware using specified function */ - def removeHeader(name: String): Middleware[Any, Nothing] = - patch((_, _) => Patch.removeHeaders(List(name))) + def collect[A]: PartialCollect[A] = new PartialCollect[A](()) /** - * Runs the effect after the response is produced + * Creates a middleware using specified effect function */ - def runAfter[R, E](effect: ZIO[R, E, Any]): Middleware[R, E] = - patchZIO((_, _) => effect.mapBoth(Option(_), _ => Patch.empty)) + def collectZIO[A]: PartialCollectZIO[A] = new PartialCollectZIO[A](()) /** - * Runs the effect before the request is passed on to the HttpApp on which the middleware is applied. + * Creates a middleware which always fail with specified error */ - def runBefore[R, E](effect: ZIO[R, E, Any]): Middleware[R, E] = - Middleware.makeZIO((_, _, _) => effect.mapError(Option(_)).unit)((_, _, _) => UIO(Patch.empty)) + def fail[E](e: E): Middleware[Any, E, Nothing, Any, Any, Nothing] = Fail(e) /** - * Creates a new middleware that always sets the response status to the provided value + * Creates a middleware with specified http App */ - def status(status: Status): Middleware[Any, Nothing] = Middleware.patch((_, _) => Patch.setStatus(status)) + def fromHttp[R, E, A, B](http: Http[R, E, A, B]): Middleware[R, E, Nothing, Any, A, B] = Constant(http) /** - * Times out the application with a 408 status code. + * An empty middleware that doesn't do anything */ - def timeout(duration: Duration): Middleware[Clock, Nothing] = - Middleware.identity.race(Middleware.fromApp(Http.status(Status.REQUEST_TIMEOUT).delayAfter(duration))) + def identity: Middleware[Any, Nothing, Nothing, Any, Any, Nothing] = Middleware.Identity /** - * Applies the middleware only if the condition function evaluates to true + * Logical operator to decide which middleware to select based on the predicate. */ - def when[R, E](cond: RequestP[Boolean])(middleware: Middleware[R, E]): Middleware[R, E] = - ifThenElse(cond)(middleware, Middleware.identity) + def ifThenElse[A]: PartialIfThenElse[A] = new PartialIfThenElse(()) /** - * Applies the middleware only when the condition for the headers are true + * Logical operator to decide which middleware to select based on the predicate effect. */ - def whenHeader[R, E](cond: Headers => Boolean, other: Middleware[R, E]): Middleware[R, E] = - when((_, _, headers) => cond(headers))(other) + def ifThenElseZIO[A]: PartialIfThenElseZIO[A] = new PartialIfThenElseZIO(()) /** - * Switches control to the app only when the condition for the headers are true + * Creates a new middleware using transformation functions */ - def whenHeader[R, E](cond: Headers => Boolean, other: HttpApp[R, E]): Middleware[R, E] = - when((_, _, headers) => cond(headers))(Middleware.fromApp(other)) + def intercept[A, B]: PartialIntercept[A, B] = new PartialIntercept[A, B](()) /** - * Applies the middleware only if the condition function effectfully evaluates to true + * Creates a new middleware using effectful transformation functions */ - def whenZIO[R, E](cond: RequestP[ZIO[R, E, Boolean]])(middleware: Middleware[R, E]): Middleware[R, E] = - ifThenElseZIO(cond)(middleware, Middleware.identity) + def interceptZIO[A, B]: PartialInterceptZIO[A, B] = new PartialInterceptZIO[A, B](()) /** - * Applies the middleware on an HttpApp + * Creates a middleware which always succeed with specified value */ - private[zhttp] def execute[R, E](mid: Middleware[R, E], app: HttpApp[R, E], flag: Flag): HttpApp[R, E] = - mid match { - case Identity => app + def succeed[B](b: B): Middleware[Any, Nothing, Nothing, Any, Any, B] = fromHttp(Http.succeed(b)) - case EmptyFlag(mid, status) => - execute(mid, app, flag.copy(withEmpty = status)) - - case TransformZIO(reqF, resF) => - Http.fromOptionFunction { req => + private[zhttp] def execute[R, E, AIn, BIn, AOut, BOut]( + http: Http[R, E, AIn, BIn], + self: Middleware[R, E, AIn, BIn, AOut, BOut], + ): Http[R, E, AOut, BOut] = + self match { + case Identity => http.asInstanceOf[Http[R, E, AOut, BOut]] + case Constant(http) => http + case OrElse(self, other) => self.execute(http).orElse(other.execute(http)) + case Fail(error) => Http.fail(error) + case AndThen(self, other) => other.execute(self.execute(http)) + case FlatMap(self, f) => self.execute(http).flatMap(f(_).execute(http)) + case ContraMapZIO(self, f) => self.execute(http).contramapZIO(a => f(a)) + case Race(self, other) => self.execute(http) race other.execute(http) + case Intercept(incoming, outgoing) => + Http.fromOptionFunction[AOut] { a => for { - s <- reqF(req.method, req.url, req.getHeaders) - res <- - if (flag.withEmpty) app(req).catchSome { case None => UIO(Response.status(Status.NOT_FOUND)) } - else app(req) - patch <- resF(res.status, res.getHeaders, s) - } yield patch(res) + s <- incoming(a) + b <- http(a.asInstanceOf[AIn]) + c <- outgoing(b, s) + } yield c.asInstanceOf[BOut] } + } - case Combine(self, other) => other.execute(self.execute(app, flag), flag) + final class PartialCollect[AOut](val unit: Unit) extends AnyVal { + def apply[R, E, AIn, BIn, BOut]( + f: PartialFunction[AOut, Middleware[R, E, AIn, BIn, AOut, BOut]], + ): Middleware[R, E, AIn, BIn, AOut, BOut] = + Middleware.fromHttp(Http.collect[AOut] { case aout if f.isDefinedAt(aout) => f(aout) }).flatten + } - case FromFunctionZIO(reqF) => - Http.fromOptionFunction { req => - for { - output <- reqF(req.method, req.url, req.getHeaders) - res <- output.execute(app, flag)(req) - } yield res - } + final class PartialCollectZIO[AOut](val unit: Unit) extends AnyVal { + def apply[R, E, AIn, BIn, BOut]( + f: PartialFunction[AOut, ZIO[R, E, Middleware[R, E, AIn, BIn, AOut, BOut]]], + ): Middleware[R, E, AIn, BIn, AOut, BOut] = + Middleware.fromHttp(Http.collectZIO[AOut] { case aout if f.isDefinedAt(aout) => f(aout) }).flatten + } - case Race(self, other) => - Http.fromOptionFunction { req => - self.execute(app, flag)(req) raceFirst other.execute(app, flag)(req) - } + final class PartialIntercept[A, B](val unit: Unit) extends AnyVal { + def apply[S, BOut](incoming: A => S)(outgoing: (B, S) => BOut): Middleware[Any, Nothing, A, B, A, BOut] = + interceptZIO[A, B](a => UIO(incoming(a)))((b, s) => UIO(outgoing(b, s))) + } + + final class PartialInterceptZIO[A, B](val unit: Unit) extends AnyVal { + def apply[R, E, S, BOut]( + incoming: A => ZIO[R, Option[E], S], + ): PartialInterceptOutgoingZIO[R, E, A, S, B] = + new PartialInterceptOutgoingZIO(incoming) + } - case Constant(self) => self + final class PartialInterceptOutgoingZIO[-R, +E, A, +S, B](val incoming: A => ZIO[R, Option[E], S]) extends AnyVal { + def apply[R1 <: R, E1 >: E, BOut]( + outgoing: (B, S) => ZIO[R1, Option[E1], BOut], + ): Middleware[R1, E1, A, B, A, BOut] = + Intercept(incoming, outgoing) + } - case OrElse(self, other) => - Http.fromOptionFunction { req => - (self.execute(app, flag)(req) orElse other.execute(app, flag)(req)) - .asInstanceOf[ZIO[R, Option[E], Response]] - } - } + final class PartialCodec[AOut, BIn](val unit: Unit) extends AnyVal { + def apply[E, AIn, BOut]( + decoder: AOut => Either[E, AIn], + encoder: BIn => Either[E, BOut], + ): Middleware[Any, E, AIn, BIn, AOut, BOut] = + Middleware.identity.mapZIO((b: BIn) => ZIO.fromEither(encoder(b))).contramapZIO(a => ZIO.fromEither(decoder(a))) + } - final case class Flag(withEmpty: Boolean = false) + final class PartialIfThenElse[AOut](val unit: Unit) extends AnyVal { + def apply[R, E, AIn, BIn, BOut](cond: AOut => Boolean)( + isTrue: AOut => Middleware[R, E, AIn, BIn, AOut, BOut], + isFalse: AOut => Middleware[R, E, AIn, BIn, AOut, BOut], + ): Middleware[R, E, AIn, BIn, AOut, BOut] = + Middleware + .fromHttp(Http.fromFunction[AOut] { a => if (cond(a)) isTrue(a) else isFalse(a) }) + .flatten + } + + final class PartialIfThenElseZIO[AOut](val unit: Unit) extends AnyVal { + def apply[R, E, AIn, BIn, BOut](cond: AOut => ZIO[R, E, Boolean])( + isTrue: AOut => Middleware[R, E, AIn, BIn, AOut, BOut], + isFalse: AOut => Middleware[R, E, AIn, BIn, AOut, BOut], + ): Middleware[R, E, AIn, BIn, AOut, BOut] = + Middleware + .fromHttp(Http.fromFunctionZIO[AOut] { a => cond(a).map(b => if (b) isTrue(a) else isFalse(a)) }) + .flatten + } - final case class PartiallyAppliedMake[S](req: (Method, URL, Headers) => S) extends AnyVal { - def apply(res: (Status, Headers, S) => Patch): Middleware[Any, Nothing] = - TransformZIO[Any, Nothing, S]( - (method, url, headers) => UIO(req(method, url, headers)), - (status, headers, state) => UIO(res(status, headers, state)), - ) + final class PartialCodecZIO[AOut, BIn](val unit: Unit) extends AnyVal { + def apply[R, E, AIn, BOut]( + decoder: AOut => ZIO[R, E, AIn], + encoder: BIn => ZIO[R, E, BOut], + ): Middleware[R, E, AIn, BIn, AOut, BOut] = + Middleware.identity.mapZIO(encoder).contramapZIO(decoder) } - final case class PartiallyAppliedMakeZIO[R, E, S](req: (Method, URL, Headers) => ZIO[R, Option[E], S]) - extends AnyVal { - def apply[R1 <: R, E1 >: E](res: (Status, Headers, S) => ZIO[R1, Option[E1], Patch]): Middleware[R1, E1] = - TransformZIO(req, res) + final class PartialContraMapZIO[-R, +E, +AIn, -BIn, -AOut, +BOut, AOut0]( + val self: Middleware[R, E, AIn, BIn, AOut, BOut], + ) extends AnyVal { + def apply[R1 <: R, E1 >: E](f: AOut0 => ZIO[R1, E1, AOut]): Middleware[R1, E1, AIn, BIn, AOut0, BOut] = + ContraMapZIO[R1, E1, AIn, BIn, AOut, BOut, AOut0](self, f) } - private final case class EmptyFlag[R, E](mid: Middleware[R, E], status: Boolean) extends Middleware[R, E] + private final case class Fail[E](error: E) extends Middleware[Any, E, Nothing, Any, Any, Nothing] + + private final case class OrElse[R, E0, E1, AIn, BIn, AOut, BOut]( + self: Middleware[R, E0, AIn, BIn, AOut, BOut], + other: Middleware[R, E1, AIn, BIn, AOut, BOut], + ) extends Middleware[R, E1, AIn, BIn, AOut, BOut] - private final case class TransformZIO[R, E, S]( - req: (Method, URL, Headers) => ZIO[R, Option[E], S], - res: (Status, Headers, S) => ZIO[R, Option[E], Patch], - ) extends Middleware[R, E] + private final case class Constant[R, E, AOut, BOut](http: Http[R, E, AOut, BOut]) + extends Middleware[R, E, Nothing, Any, AOut, BOut] - private final case class Combine[R, E](self: Middleware[R, E], other: Middleware[R, E]) extends Middleware[R, E] + private final case class Intercept[R, E, A, B, S, BOut]( + incoming: A => ZIO[R, Option[E], S], + outgoing: (B, S) => ZIO[R, Option[E], BOut], + ) extends Middleware[R, E, A, B, A, BOut] - private final case class FromFunctionZIO[R, E]( - f: (Method, URL, Headers) => ZIO[R, Option[E], Middleware[R, E]], - ) extends Middleware[R, E] + private final case class AndThen[R, E, A0, B0, A1, B1, A2, B2]( + self: Middleware[R, E, A0, B0, A1, B1], + other: Middleware[R, E, A1, B1, A2, B2], + ) extends Middleware[R, E, A0, B0, A2, B2] - private final case class Race[R, E](self: Middleware[R, E], other: Middleware[R, E]) extends Middleware[R, E] + private final case class FlatMap[R, E, AIn, BIn, AOut, BOut, BOut0]( + self: Middleware[R, E, AIn, BIn, AOut, BOut0], + f: BOut0 => Middleware[R, E, AIn, BIn, AOut, BOut], + ) extends Middleware[R, E, AIn, BIn, AOut, BOut] - private final case class Constant[R, E](app: HttpApp[R, E]) extends Middleware[R, E] + private final case class ContraMapZIO[R, E, AIn, BIn, AOut, BOut, AOut0]( + self: Middleware[R, E, AIn, BIn, AOut, BOut], + f: AOut0 => ZIO[R, E, AOut], + ) extends Middleware[R, E, AIn, BIn, AOut0, BOut] - private final case class OrElse[R, E](self: Middleware[R, Any], other: Middleware[R, E]) extends Middleware[R, E] + private final case class Race[R, E, AIn, BIn, AOut, BOut]( + self: Middleware[R, E, AIn, BIn, AOut, BOut], + other: Middleware[R, E, AIn, BIn, AOut, BOut], + ) extends Middleware[R, E, AIn, BIn, AOut, BOut] - private case object Identity extends Middleware[Any, Nothing] + private case object Identity extends Middleware[Any, Nothing, Nothing, Any, Any, Nothing] } diff --git a/zio-http/src/main/scala/zhttp/http/Response.scala b/zio-http/src/main/scala/zhttp/http/Response.scala index 60fb7939f0..928c6875df 100644 --- a/zio-http/src/main/scala/zhttp/http/Response.scala +++ b/zio-http/src/main/scala/zhttp/http/Response.scala @@ -76,7 +76,7 @@ final case class Response private ( val jHeaders = self.getHeaders.encode val jContent = self.data match { - case HttpData.Text(text, charset) => Unpooled.copiedBuffer(text, charset) + case HttpData.Text(text, charset) => Unpooled.wrappedBuffer(text.getBytes(charset)) case HttpData.BinaryChunk(data) => Unpooled.copiedBuffer(data.toArray) case HttpData.BinaryByteBuf(data) => data case HttpData.BinaryStream(_) => null diff --git a/zio-http/src/main/scala/zhttp/http/headers/HeaderChecks.scala b/zio-http/src/main/scala/zhttp/http/headers/HeaderChecks.scala index 139d821a4e..bee353ed92 100644 --- a/zio-http/src/main/scala/zhttp/http/headers/HeaderChecks.scala +++ b/zio-http/src/main/scala/zhttp/http/headers/HeaderChecks.scala @@ -18,7 +18,7 @@ trait HeaderChecks[+A] { self: HeaderExtension[A] with A => final def hasHeader(name: CharSequence, value: CharSequence): Boolean = getHeaderValue(name) match { - case Some(v1) => v1 == value + case Some(v1) => v1.contentEquals(value) case None => false } diff --git a/zio-http/src/main/scala/zhttp/http/middleware/Auth.scala b/zio-http/src/main/scala/zhttp/http/middleware/Auth.scala new file mode 100644 index 0000000000..1cd9bc5551 --- /dev/null +++ b/zio-http/src/main/scala/zhttp/http/middleware/Auth.scala @@ -0,0 +1,38 @@ +package zhttp.http.middleware + +import io.netty.handler.codec.http.HttpHeaderNames +import zhttp.http.Headers.BasicSchemeName +import zhttp.http._ + +private[zhttp] trait Auth { + + /** + * Creates a middleware for basic authentication + */ + final def basicAuth(f: Header => Boolean): HttpMiddleware[Any, Nothing] = + customAuth( + _.getBasicAuthorizationCredentials match { + case Some(header) => f(header) + case None => false + }, + Headers(HttpHeaderNames.WWW_AUTHENTICATE, BasicSchemeName), + ) + + /** + * Creates a middleware for basic authentication that checks if the credentials are same as the ones given + */ + final def basicAuth(u: String, p: String): HttpMiddleware[Any, Nothing] = + basicAuth { case (user, password) => (user == u) && (password == p) } + + /** + * Creates an authentication middleware that only allows authenticated requests to be passed on to the app. + */ + final def customAuth( + verify: Headers => Boolean, + responseHeaders: Headers = Headers.empty, + ): HttpMiddleware[Any, Nothing] = + Middleware.ifThenElse[Request](req => verify(req.getHeaders))( + _ => Middleware.identity, + _ => Middleware.fromHttp(Http.status(Status.FORBIDDEN).addHeaders(responseHeaders)), + ) +} diff --git a/zio-http/src/main/scala/zhttp/http/middleware/Cors.scala b/zio-http/src/main/scala/zhttp/http/middleware/Cors.scala new file mode 100644 index 0000000000..22ced6ab15 --- /dev/null +++ b/zio-http/src/main/scala/zhttp/http/middleware/Cors.scala @@ -0,0 +1,76 @@ +package zhttp.http.middleware + +import io.netty.handler.codec.http.HttpHeaderNames +import zhttp.http._ +import zhttp.http.middleware.Cors.CorsConfig + +private[zhttp] trait Cors { + + /** + * Creates a middleware for Cross-Origin Resource Sharing (CORS). + * @see + * https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS + */ + final def cors[R, E](config: CorsConfig = CorsConfig()): HttpMiddleware[R, E] = { + def allowCORS(origin: Header, acrm: Method): Boolean = + (config.anyOrigin, config.anyMethod, origin._2.toString, acrm) match { + case (true, true, _, _) => true + case (true, false, _, acrm) => + config.allowedMethods.exists(_.contains(acrm)) + case (false, true, origin, _) => config.allowedOrigins(origin) + case (false, false, origin, acrm) => + config.allowedMethods.exists(_.contains(acrm)) && + config.allowedOrigins(origin) + } + def corsHeaders(origin: Header, method: Method, isPreflight: Boolean): Headers = { + Headers.ifThenElse(isPreflight)( + onTrue = config.allowedHeaders.fold(Headers.empty) { h => + Headers(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS.toString(), h.mkString(",")) + }, + onFalse = config.exposedHeaders.fold(Headers.empty) { h => + Headers(HttpHeaderNames.ACCESS_CONTROL_EXPOSE_HEADERS.toString(), h.mkString(",")) + }, + ) ++ + Headers(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.toString(), origin._2) ++ + Headers( + HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS.toString(), + config.allowedMethods.fold(method.toString())(m => m.map(m => m.toString()).mkString(",")), + ) ++ + Headers.when(config.allowCredentials) { + Headers(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, config.allowCredentials.toString) + } + } + Middleware.collect[Request] { case req => + ( + req.method, + req.getHeaders.getHeader(HttpHeaderNames.ORIGIN), + req.getHeaders.getHeader(HttpHeaderNames.ACCESS_CONTROL_REQUEST_METHOD), + ) match { + case (Method.OPTIONS, Some(origin), Some(acrm)) if allowCORS(origin, Method.fromString(acrm._2.toString)) => + Middleware.succeed( + Response( + Status.NO_CONTENT, + headers = corsHeaders(origin, Method.fromString(acrm._2.toString), isPreflight = true), + ), + ) + case (_, Some(origin), _) if allowCORS(origin, req.method) => + Middleware.addHeaders(corsHeaders(origin, req.method, isPreflight = false)) + case _ => Middleware.identity + } + } + } +} + +object Cors { + final case class CorsConfig( + anyOrigin: Boolean = true, + anyMethod: Boolean = true, + allowCredentials: Boolean = true, + allowedOrigins: String => Boolean = _ => false, + allowedMethods: Option[Set[Method]] = None, + allowedHeaders: Option[Set[String]] = Some( + Set(HttpHeaderNames.CONTENT_TYPE.toString, HttpHeaderNames.AUTHORIZATION.toString, "*"), + ), + exposedHeaders: Option[Set[String]] = Some(Set("*")), + ) +} diff --git a/zio-http/src/main/scala/zhttp/http/middleware/Csrf.scala b/zio-http/src/main/scala/zhttp/http/middleware/Csrf.scala new file mode 100644 index 0000000000..6935699780 --- /dev/null +++ b/zio-http/src/main/scala/zhttp/http/middleware/Csrf.scala @@ -0,0 +1,44 @@ +package zhttp.http.middleware + +import zhttp.http._ +import zio.{UIO, ZIO} + +import java.util.UUID + +private[zhttp] trait Csrf { + + /** + * Generates a new CSRF token that can be validated using the csrfValidate middleware. + * + * CSRF middlewares: To prevent Cross-site request forgery attacks. This middleware is modeled after the double submit + * cookie pattern. Used in conjunction with [[#csrfValidate]] middleware. + * + * https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie + */ + final def csrfGenerate[R, E]( + tokenName: String = "x-csrf-token", + tokenGen: ZIO[R, Nothing, String] = UIO(UUID.randomUUID.toString), + ): HttpMiddleware[R, E] = + Middleware.addCookieZIO(tokenGen.map(Cookie(tokenName, _))) + + /** + * Validates the CSRF token appearing in the request headers. Typically the token should be set using the + * `csrfGenerate` middleware. + * + * CSRF middlewares : To prevent Cross-site request forgery attacks. This middleware is modeled after the double + * submit cookie pattern. Used in conjunction with [[#csrfGenerate]] middleware + * + * https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie + */ + def csrfValidate(tokenName: String = "x-csrf-token"): HttpMiddleware[Any, Nothing] = { + Middleware.whenHeader( + headers => { + (headers.getHeaderValue(tokenName), headers.getCookieValue(tokenName)) match { + case (Some(headerValue), Some(cookieValue)) => headerValue != cookieValue + case _ => true + } + }, + Middleware.succeed(Response.status(Status.FORBIDDEN)), + ) + } +} diff --git a/zio-http/src/main/scala/zhttp/http/middleware/Web.scala b/zio-http/src/main/scala/zhttp/http/middleware/Web.scala new file mode 100644 index 0000000000..533d4111e9 --- /dev/null +++ b/zio-http/src/main/scala/zhttp/http/middleware/Web.scala @@ -0,0 +1,183 @@ +package zhttp.http.middleware + +import zhttp.http._ +import zhttp.http.headers.HeaderModifier +import zhttp.http.middleware.Web.{PartialInterceptPatch, PartialInterceptZIOPatch} +import zio._ + +import java.io.IOException + +/** + * Middlewares on an HttpApp + */ +private[zhttp] trait Web extends Cors with Csrf with Auth with HeaderModifier[HttpMiddleware[Any, Nothing]] { + self => + + /** + * Updates the provided list of headers to the response + */ + final override def updateHeaders(update: Headers => Headers): HttpMiddleware[Any, Nothing] = + Middleware.updateResponse(_.updateHeaders(update)) + + /** + * Sets cookie in response headers + */ + final def addCookie(cookie: Cookie): HttpMiddleware[Any, Nothing] = + self.withSetCookie(cookie) + + final def addCookieZIO[R, E](cookie: ZIO[R, E, Cookie]): HttpMiddleware[R, E] = + patchZIO(_ => cookie.mapBoth(Option(_), c => Patch.addHeader(Headers.setCookie(c)))) + + /** + * Add log status, method, url and time taken from req to res + */ + final def debug: HttpMiddleware[Console with Clock, IOException] = + interceptZIOPatch(req => Clock.nanoTime.map(start => (req.method, req.url, start))) { + case (response, (method, url, start)) => + for { + end <- Clock.nanoTime + _ <- Console + .printLine(s"${response.status.asJava.code()} ${method} ${url.asString} ${(end - start) / 1000000}ms") + .mapError(Option(_)) + } yield Patch.empty + } + + /** + * Logical operator to decide which middleware to select based on the header + */ + final def ifHeaderThenElse[R, E]( + cond: Headers => Boolean, + )(left: HttpMiddleware[R, E], right: HttpMiddleware[R, E]): HttpMiddleware[R, E] = + Middleware.ifThenElse[Request](req => cond(req.getHeaders))(_ => left, _ => right) + + /** + * Logical operator to decide which middleware to select based on the method. + */ + final def ifMethodThenElse[R, E]( + cond: Method => Boolean, + )(left: HttpMiddleware[R, E], right: HttpMiddleware[R, E]): HttpMiddleware[R, E] = + Middleware.ifThenElse[Request](req => cond(req.method))(_ => left, _ => right) + + /** + * Logical operator to decide which middleware to select based on the predicate. + */ + final def ifRequestThenElse[R, E]( + cond: Request => Boolean, + )(left: HttpMiddleware[R, E], right: HttpMiddleware[R, E]): HttpMiddleware[R, E] = + Middleware.ifThenElse[Request](cond)(_ => left, _ => right) + + /** + * Logical operator to decide which middleware to select based on the predicate. + */ + final def ifRequestThenElseZIO[R, E]( + cond: Request => ZIO[R, E, Boolean], + )(left: HttpMiddleware[R, E], right: HttpMiddleware[R, E]): HttpMiddleware[R, E] = + Middleware.ifThenElseZIO[Request](cond)(_ => left, _ => right) + + /** + * Creates a new middleware using transformation functions + */ + final def interceptPatch[S](req: Request => S): PartialInterceptPatch[S] = PartialInterceptPatch(req) + + /** + * Creates a new middleware using effectful transformation functions + */ + final def interceptZIOPatch[R, E, S](req: Request => ZIO[R, Option[E], S]): PartialInterceptZIOPatch[R, E, S] = + PartialInterceptZIOPatch(req) + + /** + * Creates a middleware that produces a Patch for the Response + */ + final def patch[R, E](f: Response => Patch): HttpMiddleware[R, E] = + Middleware.interceptPatch(_ => ())((res, _) => f(res)) + + /** + * Creates a middleware that produces a Patch for the Response effectfully. + */ + final def patchZIO[R, E](f: Response => ZIO[R, Option[E], Patch]): HttpMiddleware[R, E] = + Middleware.interceptZIOPatch(_ => ZIO.unit)((res, _) => f(res)) + + /** + * Runs the effect after the middleware is applied + */ + final def runAfter[R, E](effect: ZIO[R, E, Any]): HttpMiddleware[R, E] = + Middleware.interceptZIO[Request, Response](_ => ZIO.unit)((res, _) => effect.mapBoth(Option(_), _ => res)) + + /** + * Runs the effect before the request is passed on to the HttpApp on which the middleware is applied. + */ + final def runBefore[R, E](effect: ZIO[R, E, Any]): HttpMiddleware[R, E] = + Middleware.interceptZIOPatch(_ => effect.mapError(Option(_)).unit)((_, _) => UIO(Patch.empty)) + + /** + * Creates a new middleware that always sets the response status to the provided value + */ + final def setStatus(status: Status): HttpMiddleware[Any, Nothing] = patch(_ => Patch.setStatus(status)) + + /** + * Creates a middleware for signing cookies + */ + final def signCookies(secret: String): HttpMiddleware[Any, Nothing] = + updateHeaders { + case h if h.getHeader(HeaderNames.setCookie).isDefined => + Headers( + HeaderNames.setCookie, + Cookie.decodeResponseCookie(h.getHeader(HeaderNames.setCookie).get._2.toString).get.sign(secret).encode, + ) + case h => h + } + + /** + * Times out the application with a 408 status code. + */ + final def timeout(duration: Duration): HttpMiddleware[Clock, Nothing] = + Middleware.identity.race(Middleware.fromHttp(Http.status(Status.REQUEST_TIMEOUT).delayAfter(duration))) + + /** + * Creates a middleware that updates the response produced + */ + final def updateResponse[R, E](f: Response => Response): HttpMiddleware[R, E] = + Middleware.intercept[Request, Response](_ => ())((res, _) => f(res)) + + /** + * Applies the middleware only when the condition for the headers are true + */ + final def whenHeader[R, E](cond: Headers => Boolean, middleware: HttpMiddleware[R, E]): HttpMiddleware[R, E] = + middleware.when[Request](req => cond(req.getHeaders)) + + /** + * Applies the middleware only if the condition function evaluates to true + */ + final def whenRequest[R, E](cond: Request => Boolean)( + middleware: HttpMiddleware[R, E], + ): HttpMiddleware[R, E] = + middleware.when[Request](cond) + + /** + * Applies the middleware only if the condition function effectfully evaluates to true + */ + final def whenRequestZIO[R, E]( + cond: Request => ZIO[R, E, Boolean], + )(middleware: HttpMiddleware[R, E]): HttpMiddleware[R, E] = + Middleware.ifThenElseZIO[Request](cond)( + _ => middleware, + _ => Middleware.identity, + ) +} + +object Web { + + final case class PartialInterceptPatch[S](req: Request => S) extends AnyVal { + def apply(res: (Response, S) => Patch): HttpMiddleware[Any, Nothing] = { + Middleware.intercept[Request, Response](req(_))((response, state) => res(response, state)(response)) + } + } + + final case class PartialInterceptZIOPatch[R, E, S](req: Request => ZIO[R, Option[E], S]) extends AnyVal { + def apply[R1 <: R, E1 >: E](res: (Response, S) => ZIO[R1, Option[E1], Patch]): HttpMiddleware[R1, E1] = + Middleware + .interceptZIO[Request, Response](req(_))((response, state) => + res(response, state).map(patch => patch(response)), + ) + } +} diff --git a/zio-http/src/main/scala/zhttp/http/middleware/package.scala b/zio-http/src/main/scala/zhttp/http/middleware/package.scala new file mode 100644 index 0000000000..3baa3d9fc4 --- /dev/null +++ b/zio-http/src/main/scala/zhttp/http/middleware/package.scala @@ -0,0 +1,5 @@ +package zhttp.http + +package object middleware { + type HttpMiddleware[-R, +E] = Middleware[R, E, Request, Response, Request, Response] +} diff --git a/zio-http/src/main/scala/zhttp/http/package.scala b/zio-http/src/main/scala/zhttp/http/package.scala index 7c6adc1c2d..a3673941c6 100644 --- a/zio-http/src/main/scala/zhttp/http/package.scala +++ b/zio-http/src/main/scala/zhttp/http/package.scala @@ -6,13 +6,14 @@ import zio.ZIO import java.nio.charset.Charset package object http extends PathModule with RequestSyntax with RouteDecoderModule { - type HttpApp[-R, +E] = Http[R, E, Request, Response] - type UHttpApp = HttpApp[Any, Nothing] - type RHttpApp[-R] = HttpApp[R, Throwable] - type UHttp[-A, +B] = Http[Any, Nothing, A, B] - type SilentResponse[-E] = CanBeSilenced[E, Response] - type ResponseZIO[-R, +E] = ZIO[R, E, Response] - type Header = (CharSequence, CharSequence) + type HttpApp[-R, +E] = Http[R, E, Request, Response] + type UHttpApp = HttpApp[Any, Nothing] + type RHttpApp[-R] = HttpApp[R, Throwable] + type UHttp[-A, +B] = Http[Any, Nothing, A, B] + type SilentResponse[-E] = CanBeSilenced[E, Response] + type ResponseZIO[-R, +E] = ZIO[R, E, Response] + type Header = (CharSequence, CharSequence) + type UMiddleware[+AIn, -BIn, -AOut, +BOut] = Middleware[Any, Nothing, AIn, BIn, AOut, BOut] /** * Default HTTP Charset diff --git a/zio-http/src/main/scala/zhttp/service/Server.scala b/zio-http/src/main/scala/zhttp/service/Server.scala index 080daa2d80..5cc1c61ed9 100644 --- a/zio-http/src/main/scala/zhttp/service/Server.scala +++ b/zio-http/src/main/scala/zhttp/service/Server.scala @@ -19,17 +19,17 @@ sealed trait Server[-R, +E] { self => Concat(self, other) private def settings[R1 <: R, E1 >: E](s: Config[R1, E1] = Config()): Config[R1, E1] = self match { - case Concat(self, other) => other.settings(self.settings(s)) - case LeakDetection(level) => s.copy(leakDetectionLevel = level) - case MaxRequestSize(size) => s.copy(maxRequestSize = size) - case Error(errorHandler) => s.copy(error = Some(errorHandler)) - case Ssl(sslOption) => s.copy(sslOption = sslOption) - case App(app) => s.copy(app = app) - case Address(address) => s.copy(address = address) - case AcceptContinue => s.copy(acceptContinue = true) - case KeepAlive => s.copy(keepAlive = true) - case FlowControl => s.copy(flowControl = false) - case ConsolidateFlush => s.copy(consolidateFlush = true) + case Concat(self, other) => other.settings(self.settings(s)) + case LeakDetection(level) => s.copy(leakDetectionLevel = level) + case MaxRequestSize(size) => s.copy(maxRequestSize = size) + case Error(errorHandler) => s.copy(error = Some(errorHandler)) + case Ssl(sslOption) => s.copy(sslOption = sslOption) + case App(app) => s.copy(app = app) + case Address(address) => s.copy(address = address) + case AcceptContinue(enabled) => s.copy(acceptContinue = enabled) + case KeepAlive(enabled) => s.copy(keepAlive = enabled) + case FlowControl(enabled) => s.copy(flowControl = enabled) + case ConsolidateFlush(enabled) => s.copy(consolidateFlush = enabled) } def make(implicit @@ -39,6 +39,81 @@ sealed trait Server[-R, +E] { self => def start(implicit ev: E <:< Throwable): ZIO[R with EventLoopGroup with ServerChannelFactory, Throwable, Nothing] = make.useForever + + /** + * Launches the app with current settings: default EventLoopGroup (nThreads = 0) and ServerChannelFactory.auto. + */ + def startDefault[R1 <: R](implicit ev: E <:< Throwable): ZIO[R1, Throwable, Nothing] = + start.provideSomeLayer[R1](EventLoopGroup.auto(0) ++ ServerChannelFactory.auto) + + /** + * Creates a new server with the maximum size of the request specified in bytes. + */ + def withMaxRequestSize(size: Int): Server[R, E] = Concat(self, Server.MaxRequestSize(size)) + + /** + * Creates a new server listening on the provided port. + */ + def withPort(port: Int): Server[R, E] = Concat(self, Server.Address(new InetSocketAddress(port))) + + /** + * Creates a new server listening on the provided hostname and port. + */ + def withBinding(hostname: String, port: Int): Server[R, E] = + Concat(self, Server.Address(new InetSocketAddress(hostname, port))) + + /** + * Creates a new server listening on the provided InetAddress and port. + */ + def withBinding(address: InetAddress, port: Int): Server[R, E] = + Concat(self, Server.Address(new InetSocketAddress(address, port))) + + /** + * Creates a new server listening on the provided InetSocketAddress. + */ + def withBinding(inetSocketAddress: InetSocketAddress): Server[R, E] = Concat(self, Server.Address(inetSocketAddress)) + + /** + * Creates a new server with the errorHandler provided. + */ + def withError[R1](errorHandler: Throwable => ZIO[R1, Nothing, Unit]): Server[R with R1, E] = + Concat(self, Server.Error(errorHandler)) + + /** + * Creates a new server with the following ssl options. + */ + def withSsl(sslOptions: ServerSSLOptions): Server[R, E] = Concat(self, Server.Ssl(sslOptions)) + + /** + * Creates a new server using a HttpServerExpectContinueHandler to send a 100 HttpResponse if necessary. + */ + def withAcceptContinue(enable: Boolean): Server[R, E] = Concat(self, Server.AcceptContinue(enable)) + + /** + * Creates a new server using netty FlowControlHandler if enable (@see FlowControlHandler). + */ + def withFlowControl(enable: Boolean): Server[R, E] = Concat(self, Server.FlowControl(enable)) + + /** + * Creates a new server with the leak detection level provided (@see ResourceLeakDetector.Level). + */ + def withLeakDetection(level: LeakDetectionLevel): Server[R, E] = Concat(self, LeakDetection(level)) + + /** + * Creates a new server with netty's HttpServerKeepAliveHandler to close persistent connections when enable is true + * (@see HttpServerKeepAliveHandler). + */ + def withKeepAlive(enable: Boolean): Server[R, E] = Concat(self, KeepAlive(enable)) + + /** + * Creates a new server with FlushConsolidationHandler to control the flush operations in a more efficient way if + * enabled (@see FlushConsolidationHandler). + */ + def withConsolidateFlush(enable: Boolean): Server[R, E] = Concat(self, ConsolidateFlush(enable)) } object Server { @@ -69,10 +144,10 @@ object Server { private final case class Ssl(sslOptions: ServerSSLOptions) extends UServer private final case class Address(address: InetSocketAddress) extends UServer private final case class App[R, E](app: HttpApp[R, E]) extends Server[R, E] - private case object KeepAlive extends Server[Any, Nothing] - private case object ConsolidateFlush extends Server[Any, Nothing] - private case object AcceptContinue extends UServer - private case object FlowControl extends UServer + private final case class KeepAlive(enabled: Boolean) extends Server[Any, Nothing] + private final case class ConsolidateFlush(enabled: Boolean) extends Server[Any, Nothing] + private final case class AcceptContinue(enabled: Boolean) extends UServer + private final case class FlowControl(enabled: Boolean) extends UServer def app[R, E](http: HttpApp[R, E]): Server[R, E] = Server.App(http) def maxRequestSize(size: Int): UServer = Server.MaxRequestSize(size) @@ -83,14 +158,19 @@ object Server { def bind(inetSocketAddress: InetSocketAddress): UServer = Server.Address(inetSocketAddress) def error[R](errorHandler: Throwable => ZIO[R, Nothing, Unit]): Server[R, Nothing] = Server.Error(errorHandler) def ssl(sslOptions: ServerSSLOptions): UServer = Server.Ssl(sslOptions) - def acceptContinue: UServer = Server.AcceptContinue - def disableFlowControl: UServer = Server.FlowControl + def acceptContinue: UServer = Server.AcceptContinue(true) + val disableFlowControl: UServer = Server.FlowControl(false) val disableLeakDetection: UServer = LeakDetection(LeakDetectionLevel.DISABLED) val simpleLeakDetection: UServer = LeakDetection(LeakDetectionLevel.SIMPLE) val advancedLeakDetection: UServer = LeakDetection(LeakDetectionLevel.ADVANCED) val paranoidLeakDetection: UServer = LeakDetection(LeakDetectionLevel.PARANOID) - val keepAlive: UServer = KeepAlive - val consolidateFlush: UServer = ConsolidateFlush + val keepAlive: UServer = KeepAlive(true) + val consolidateFlush: UServer = ConsolidateFlush(true) + + /** + * Creates a server from a http app. + */ + def apply[R, E](http: HttpApp[R, E]): Server[R, E] = Server.App(http) /** * Launches the app on the provided port. @@ -99,7 +179,9 @@ object Server { port: Int, http: HttpApp[R, Throwable], ): ZIO[R, Throwable, Nothing] = { - (Server.bind(port) ++ Server.app(http)).make + (Server(http) + .withPort(port)) + .make .flatMap(start => ZManaged.succeed(println(s"Server started on port: ${start.port}"))) .useForever .provideSomeLayer[R](EventLoopGroup.auto(0) ++ ServerChannelFactory.auto) @@ -110,14 +192,20 @@ object Server { port: Int, http: HttpApp[R, Throwable], ): ZIO[R, Throwable, Nothing] = - (Server.app(http) ++ Server.bind(address, port)).make.useForever + (Server(http) + .withBinding(address, port)) + .make + .useForever .provideSomeLayer[R](EventLoopGroup.auto(0) ++ ServerChannelFactory.auto) def start[R]( socketAddress: InetSocketAddress, http: HttpApp[R, Throwable], ): ZIO[R, Throwable, Nothing] = - (Server.app(http) ++ Server.bind(socketAddress)).make.useForever + (Server(http) + .withBinding(socketAddress)) + .make + .useForever .provideSomeLayer[R](EventLoopGroup.auto(0) ++ ServerChannelFactory.auto) def make[R]( diff --git a/zio-http/src/main/scala/zhttp/service/server/ServerChannelInitializer.scala b/zio-http/src/main/scala/zhttp/service/server/ServerChannelInitializer.scala index 055bf7aacf..e66f840e20 100644 --- a/zio-http/src/main/scala/zhttp/service/server/ServerChannelInitializer.scala +++ b/zio-http/src/main/scala/zhttp/service/server/ServerChannelInitializer.scala @@ -2,12 +2,12 @@ package zhttp.service.server import io.netty.channel.ChannelHandler.Sharable import io.netty.channel.{Channel, ChannelHandler, ChannelInitializer} -import io.netty.handler.codec.http.{ - HttpObjectAggregator, - HttpServerCodec, - HttpServerExpectContinueHandler, - HttpServerKeepAliveHandler, +import io.netty.handler.codec.http.HttpObjectDecoder.{ + DEFAULT_MAX_CHUNK_SIZE, + DEFAULT_MAX_HEADER_SIZE, + DEFAULT_MAX_INITIAL_LINE_LENGTH, } +import io.netty.handler.codec.http._ import io.netty.handler.flow.FlowControlHandler import io.netty.handler.flush.FlushConsolidationHandler import zhttp.service.Server.Config @@ -31,11 +31,19 @@ final case class ServerChannelInitializer[R]( // SSL // Add SSL Handler if CTX is available val sslctx = if (cfg.sslOption == null) null else cfg.sslOption.sslContext - if (sslctx != null) pipeline.addFirst(SSL_HANDLER, new OptionalSSLHandler(sslctx, cfg.sslOption.httpBehaviour, cfg)) + if (sslctx != null) + pipeline + .addFirst(SSL_HANDLER, new OptionalSSLHandler(sslctx, cfg.sslOption.httpBehaviour, cfg)) // ServerCodec - // Always add ServerCodec - pipeline.addLast(HTTP_SERVER_CODEC, new HttpServerCodec()) // TODO: See if server codec is really required + // Instead of ServerCodec, we should use Decoder and Encoder separately to have more granular control over performance. + pipeline.addLast( + "decoder", + new HttpRequestDecoder(DEFAULT_MAX_INITIAL_LINE_LENGTH, DEFAULT_MAX_HEADER_SIZE, DEFAULT_MAX_CHUNK_SIZE, false), + ) + pipeline.addLast("encoder", new HttpResponseEncoder()) + + // TODO: See if server codec is really required // ObjectAggregator // Always add ObjectAggregator diff --git a/zio-http/src/main/scala/zhttp/service/server/WebSocketUpgrade.scala b/zio-http/src/main/scala/zhttp/service/server/WebSocketUpgrade.scala index 573acb34f7..5ca7cac28d 100644 --- a/zio-http/src/main/scala/zhttp/service/server/WebSocketUpgrade.scala +++ b/zio-http/src/main/scala/zhttp/service/server/WebSocketUpgrade.scala @@ -11,7 +11,7 @@ import zhttp.service.{HttpRuntime, WEB_SOCKET_HANDLER} */ trait WebSocketUpgrade[R] { self: ChannelHandler => final def isWebSocket(res: Response): Boolean = - res.status == Status.SWITCHING_PROTOCOLS && res.attribute.socketApp.nonEmpty + res.status.asJava.code() == Status.SWITCHING_PROTOCOLS.asJava.code() && res.attribute.socketApp.nonEmpty /** * Checks if the response requires to switch protocol to websocket. Returns true if it can, otherwise returns false diff --git a/zio-http/src/main/scala/zhttp/socket/Socket.scala b/zio-http/src/main/scala/zhttp/socket/Socket.scala index e86584a7f7..4fa1fee8ed 100644 --- a/zio-http/src/main/scala/zhttp/socket/Socket.scala +++ b/zio-http/src/main/scala/zhttp/socket/Socket.scala @@ -57,21 +57,21 @@ sealed trait Socket[-R, +E, -A, +B] { self => } object Socket { - def collect[A]: MkCollect[A] = new MkCollect[A](()) + def collect[A]: PartialCollect[A] = new PartialCollect[A](()) def end: ZStream[Any, Nothing, Nothing] = ZStream.failCause(Cause.empty) - def fromFunction[A]: MkFromFunction[A] = new MkFromFunction[A](()) + def fromFunction[A]: PartialFromFunction[A] = new PartialFromFunction[A](()) def fromStream[R, E, B](stream: ZStream[R, E, B]): Socket[R, E, Any, B] = FromStream(stream) def succeed[A](a: A): Socket[Any, Nothing, Any, A] = Succeed(a) - final class MkFromFunction[A](val unit: Unit) extends AnyVal { + final class PartialFromFunction[A](val unit: Unit) extends AnyVal { def apply[R, E, B](f: A => ZStream[R, E, B]): Socket[R, E, A, B] = FromStreamingFunction(f) } - final class MkCollect[A](val unit: Unit) extends AnyVal { + final class PartialCollect[A](val unit: Unit) extends AnyVal { def apply[R, E, B](pf: PartialFunction[A, ZStream[R, E, B]]): Socket[R, E, A, B] = Socket.FromStreamingFunction { a => if (pf.isDefinedAt(a)) pf(a) else ZStream.empty diff --git a/zio-http/src/test/scala/zhttp/http/HExitSpec.scala b/zio-http/src/test/scala/zhttp/http/HExitSpec.scala index b4b234a7de..868c97625d 100644 --- a/zio-http/src/test/scala/zhttp/http/HExitSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/HExitSpec.scala @@ -13,7 +13,7 @@ object HExitSpec extends DefaultRunnableSpec with HExitAssertion { empty === isEmpty && succeed(1) === isSuccess(equalTo(1)) && fail(1) === isFailure(equalTo(1)) && - effect(UIO(1)) === isEffect + fromZIO(UIO(1)) === isEffect } + test("flatMapError") { succeed(0) *> fail(1) <> fail(2) === isFailure(equalTo(2)) && @@ -37,9 +37,9 @@ object HExitSpec extends DefaultRunnableSpec with HExitAssertion { empty <+> empty === isEmpty } + test("effect") { - effect(UIO(1)) <+> empty === isEffect && - empty <+> effect(UIO(1)) === isEffect && - empty *> effect(UIO(1)) *> effect(UIO(1)) === isEmpty + fromZIO(UIO(1)) <+> empty === isEffect && + empty <+> fromZIO(UIO(1)) === isEffect && + empty *> fromZIO(UIO(1)) *> fromZIO(UIO(1)) === isEmpty } + test("nested succeed") { empty <+> succeed(1) <+> succeed(2) === isSuccess(equalTo(1)) && diff --git a/zio-http/src/test/scala/zhttp/http/HeaderSpec.scala b/zio-http/src/test/scala/zhttp/http/HeaderSpec.scala index f67a68f8ec..5e275d8318 100644 --- a/zio-http/src/test/scala/zhttp/http/HeaderSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/HeaderSpec.scala @@ -61,6 +61,12 @@ object HeaderSpec extends DefaultRunnableSpec { assert(actual)(isNone) }, ) + + suite("hasHeader")( + test("should return true if content-type is application/json") { + val actual = contentTypeJson.hasHeader(HeaderNames.contentType, HeaderValues.applicationJson) + assert(actual)(isTrue) + }, + ) + suite("hasJsonContentType")( test("should return true if content-type is application/json") { val actual = contentTypeJson.hasJsonContentType diff --git a/zio-http/src/test/scala/zhttp/http/HttpSpec.scala b/zio-http/src/test/scala/zhttp/http/HttpSpec.scala index 2647892830..07ae5f3f52 100644 --- a/zio-http/src/test/scala/zhttp/http/HttpSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/HttpSpec.scala @@ -92,12 +92,12 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { suite("asEffect")( test("should resolve") { val a = Http.collect[Int] { case 1 => "A" } - val actual = a.execute(1).toEffect + val actual = a.execute(1).toZIO assertM(actual)(equalTo("A")) } + test("should complete") { val a = Http.collect[Int] { case 1 => "A" } - val actual = a.execute(2).toEffect.either + val actual = a.execute(2).toZIO.either assertM(actual)(isLeft(isNone)) }, ) + @@ -138,8 +138,8 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { test("taps the successs") { for { r <- Ref.make(0) - app = Http.succeed(1).tap(v => Http.fromEffect(r.set(v))) - _ <- app.execute(()).toEffect + app = Http.succeed(1).tap(v => Http.fromZIO(r.set(v))) + _ <- app.execute(()).toZIO res <- r.get } yield assert(res)(equalTo(1)) }, @@ -149,7 +149,7 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { for { r <- Ref.make(0) app = Http.succeed(1).tapZIO(r.set) - _ <- app.execute(()).toEffect + _ <- app.execute(()).toZIO res <- r.get } yield assert(res)(equalTo(1)) }, @@ -158,8 +158,8 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { test("taps the error") { for { r <- Ref.make(0) - app = Http.fail(1).tapError(v => Http.fromEffect(r.set(v))) - _ <- app.execute(()).toEffect.ignore + app = Http.fail(1).tapError(v => Http.fromZIO(r.set(v))) + _ <- app.execute(()).toZIO.ignore res <- r.get } yield assert(res)(equalTo(1)) }, @@ -169,7 +169,7 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { for { r <- Ref.make(0) app = Http.fail(1).tapErrorZIO(r.set) - _ <- app.execute(()).toEffect.ignore + _ <- app.execute(()).toZIO.ignore res <- r.get } yield assert(res)(equalTo(1)) }, @@ -179,8 +179,8 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { for { r <- Ref.make(0) app = (Http.succeed(1): Http[Any, Any, Any, Int]) - .tapAll(_ => Http.empty, v => Http.fromEffect(r.set(v)), Http.empty) - _ <- app.execute(()).toEffect + .tapAll(_ => Http.empty, v => Http.fromZIO(r.set(v)), Http.empty) + _ <- app.execute(()).toZIO res <- r.get } yield assert(res)(equalTo(1)) } + @@ -188,8 +188,8 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { for { r <- Ref.make(0) app = (Http.fail(1): Http[Any, Int, Any, Any]) - .tapAll(v => Http.fromEffect(r.set(v)), _ => Http.empty, Http.empty) - _ <- app.execute(()).toEffect.ignore + .tapAll(v => Http.fromZIO(r.set(v)), _ => Http.empty, Http.empty) + _ <- app.execute(()).toZIO.ignore res <- r.get } yield assert(res)(equalTo(1)) } + @@ -197,8 +197,8 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { for { r <- Ref.make(0) app = (Http.empty: Http[Any, Any, Any, Any]) - .tapAll(_ => Http.empty, _ => Http.empty, Http.fromEffect(r.set(1))) - _ <- app.execute(()).toEffect.ignore + .tapAll(_ => Http.empty, _ => Http.empty, Http.fromZIO(r.set(1))) + _ <- app.execute(()).toZIO.ignore res <- r.get } yield assert(res)(equalTo(1)) }, @@ -208,7 +208,7 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { for { r <- Ref.make(0) app = (Http.succeed(1): Http[Any, Any, Any, Int]).tapAllZIO(_ => ZIO.unit, r.set, ZIO.unit) - _ <- app.execute(()).toEffect + _ <- app.execute(()).toZIO res <- r.get } yield assert(res)(equalTo(1)) } + @@ -216,7 +216,7 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { for { r <- Ref.make(0) app = (Http.fail(1): Http[Any, Int, Any, Any]).tapAllZIO(r.set, _ => ZIO.unit, ZIO.unit) - _ <- app.execute(()).toEffect.ignore + _ <- app.execute(()).toZIO.ignore res <- r.get } yield assert(res)(equalTo(1)) } + @@ -225,7 +225,7 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { r <- Ref.make(0) app = (Http.empty: Http[Any, Any, Any, Any]) .tapAllZIO(_ => ZIO.unit, _ => ZIO.unit, r.set(1)) - _ <- app.execute(()).toEffect.ignore + _ <- app.execute(()).toZIO.ignore res <- r.get } yield assert(res)(equalTo(1)) }, @@ -236,11 +236,11 @@ object HttpSpec extends DefaultRunnableSpec with HExitAssertion { assertM(http(()))(equalTo(1)) } + test("sync right wins") { - val http = Http.fromEffect(UIO(1)) race Http.succeed(2) + val http = Http.fromZIO(UIO(1)) race Http.succeed(2) assertM(http(()))(equalTo(2)) } + test("sync left wins") { - val http = Http.succeed(1) race Http.fromEffect(UIO(2)) + val http = Http.succeed(1) race Http.fromZIO(UIO(2)) assertM(http(()))(equalTo(1)) } + test("async fast wins") { diff --git a/zio-http/src/test/scala/zhttp/http/MiddlewareSpec.scala b/zio-http/src/test/scala/zhttp/http/MiddlewareSpec.scala new file mode 100644 index 0000000000..0707b0d718 --- /dev/null +++ b/zio-http/src/test/scala/zhttp/http/MiddlewareSpec.scala @@ -0,0 +1,155 @@ +package zhttp.http + +import zio._ +import zio.test.Assertion._ +import zio.test._ + +object MiddlewareSpec extends DefaultRunnableSpec with HExitAssertion { + def spec = suite("Middleware") { + val increment = Middleware.codec[Int, Int](decoder = a => Right(a + 1), encoder = b => Right(b + 1)) + test("empty") { + val http = Http.empty + val app = Middleware.identity(http) + assertM(app(()).either)(isLeft(isNone)) + } + + test("constant") { + val mid = Middleware.fromHttp(Http.succeed("OK")) + val app = Http.succeed(1) @@ mid + assertM(app(()))(equalTo("OK")) + } + + test("as") { + val mid = Middleware.fromHttp(Http.succeed("Not OK")).as("OK") + val app = Http.succeed(1) @@ mid + assertM(app(()))(equalTo("OK")) + } + + test("interceptZIO") { + for { + ref <- Ref.make(0) + mid = Middleware.interceptZIO[Int, Int](i => UIO(i * 10))((i, j) => ref.set(i + j)) + app = Http.identity[Int] @@ mid + _ <- app(1) + i <- ref.get + } yield assert(i)(equalTo(11)) + } + + test("orElse") { + val mid = Middleware.fail("left") <> Middleware.fail("right") + val app = Http.empty @@ mid + assertM(app(()).flip)(isSome(equalTo("right"))) + } + + test("combine") { + val mid1 = increment + val mid2 = increment + val mid = mid1 andThen mid2 + val app = Http.identity[Int] @@ mid + assertM(app(0))(equalTo(4)) + } + + test("flatMap") { + val mid = increment.flatMap(i => Middleware.succeed(i + 1)) + val app = Http.identity[Int] @@ mid + assertM(app(0))(equalTo(3)) + } + + test("mapZIO") { + val mid = increment.mapZIO(i => UIO(i + 1)) + val app = Http.identity[Int] @@ mid + assertM(app(0))(equalTo(3)) + } + + test("runBefore") { + val mid = Middleware.identity.runBefore(Console.printLine("A")) + val app = Http.fromZIO(Console.printLine("B")) @@ mid + assertM(app(()) *> TestConsole.output)(equalTo(Vector("A\n", "B\n"))) + } + + test("runAfter") { + val mid = Middleware.identity.runAfter(Console.printLine("B")) + val app = Http.fromZIO(Console.printLine("A")) @@ mid + assertM(app(()) *> TestConsole.output)(equalTo(Vector("A\n", "B\n"))) + } + + test("runBefore and runAfter") { + val mid = Middleware.identity.runBefore(Console.printLine("A")).runAfter(Console.printLine("C")) + val app = Http.fromZIO(Console.printLine("B")) @@ mid + assertM(app(()) *> TestConsole.output)(equalTo(Vector("A\n", "B\n", "C\n"))) + } + + test("race") { + val mid = Middleware.succeed('A').delay(2 second) race Middleware.succeed("B").delay(1 second) + val app = Http.succeed(1) @@ mid + assertM(app(()) <& TestClock.adjust(3 second))(equalTo("B")) + } + + suite("ifThenElse") { + val mid = Middleware.ifThenElse[Int](_ > 5)( + isTrue = i => Middleware.succeed(i + 1), + isFalse = i => Middleware.succeed(i - 1), + ) + test("isTrue") { + val app = Http.identity[Int] @@ mid + assertM(app(10))(equalTo(11)) + } + + test("isFalse") { + val app = Http.identity[Int] @@ mid + assertM(app(1))(equalTo(0)) + } + } + + suite("ifThenElseZIO") { + val mid = Middleware.ifThenElseZIO[Int](i => UIO(i > 5))( + isTrue = i => Middleware.succeed(i + 1), + isFalse = i => Middleware.succeed(i - 1), + ) + test("isTrue") { + val app = Http.identity[Int] @@ mid + assertM(app(10))(equalTo(11)) + } + + test("isFalse") { + val app = Http.identity[Int] @@ mid + assertM(app(1))(equalTo(0)) + } + } + + suite("contramap") { + val mid = Middleware.intercept[String, String](a => a + "Bar")((b, s) => b + s) + test("contramap") { + val app = Http.identity[String] @@ mid.contramap[Int] { i => s"${i}Foo" } + assertM(app(0))(equalTo("0Foo0FooBar")) + } + + test("contramapZIO") { + val app = Http.identity[String] @@ mid.contramapZIO[Int] { i => UIO(s"${i}Foo") } + assertM(app(0))(equalTo("0Foo0FooBar")) + } + } + + suite("when") { + val mid = Middleware.succeed(0) + test("condition is true") { + val app = Http.identity[Int] @@ mid.when[Int](_ => true) + assertM(app(10))(equalTo(0)) + } + + test("condition is false") { + val app = Http.identity[Int] @@ mid.when[Int](_ => false) + assertM(app(1))(equalTo(1)) + } + } + + suite("whenZIO") { + val mid = Middleware.succeed(0) + test("condition is true") { + val app = Http.identity[Int] @@ mid.whenZIO[Any, Nothing, Int](_ => UIO(true)) + assertM(app(10))(equalTo(0)) + } + + test("condition is false") { + val app = Http.identity[Int] @@ mid.whenZIO[Any, Nothing, Int](_ => UIO(false)) + assertM(app(1))(equalTo(1)) + } + } + + suite("codec") { + test("codec success") { + val mid = Middleware.codec[String, Int](a => Right(a.toInt), b => Right(b.toString)) + val app = Http.identity[Int] @@ mid + assertM(app("1"))(equalTo("1")) + } + + test("decoder failure") { + val mid = Middleware.codec[String, Int](a => Left(a), b => Right(b.toString)) + val app = Http.identity[Int] @@ mid + assertM(app("a").exit)(fails(anything)) + } + + test("encoder failure") { + val mid = Middleware.codec[String, Int](a => Right(a.toInt), b => Left(b.toString)) + val app = Http.identity[Int] @@ mid + assertM(app("1").exit)(fails(anything)) + } + } + } +} diff --git a/zio-http/src/test/scala/zhttp/http/middleware/AuthSpec.scala b/zio-http/src/test/scala/zhttp/http/middleware/AuthSpec.scala new file mode 100644 index 0000000000..9aac6fcd2c --- /dev/null +++ b/zio-http/src/test/scala/zhttp/http/middleware/AuthSpec.scala @@ -0,0 +1,29 @@ +package zhttp.http.middleware + +import zhttp.http._ +import zhttp.internal.HttpAppTestExtensions +import zio.test.Assertion._ +import zio.test._ + +object AuthSpec extends DefaultRunnableSpec with HttpAppTestExtensions { + private val basicHS = Headers.basicAuthorizationHeader("user", "resu") + private val basicHF = Headers.basicAuthorizationHeader("user", "user") + private val basicAuthM = Middleware.basicAuth { case (u, p) => p.toString.reverse == u } + + def spec = suite("AuthSpec") { + suite("basicAuth") { + test("HttpApp is accepted if the basic authentication succeeds") { + val app = (Http.ok @@ basicAuthM).getStatus + assertM(app(Request().addHeaders(basicHS)))(equalTo(Status.OK)) + } + + test("Uses forbidden app if the basic authentication fails") { + val app = (Http.ok @@ basicAuthM).getStatus + assertM(app(Request().addHeaders(basicHF)))(equalTo(Status.FORBIDDEN)) + } + + test("Responses should have WWW-Authentication header if Basic Auth failed") { + val app = Http.ok @@ basicAuthM getHeader "WWW-AUTHENTICATE" + assertM(app(Request().addHeaders(basicHF)))(isSome) + } + } + } +} diff --git a/zio-http/src/test/scala/zhttp/http/middleware/CorsSpec.scala b/zio-http/src/test/scala/zhttp/http/middleware/CorsSpec.scala new file mode 100644 index 0000000000..bd48d8218d --- /dev/null +++ b/zio-http/src/test/scala/zhttp/http/middleware/CorsSpec.scala @@ -0,0 +1,54 @@ +package zhttp.http.middleware + +import zhttp.http.Middleware.cors +import zhttp.http._ +import zhttp.http.middleware.Cors.CorsConfig +import zhttp.internal.HttpAppTestExtensions +import zio.test.Assertion.hasSubset +import zio.test._ + +object CorsSpec extends DefaultRunnableSpec with HttpAppTestExtensions { + override def spec = suite("CorsMiddlewares") { + val app = Http.ok @@ cors() + test("OPTIONS request") { + val request = Request( + method = Method.OPTIONS, + url = URL(!! / "success"), + headers = Headers.accessControlRequestMethod(Method.GET) ++ Headers.origin("test-env"), + ) + + val expected = Headers + .accessControlAllowCredentials(true) + .withAccessControlAllowMethods(Method.GET) + .withAccessControlAllowOrigin("test-env") + .withAccessControlAllowHeaders( + CorsConfig().allowedHeaders.getOrElse(Set.empty).mkString(","), + ) + .toList + + for { + res <- app(request) + } yield assert(res.getHeadersAsList)(hasSubset(expected)) && + assertTrue(res.status == Status.NO_CONTENT) + } + + test("GET request") { + val request = + Request( + method = Method.GET, + url = URL(!! / "success"), + headers = Headers.accessControlRequestMethod(Method.GET) ++ Headers.origin("test-env"), + ) + + val expected = Headers + .accessControlExposeHeaders("*") + .withAccessControlAllowOrigin("test-env") + .withAccessControlAllowMethods(Method.GET) + .withAccessControlAllowCredentials(true) + .toList + + for { + res <- app(request) + } yield assert(res.getHeadersAsList)(hasSubset(expected)) + } + } +} diff --git a/zio-http/src/test/scala/zhttp/http/middleware/CsrfSpec.scala b/zio-http/src/test/scala/zhttp/http/middleware/CsrfSpec.scala new file mode 100644 index 0000000000..aba6a4dca5 --- /dev/null +++ b/zio-http/src/test/scala/zhttp/http/middleware/CsrfSpec.scala @@ -0,0 +1,39 @@ +package zhttp.http.middleware + +import zhttp.http.Middleware.csrfValidate +import zhttp.http._ +import zhttp.internal.HttpAppTestExtensions +import zio.Ref +import zio.test.Assertion.equalTo +import zio.test._ + +object CsrfSpec extends DefaultRunnableSpec with HttpAppTestExtensions { + override def spec = suite("CSRF Middlewares") { + val app = (Http.ok @@ csrfValidate("x-token")).getStatus + val setCookie = Headers.cookie(Cookie("x-token", "secret")) + val invalidXToken = Headers("x-token", "secret1") + val validXToken = Headers("x-token", "secret") + test("x-token not present") { + assertM(app(Request(headers = setCookie)))(equalTo(Status.FORBIDDEN)) + } + + test("x-token mismatch") { + assertM(app(Request(headers = setCookie ++ invalidXToken)))( + equalTo(Status.FORBIDDEN), + ) + } + + test("x-token match") { + assertM(app(Request(headers = setCookie ++ validXToken)))( + equalTo(Status.OK), + ) + } + + test("app execution skipped") { + for { + r <- Ref.make(false) + app = Http.ok.tapZIO(_ => r.set(true)) @@ csrfValidate("x-token") + _ <- app(Request(headers = setCookie ++ invalidXToken)) + res <- r.get + } yield assert(res)(equalTo(false)) + } + } + +} diff --git a/zio-http/src/test/scala/zhttp/http/middleware/WebSpec.scala b/zio-http/src/test/scala/zhttp/http/middleware/WebSpec.scala new file mode 100644 index 0000000000..ca0d78009c --- /dev/null +++ b/zio-http/src/test/scala/zhttp/http/middleware/WebSpec.scala @@ -0,0 +1,179 @@ +package zhttp.http.middleware + +import zhttp.http.Middleware._ +import zhttp.http._ +import zhttp.internal.HttpAppTestExtensions +import zio._ +import zio.test.Assertion._ +import zio.test._ + +object WebSpec extends DefaultRunnableSpec with HttpAppTestExtensions { self => + private val app = Http.collectZIO[Request] { case Method.GET -> !! / "health" => + UIO(Response.ok).delay(1 second) + } + private val midA = Middleware.addHeader("X-Custom", "A") + private val midB = Middleware.addHeader("X-Custom", "B") + + def spec = suite("HttpMiddleware") { + suite("headers suite") { + test("addHeaders") { + val middleware = addHeaders(Headers("KeyA", "ValueA") ++ Headers("KeyB", "ValueB")) + val headers = (Http.ok @@ middleware).getHeaderValues + assertM(headers(Request()))(contains("ValueA") && contains("ValueB")) + } + + test("addHeader") { + val middleware = addHeader("KeyA", "ValueA") + val headers = (Http.ok @@ middleware).getHeaderValues + assertM(headers(Request()))(contains("ValueA")) + } + + test("updateHeaders") { + val middleware = updateHeaders(_ => Headers("KeyA", "ValueA")) + val headers = (Http.ok @@ middleware).getHeaderValues + assertM(headers(Request()))(contains("ValueA")) + } + + test("removeHeader") { + val middleware = removeHeader("KeyA") + val headers = (Http.succeed(Response.ok.setHeaders(Headers("KeyA", "ValueA"))) @@ middleware) getHeader "KeyA" + assertM(headers(Request()))(isNone) + } + } + + suite("debug") { + test("log status method url and time") { + val program = runApp(app @@ debug) *> TestConsole.output + assertM(program)(equalTo(Vector("200 GET /health 1000ms\n"))) + } + + test("log 404 status method url and time") { + val program = runApp(Http.empty ++ Http.notFound @@ debug) *> TestConsole.output + assertM(program)(equalTo(Vector("404 GET /health 0ms\n"))) + } + } + + suite("when") { + test("condition is true") { + val program = runApp(self.app @@ debug.when(_ => true)) *> TestConsole.output + assertM(program)(equalTo(Vector("200 GET /health 1000ms\n"))) + } + + test("condition is false") { + val log = runApp(self.app @@ debug.when(_ => false)) *> TestConsole.output + assertM(log)(equalTo(Vector())) + } + } + + suite("whenZIO") { + test("condition is true") { + val program = runApp(self.app @@ debug.whenZIO(_ => UIO(true))) *> TestConsole.output + assertM(program)(equalTo(Vector("200 GET /health 1000ms\n"))) + } + + test("condition is false") { + val log = runApp(self.app @@ debug.whenZIO(_ => UIO(false))) *> TestConsole.output + assertM(log)(equalTo(Vector())) + } + } + + suite("race") { + test("achieved") { + val program = runApp(self.app @@ timeout(5 seconds)).map(_.status) + assertM(program)(equalTo(Status.OK)) + } + + test("un-achieved") { + val program = runApp(self.app @@ timeout(500 millis)).map(_.status) + assertM(program)(equalTo(Status.REQUEST_TIMEOUT)) + } + } + + suite("combine") { + test("before and after") { + val middleware = runBefore(Console.printLine("A")) + val program = runApp(self.app @@ middleware) *> TestConsole.output + assertM(program)(equalTo(Vector("A\n"))) + } + + test("add headers twice") { + val middleware = addHeader("KeyA", "ValueA") ++ addHeader("KeyB", "ValueB") + val headers = (Http.ok @@ middleware).getHeaderValues + assertM(headers(Request()))(contains("ValueA") && contains("ValueB")) + } + + test("add and remove header") { + val middleware = addHeader("KeyA", "ValueA") ++ removeHeader("KeyA") + val program = (Http.ok @@ middleware) getHeader "KeyA" + assertM(program(Request()))(isNone) + } + } + + suite("ifRequestThenElseZIO") { + test("if the condition is true take first") { + val app = (Http.ok @@ ifRequestThenElseZIO(condM(true))(midA, midB)) getHeader "X-Custom" + assertM(app(Request()))(isSome(equalTo("A"))) + } + + test("if the condition is false take 2nd") { + val app = + (Http.ok @@ ifRequestThenElseZIO(condM(false))(midA, midB)) getHeader "X-Custom" + assertM(app(Request()))(isSome(equalTo("B"))) + } + } + + suite("ifRequestThenElse") { + test("if the condition is true take first") { + val app = Http.ok @@ ifRequestThenElse(cond(true))(midA, midB) getHeader "X-Custom" + assertM(app(Request()))(isSome(equalTo("A"))) + } + + test("if the condition is false take 2nd") { + val app = Http.ok @@ ifRequestThenElse(cond(false))(midA, midB) getHeader "X-Custom" + assertM(app(Request()))(isSome(equalTo("B"))) + } + } + + suite("whenRequestZIO") { + test("if the condition is true apply middleware") { + val app = (Http.ok @@ whenRequestZIO(condM(true))(midA)) getHeader "X-Custom" + assertM(app(Request()))(isSome(equalTo("A"))) + } + + test("if the condition is false don't apply any middleware") { + val app = (Http.ok @@ whenRequestZIO(condM(false))(midA)) getHeader "X-Custom" + assertM(app(Request()))(isNone) + } + } + + suite("whenRequest") { + test("if the condition is true apple middleware") { + val app = Http.ok @@ Middleware.whenRequest(cond(true))(midA) getHeader "X-Custom" + assertM(app(Request()))(isSome(equalTo("A"))) + } + + test("if the condition is false don't apply the middleware") { + val app = Http.ok @@ Middleware.whenRequest(cond(false))(midA) getHeader "X-Custom" + assertM(app(Request()))(isNone) + } + } + + suite("cookie") { + test("addCookie") { + val cookie = Cookie("test", "testValue") + val app = (Http.ok @@ addCookie(cookie)).getHeader("set-cookie") + assertM(app(Request()))( + equalTo(Some(cookie.encode)), + ) + } + + test("addCookieM") { + val cookie = Cookie("test", "testValue") + val app = + (Http.ok @@ addCookieZIO(UIO(cookie))).getHeader("set-cookie") + assertM(app(Request()))( + equalTo(Some(cookie.encode)), + ) + } + } + + suite("signCookies") { + test("should sign cookies") { + val cookie = Cookie("key", "value").withHttpOnly + val app = Http.ok.withSetCookie(cookie) @@ signCookies("secret") getHeader "set-cookie" + assertM(app(Request()))(isSome(equalTo(cookie.sign("secret").encode))) + } + + test("sign cookies no cookie header") { + val app = (Http.ok.addHeader("keyA", "ValueA") @@ signCookies("secret")).getHeaderValues + assertM(app(Request()))(contains("ValueA")) + } + } + } + + private def cond(flg: Boolean) = (_: Any) => flg + + private def condM(flg: Boolean) = (_: Any) => UIO(flg) + + private def runApp[R, E](app: HttpApp[R, E]): ZIO[TestClock with R, Option[E], Response] = { + for { + fib <- app { Request(url = URL(!! / "health")) }.fork + _ <- TestClock.adjust(10 seconds) + res <- fib.join + } yield res + } +} diff --git a/zio-http/src/test/scala/zhttp/middleware/MiddlewareSpec.scala b/zio-http/src/test/scala/zhttp/middleware/MiddlewareSpec.scala deleted file mode 100644 index d619f8ff30..0000000000 --- a/zio-http/src/test/scala/zhttp/middleware/MiddlewareSpec.scala +++ /dev/null @@ -1,243 +0,0 @@ -package zhttp.middleware - -import zhttp.http._ -import zhttp.internal.HttpAppTestExtensions -import zio._ -import zio.test.Assertion._ -import zio.test.{TestClock, TestConsole, _} - -object MiddlewareSpec extends DefaultRunnableSpec with HttpAppTestExtensions { - def spec = suite("HttpMiddleware") { - import Middleware._ - - suite("debug") { - test("log status method url and time") { - val program = run(app @@ debug) *> TestConsole.output - assertM(program)(equalTo(Vector("200 GET /health 1000ms\n"))) - } + - test("log 404 status method url and time") { - val program = run(Http.empty @@ debug.withEmpty) *> TestConsole.output - assertM(program)(equalTo(Vector("404 GET /health 0ms\n"))) - } - } + - suite("withEmpty") { - test("log 404 status method url and time") { - val program = run(Http.empty @@ debug.withEmpty) *> TestConsole.output - assertM(program)(equalTo(Vector("404 GET /health 0ms\n"))) - } - } + - suite("when") { - test("condition is true") { - val program = run(app @@ debug.when((_, _, _) => true)) *> TestConsole.output - assertM(program)(equalTo(Vector("200 GET /health 1000ms\n"))) - } + - test("condition is false") { - val log = run(app @@ debug.when((_, _, _) => false)) *> TestConsole.output - assertM(log)(equalTo(Vector())) - } - } + - suite("race") { - test("achieved") { - val program = run(app @@ timeout(5 seconds)).map(_.status) - assertM(program)(equalTo(Status.OK)) - } + - test("un-achieved") { - val program = run(app @@ timeout(500 millis)).map(_.status) - assertM(program)(equalTo(Status.REQUEST_TIMEOUT)) - } - } + - suite("combine") { - test("before and after") { - val middleware = runBefore(Console.printLine("A")) ++ runAfter(Console.printLine("B")) - val program = run(app @@ middleware) *> TestConsole.output - assertM(program)(equalTo(Vector("A\n", "B\n"))) - } + - test("add headers twice") { - val middleware = addHeader("KeyA", "ValueA") ++ addHeader("KeyB", "ValueB") - val headers = (Http.ok @@ middleware).getHeaderValues - assertM(headers(Request()))(contains("ValueA") && contains("ValueB")) - } + - test("add and remove header") { - val middleware = addHeader("KeyA", "ValueA") ++ removeHeader("KeyA") - val program = (Http.ok @@ middleware) getHeader "KeyA" - assertM(program(Request()))(isNone) - } - } + - suite("ifThenElseM") { - test("if the condition is true take first") { - val app = (Http.ok @@ ifThenElseZIO(condM(true))(midA, midB)) getHeader "X-Custom" - assertM(app(Request()))(isSome(equalTo("A"))) - } + - test("if the condition is false take 2nd") { - val app = - (Http.ok @@ ifThenElseZIO(condM(false))(midA, midB)) getHeader "X-Custom" - assertM(app(Request()))(isSome(equalTo("B"))) - } - } + - suite("ifThenElse") { - test("if the condition is true take first") { - val app = Http.ok @@ ifThenElse(cond(true))(midA, midB) getHeader "X-Custom" - assertM(app(Request()))(isSome(equalTo("A"))) - } + - test("if the condition is false take 2nd") { - val app = Http.ok @@ ifThenElse(cond(false))(midA, midB) getHeader "X-Custom" - assertM(app(Request()))(isSome(equalTo("B"))) - } - } + - suite("whenM") { - test("if the condition is true apply middleware") { - val app = (Http.ok @@ whenZIO(condM(true))(midA)) getHeader "X-Custom" - assertM(app(Request()))(isSome(equalTo("A"))) - } + - test("if the condition is false don't apply any middleware") { - val app = (Http.ok @@ whenZIO(condM(false))(midA)) getHeader "X-Custom" - assertM(app(Request()))(isNone) - } - } + - suite("when") { - test("if the condition is true apple middleware") { - val app = Http.ok @@ when(cond(true))(midA) getHeader "X-Custom" - assertM(app(Request()))(isSome(equalTo("A"))) - } + - test("if the condition is false don't apply the middleware") { - val app = Http.ok @@ when(cond(false))(midA) getHeader "X-Custom" - assertM(app(Request()))(isNone) - } - } + - suite("Authentication middleware") { - suite("basicAuth") { - test("HttpApp is accepted if the basic authentication succeeds") { - val app = (Http.ok @@ basicAuthM).getStatus - assertM(app(Request().addHeaders(basicHS)))(equalTo(Status.OK)) - } + - test("Uses forbidden app if the basic authentication fails") { - val app = (Http.ok @@ basicAuthM).getStatus - assertM(app(Request().addHeaders(basicHF)))(equalTo(Status.FORBIDDEN)) - } + - test("Responses should have WWW-Authentication header if Basic Auth failed") { - val app = Http.ok @@ basicAuthM getHeader "WWW-AUTHENTICATE" - assertM(app(Request().addHeaders(basicHF)))(isSome) - } - } - } + - suite("cors") { - // FIXME:The test should ideally pass with `Http.ok` also - val app = Http.collect[Request] { case Method.GET -> !! / "success" => Response.ok } @@ cors() - test("OPTIONS request") { - val request = Request( - method = Method.OPTIONS, - url = URL(!! / "success"), - headers = Headers.accessControlRequestMethod(Method.GET) ++ Headers.origin("test-env"), - ) - - val expected = Headers - .accessControlAllowCredentials(true) - .withAccessControlAllowMethods(Method.GET) - .withAccessControlAllowOrigin("test-env") - .withAccessControlAllowHeaders( - CORS.DefaultCORSConfig.allowedHeaders.getOrElse(Set.empty).mkString(","), - ) - .toList - - for { - res <- app(request) - } yield { - val set = res.getHeadersAsList.toSet - - assertTrue(expected.forall(h => set.contains(h))) && assertTrue(res.status == Status.NO_CONTENT) - } - } + - test("GET request") { - val request = - Request( - method = Method.GET, - url = URL(!! / "success"), - headers = Headers.accessControlRequestMethod(Method.GET) ++ Headers.origin("test-env"), - ) - - val expected = Headers - .accessControlExposeHeaders("*") - .withAccessControlAllowOrigin("test-env") - .withAccessControlAllowMethods(Method.GET) - .withAccessControlAllowCredentials(true) - .toList - - for { - res <- app(request).map(_.getHeadersAsList.toSet) - } yield assertTrue(expected.forall(h => res.contains(h))) - } - } + - suite("cookie") { - test("addCookie") { - val cookie = Cookie("test", "testValue") - val app = (Http.ok @@ addCookie(cookie)).getHeader("set-cookie") - assertM(app(Request()))( - equalTo(Some(cookie.encode)), - ) - } + - test("addCookieM") { - val cookie = Cookie("test", "testValue") - val app = - (Http.ok @@ addCookieM(UIO(cookie))).getHeader("set-cookie") - assertM(app(Request()))( - equalTo(Some(cookie.encode)), - ) - } - } + - suite("csrf") { - val app = (Http.ok @@ csrfValidate("x-token")).getStatus - val setCookie = Headers.cookie(Cookie("x-token", "secret")) - val invalidXToken = Headers("x-token", "secret1") - val validXToken = Headers("x-token", "secret") - test("x-token not present") { - assertM(app(Request(headers = setCookie)))(equalTo(Status.FORBIDDEN)) - } + - test("x-token mismatch") { - assertM(app(Request(headers = setCookie ++ invalidXToken)))( - equalTo(Status.FORBIDDEN), - ) - } + - test("x-token match") { - assertM(app(Request(headers = setCookie ++ validXToken)))( - equalTo(Status.OK), - ) - } + - test("app execution skipped") { - for { - r <- Ref.make(false) - app = Http.ok.tapZIO(_ => r.set(true)) @@ csrfValidate("x-token") - _ <- app(Request(headers = setCookie ++ invalidXToken)) - res <- r.get - } yield assertTrue(res == false) - } - } + - suite("signCookies") { - test("should sign cookies") { - val cookie = Cookie("key", "value").withHttpOnly - val app = Http.ok.withSetCookie(cookie) @@ signCookies("secret") getHeader "set-cookie" - assertM(app(Request()))(isSome(equalTo(cookie.sign("secret").encode))) - } - } - } - - private def app: HttpApp[Any with Clock, Nothing] = Http.collectZIO[Request] { case Method.GET -> !! / "health" => - UIO(Response.ok).delay(1 second) - } - private val midA = Middleware.addHeader("X-Custom", "A") - private val midB = Middleware.addHeader("X-Custom", "B") - private val basicHS = Headers.basicAuthorizationHeader("user", "resu") - private val basicHF = Headers.basicAuthorizationHeader("user", "user") - private val basicAuthM = Middleware.basicAuth { case (u, p) => p.toString.reverse == u } - - private def cond(flg: Boolean) = (_: Any, _: Any, _: Any) => flg - - private def condM(flg: Boolean) = (_: Any, _: Any, _: Any) => UIO(flg) - - private def run[R, E](app: HttpApp[R, E]): ZIO[TestClock with R, Option[E], Response] = { - for { - fib <- app { Request(url = URL(!! / "health")) }.fork - _ <- TestClock.adjust(10 seconds) - res <- fib.join - } yield res - } -}