From 085ea0dc66c3f021abefaa6f214bb33c8cf475fa Mon Sep 17 00:00:00 2001 From: Amit Kumar Singh Date: Thu, 2 Jun 2022 16:34:52 +0530 Subject: [PATCH] Backmerge main 30 may 2022 (#1280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * Disable benchmarks comment on fork pull request (#820) * Disable benchmarks on fork PR * run benchmarks on fork PR but disbable PR comment * 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 * 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 * refactor: rename ClientParams to ClientRequest (#856) * refactor: use declarative encoding for http.middleware (#869) * Move docs to v1.x directory (#861) * Bug Add host in client from absolute URL (#847) * feat(Client): Add host in client from absolute URL * feat(Client): Refactor and add spec for host value * feat(Client): fmt formatting * feat(Client): fmt formatting * feat(Client): use setHeaders from netty * feat(Client): Add spec for host * feat(Client): Add spec for host * feat(Client): Rename clientParams to clientRequest * Doc website fix (#871) * Update scala3-library to 3.1.1 (#873) * Update scala3-library to 3.1.1 * Regenerate workflow with sbt-github-actions * Doc: Fix getting started link (#874) * Update scalafmt-core to 3.3.2 (#864) * Doc: `Headers` documentation (#817) * doc: add categories * refactor: doc structure refactored * fix: package-lock.json removed * refactor: create outline sub-directories * initial draft on Headers documentaiton * refactor: rename test to testing * migrated Headers doc in the right structure * re-organized the section. added more examples on the service, client and middleware side. * updated documentation * updated based on review * more updates * more updates * minor changes * minor changes * Update docs/website/docs/v1.x/dsl/headers/index.md * Update docs/website/docs/v1.x/dsl/headers/index.md Co-authored-by: Shubham Girdhar Co-authored-by: amitsingh Co-authored-by: Amit Kumar Singh * Fix collapsible Headers Doc (#876) * Test: Added Integration tests for `HExit.Success` (#852) * feat: added validation app for HExit without zio * feat: removed type from validationAppSpec * test: Iterating using HttpGen.Method * test: rearranged status method params * test: name fixes * Update scalafmt-core to 3.3.3 (#881) * Update scalafmt-core to 3.3.3 (#884) * Fix: EncodeClientParams (#868) * fix: encodeClientParams * test(client): added test for req url string * test(client): variable name changed * test: added test in encodeClientSpec * simplifies test case * doc: add comment on why relative path needs to be used. Co-authored-by: Tushar Mathur * Refactor: HttpRunnableSpec clean up (#857) * refactor: reduce HttpRunnableSpec boilerplate * refactor: remove all helpers from HttpRunnableSpec and inline the method * doc: update documentation for test module * refactor: add type-constraints to Http for specialized methods * refactor: use IsResponse type constraint * style(*): apply scala fmt * doc: update documentation * update doc (#897) * Docs: Getting started (#887) * fix: added more content * fixed comments * fix: getting started * modifications * added more * minor changes * minor changes * changes * refactor: rename asString to encode in URI (#898) * refactor: rename asString to encode in Scheme (#904) * fix: example links (#906) * Update getting-started.md (#907) * refactor: rename asString to encode in Scheme (#905) * Doc: setup (#886) * doc: setup * resolve: PR comments * Update netty-incubator-transport-native-io_uring to 0.0.12.Final (#908) * Documentation for Server (#885) * removed config.md and make configurations part of Server page * added server configurations * markdwon appearing fine in docusaurus generated page * added one missing configuration * Server documentation changed according to PR comments * change note to tip Co-authored-by: Sumant Awasthi Co-authored-by: amitsingh * Performance: Improve performance of `collectM` (#882) * Use ZIO response * fix: improve cancellation performance * refactor: use sticky server * refactor: reduce allocations * revert example * Doc: Added <> operator in getting started (#910) * fix: ++ operator doc * fix: composition * feature: Add `collectManaged` to Http (#909) * Add collectManaged * comment typo * feature: add `echo` operator to `Socket` (#900) * feature: add `Socket.empty` (#901) * feature: add `Socket.empty` * test: add unit test case for Socket#empty Co-authored-by: Shubham Girdhar * feature: add `toHttp` to Socket.scala (#902) * feature: add `toHttp` to Socket.scala * test: add unit test for `toApp` to Socket.scala Co-authored-by: Shubham Girdhar * refactor: merge Request for Client and Server (#894) * Revert "refactor: merge Request for Client and Server (#894)" (#915) This reverts commit fc8b9b5a155e7c59b548d191265d30e0e6b61d8b. * Feature: add `toHttp` to Response (#903) * feature: add `wrapHttp` to Response * test: add unit test for wrapHttp in Response.scala * refactor: operator name changed to `toHttp` Co-authored-by: Shubham Girdhar * Update scalafmt-core to 3.4.0 (#920) * Update sbt to 1.6.2 (#931) * Remove outdated benchmark.md (#940) * removed benchmark file * removed benchmark hyperlink from readme * refactor: rename `getHeaders` to `headers` in `ClientRequest` (#928) * refactor: rename `getHeaders` to `headers` in `ClientRequest` * fix: compiler errors * fix: compiler errors * Refactor: rename `asString` to `encode` in `Path` (#927) * refactor: rename `asString` to `encode` in Path * fix: compiler errors * Disable flow Control (#854) * Add builder pattern for URL (#930) * Add builder pattern for URL * Compare encoded value to string literal * Documentation: Http (#888) * initial commit for http documentation * refactor: adding bold formatting for HTTP and resolving review comments * refactor: Chnage app to application in http documentation * refactor:fixed review comments * added combinators section * style: formatting the code blocks * added getBodyAsString to http documentation * added HttpApp section * fixing typos * added mapZIO and contamapZIO documentation for http * provide layer documentation for http * doc: added a secion- Running an HttpApp Co-authored-by: Dino John * feature: add `Http.apply` (#949) * Refactor: Drop `toZIO` and `wrapZIO` APIs (#950) * feature: add `Http.apply` * refactor: remove ZIO wrapping APIs * Fix: Server KeepAlive true by default and enable client to set Http version on requests (#792) * IT cases for KeepAlive included in ServerConfigSpec * Allow possibility of modifying server conf in HttpRunnableSpec * reverted Header.scala * reverted Header.scala * reverted Header.scala * struggling with fmt * pull latest from main * reverting changes in ci files * included review recommendations * Concise test description * removed extra comments * readjusted ServerConfigSpec * enable keep alive by default; added more test cases for Http 1.0; tweaked test cases and description * removed configurableServe * removed unnecessary lines; shorter tests * reverted Server disabled and some spec changes * Test/keepalive httpversion (#800) * make httpVersion first param in request methods * tweaked test cases even further using requestHeaderValueByName * changed names * resolved comments, and more tweaking of test * deleted extra line Co-authored-by: Sumant Awasthi * Renamed ServerConfigSpec to KeepAliveSpec * made httpVersion first param in ClientParams, made relevant changes * in EncodeClientParams use http version from ClientParams * a minor change * changed server keepAlive = true by default, also rebased with main Co-authored-by: Sumant Awasthi * feat: url add `isAbsolute` and `isRelative` operators (#946) * refactor: add type-params * feat: url add `isAbsolute` and `isRelative` operators # Conflicts: # zio-http/src/main/scala/zhttp/http/URL.scala * WebSocket Client Support (#933) * wip: websocket client support * test: add unit test for WebSocket using native websocket client * fix: fromZIO in websocket client spec * refactor: queue usage simplified * feat: websocket client support * refactor: imports optimized * doc: for ClientSocketHandler * refactor: bootstrap method * websocket client with response support * feat: wss support * refactor: SocketProtocol * refactor: add SocketClient example: add WebSocketSimpleClient * refactor: SocketProtocol with type evidence refactor: general clean up * test: for Scheme * refactor: rename asString to encode in URI * refactor: remove example of SecureClient * refactor: url doesn't need to be part of the protocol. Url can be passed as an additional parameter for now. This simplifies the overall design. * refactor: socket client for now will take the complete url * refactor: update client example * refactor: add `connect` operator to SocketApp * refactor: use `url` as a string instead of URL type * refactor: re-order methods * style(*): apply scala fmt * revert: unnecessary changes * feature: add `echo` operator to `Socket` * refactor: update type info * feature: add `Socket.empty` * feature: add `wrapHttp` to Response * feature: add `toHttp` to Socket.scala * style(*): apply scala fmt * refactor: rename asString to encode in Scheme * refactor: cleanup Scheme changes * test: update test assertion * refactor: combine server and client handlers * refactor: add hint in touch for ClientSocketUpgradeHandler * refactor: change package for SocketAppHandler * refactor: rename methods in SocketProtocol * fix: add host header and port in Request automatically * fix: example links (#906) * Update getting-started.md (#907) * refactor: rename asString to encode in Scheme (#905) * Doc: setup (#886) * doc: setup * resolve: PR comments * Update netty-incubator-transport-native-io_uring to 0.0.12.Final (#908) * Documentation for Server (#885) * removed config.md and make configurations part of Server page * added server configurations * markdwon appearing fine in docusaurus generated page * added one missing configuration * Server documentation changed according to PR comments * change note to tip Co-authored-by: Sumant Awasthi Co-authored-by: amitsingh * Performance: Improve performance of `collectM` (#882) * Use ZIO response * fix: improve cancellation performance * refactor: use sticky server * refactor: reduce allocations * revert example * Doc: Added <> operator in getting started (#910) * fix: ++ operator doc * fix: composition * feature: Add `collectManaged` to Http (#909) * Add collectManaged * comment typo * feature: add `echo` operator to `Socket` (#900) * feature: add `Socket.empty` (#901) * feature: add `Socket.empty` * test: add unit test case for Socket#empty Co-authored-by: Shubham Girdhar * refactor: add attribute to Request created by Handler * refactor: remove "case" from ClientInboundHandler * refactor: rename to WebSocketAppHandler * feature: add `toHttp` to Socket.scala (#902) * feature: add `toHttp` to Socket.scala * test: add unit test for `toApp` to Socket.scala Co-authored-by: Shubham Girdhar * cleaning up residue files * refactor: rename `asString` to `encode` in Path * refactor: rename `getHeaders` to `headers` in `ClientRequest` * refactor: simplify client API * refactor: update client params encoder * style: ordering methods in URL.scala * refactor: ClientRequest will throw if the url is invalid * refactor: pass URL instead of String in ClientRequest * test: fix `GetBodyAsStringSpec` * style(*): apply scala fmt * refactor: fix invalid url being passed in HttpRunnableSpec * refactor: Move SSL requirements to ClientAttirbute * feature: add `scheme` operator on URL * feature: add `IsWebSocket` and `isSecure` operators on Scheme * refactor: use named imports in WebSocketAppHandler * test: fix base url for websockets * refactor: update how flags are set inside client * test: add non-zio spec to the spec list * refactor: fix client test * style: scalafmt error and remove TODO * refactor: remove unused handler * refactor: remove host header changes and RequestSpec (#945) * refactor: move java scheme generators to SchemeSpec (#944) * refactor: move java scheme generators to SchemeSpec * refactor: resolve PR comments * feat: url add `isAbsolute` and `isRelative` operators * refactor: rename EncodeClientParams.scala to EncodeClientRequest * fix: handle errors on client connection * style(*): apply scala fmt * refactor: simplify multiple websocket upgrades test (#948) * refactor: resolve PR comments style: fmt fix * refactor: general refactor and style changes Co-authored-by: Shubham Girdhar Co-authored-by: Shruti Verma <62893271+ShrutiVerma97@users.noreply.github.com> Co-authored-by: Amit Kumar Singh Co-authored-by: Scala Steward <43047562+scala-steward@users.noreply.github.com> Co-authored-by: sumawa Co-authored-by: Sumant Awasthi Co-authored-by: amitsingh Co-authored-by: James Beem * Documentation: Request (#926) * docs: request * doc: added client request docs * added query param section * doc: refactoring * WIP * fix: doc * fix: doc * request * request * example fixed * Fix ssl issue due to missing peer host port hint (#952) * feature: add connect operator on Socket (#955) * Style: Update scala doc wrapping (#959) * chore: update scalafmt config * style(*): apply scala fmt * remove get prefix (#958) * Update scalafmt-core to 3.4.1 (#960) * Update scalafmt-core to 3.4.2 (#962) * Update scalafmt-core to 3.4.2 (#961) * remove declarative API (#957) * Update sbt-updates to 0.6.2 (#966) * Doc: Readme (#970) * fix: readme * changed note * style: rearrange methods in files (#963) * Doc: Cookie (#974) * doc: cookie * doc: cookie * cookie: changes * refactor: drop `CanBeSilent` type-constraint from Http (#964) * refactor: drop `CanBeSilent` type-constraint from Http * update usage of `silent` operator * Feature: add `Version` (#965) * refactor: rename `asHttpMethod` to `toJava` * refactor: add `Version` domain * style(*): apply scala fmt * Fix: Fire response instead of error in HExit.Failure() (#980) * Bug fix: fixes #979 * Made changes to nonZIOSpec in ServerSpec to include tests for HExit.Failure() * fix: typo in bug_report.md (#981) * Update netty-all to 4.1.74.Final (#982) * refactor: remove sealed modifier from Middleware trait (#984) * refactor: rename method on Client (#985) * fix: request doc (#989) * Use a Gen of methods without HEAD for Responses with content (#992) * doc: setup g8 (#988) * doc: setup g8 * refactor: bump zhttp version * Doc: for WebSocketFrame (#953) * doc: for WebSocketFrame * resolve: PR comments * resolve: PR comments * Documentation for HttpData (#987) * Documentation for HttpData * added server and client side usage of HttpData * Update docs/website/docs/v1.x/dsl/http-data/index.md Co-authored-by: Amit Kumar Singh * refactor: rename `provide` and `provideSome` (#996) * Fix: `Content-Type` header gets incorrect values. (#899) * initial work * Fix tests * Perf enhancements * compilation error fix * rename getContentType * Update zio-http/src/main/scala/zhttp/http/MediaType.scala Co-authored-by: Tushar Mathur * Make MediaType private, and final. Co-authored-by: Tushar Mathur * Feat: Http from HExit (#986) * added 2 operators to create http from HExit * added tests for collectHExit and fromFunctionHExit * rewrote nonZIO app in serverSpec using collectHExit * Refactor: Remove Collect and FromFunctionZIO (#1002) * Refactor: Implemented FromFunctionZIO using FromFunctionHExit * Refactor: Implement Collect using FromFunctionHExit * Fix: Binary WebSocketFrame (#1005) * fix: change binary websocket frame arg to Chunk[Byte] from ByteBuf Closes #1004 * test: add unit test for binary websocketframe * style: fix formatting * feature: add `fromFileZIO` (#1010) * refactor: `fromFile` uses `fromFileZIO` * feature: add `Http.fromResource` (#1009) * Refactor: Content Type Fixes (#1008) * style: scalafmt fixes * style: sorting alphabetically * fix: set conten-type only if not set already * refactor: make internal fields private * doc: add todo * refactor: make caching optional * refactor: use `Http.fromResource` in test * refactor: fix test * doc: fix binary constructor usage (#1014) * Add data in `Request` trait (#1017) * maintenance: run scalafmt once (#1027) * Build(deps): Bump follow-redirects in /docs/website (#1029) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update scalafmt-core to 3.4.3 (#1022) * Remove tuple from ResponseHandler (#1033) * Remove tuple from ResponseHandler * cleanup * restructuring (#1040) * added more examples (#1038) * Doc: Response (#967) * updated with main * added operators * revert * setstatus change * renaming (#1047) * Fix: Releasing request twice (#1046) * refactor: not releasing request * Added failing test * optimize path encode method (#1035) * Feature: Static Server (#1024) * WIP : static server from file path * fix CI errrors * format * Basic functionalities work without options * Fix errors on java8 * Fix errors on java8 * Fix refactoring error * clean up * Fixig tests * working around file type detection bug * Method not allowed. * Better comments. * Update zio-http/src/main/scala/zhttp/http/Http.scala Co-authored-by: Tushar Mathur * Refactor * More tests. * Fix PR comments * Moved headers to fromPath * Update zio-http/src/main/scala/zhttp/http/Http.scala Co-authored-by: Tushar Mathur * Update zio-http/src/main/scala/zhttp/http/Http.scala Co-authored-by: Tushar Mathur * Fixes. * Fixing tests * Update zio-http/src/main/scala/zhttp/http/HttpData.scala Co-authored-by: Tushar Mathur * Update zio-http/src/main/scala/zhttp/http/HttpData.scala Co-authored-by: Tushar Mathur * Update zio-http/src/main/scala/zhttp/http/Http.scala Co-authored-by: Tushar Mathur * PR is closed * Used Http.fromFile in implementation * Remove unwanted commits * Fixing compile issues. * feature: add `withMediaType` operator in HeaderModifier * refactor: drop `setMediaType` from Response * refactor: inline file creation * refactor: simplify implementation for fromFile and fromFileZIO * doc: add todo comments * refactor: update listFilesHtml implementation * refactor: update scaladoc * test: fix refactored misses * test: use `+` instead of `,` * style: scalafmt * feature: add `mediaType` to HeaderGetters * feature: add Http.attempt` operator * refactor: MediaType works on extensions directly * refactor: add tests for fromResource * test: StaticFileServerSpec add tests for directory * refactor: make `fromPath` use `fromFile` internally * refactor: simplify `fromPath` * refactor: update scala docs for Http operators * refactor: fix type info of `Http.response` * feature: add `listDirectory` operator * feature: add StyledContainerHtml * refactor: drop `Util` * refactor: inline util HTML constructors * test: add test for invalid file size * doc: update comments * style: reorder methods in Http Co-authored-by: ashprakasan * Revert "Feature: Static Server (#1024)" (#1060) This reverts commit 33ac08d1a528143dfbcfcbee2d3202b31abbbd30. * Performance: Use `CharSequence` for ServerTime (#1064) * feature: use CharSequence in for ServerTime * style: scalafmt * Feature: add `dropLast` to Path (#1065) * feature: add `dropLast` to Path * style: scalafmt * feature: Response.html now supports taking in Status also (#1067) * feature: add `code` method on Status (#1068) * refactor: fix type info of `Http.response` (#1073) * feature: add `foldCause` to HttpError (#1066) * feature: add `withMediaType` operator in HeaderModifier (#1071) * Feature: add `Http.attempt` operator (#1070) * feature: add Http.attempt` operator * test: add tests for Http.attempt * feature: add `getResource` and `getResourceAsFile` operators on Http (#1074) * Update jwt-core to 9.0.4 (#1056) * Update sbt-bloop to 1.4.13 (#1055) * Build(deps): Bump url-parse from 1.5.4 to 1.5.7 in /docs/website (#1062) Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.4 to 1.5.7. - [Release notes](https://github.com/unshiftio/url-parse/releases) - [Commits](https://github.com/unshiftio/url-parse/compare/1.5.4...1.5.7) --- updated-dependencies: - dependency-name: url-parse dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feature: add `ifModifiedSinceDecoded` (#1075) * Feature: Static Server (#1061) * WIP : static server from file path * fix CI errrors * format * Basic functionalities work without options * Fix errors on java8 * Fix errors on java8 * Fix refactoring error * clean up * Fixig tests * working around file type detection bug * Method not allowed. * Better comments. * Update zio-http/src/main/scala/zhttp/http/Http.scala Co-authored-by: Tushar Mathur * Refactor * More tests. * Fix PR comments * Moved headers to fromPath * Update zio-http/src/main/scala/zhttp/http/Http.scala Co-authored-by: Tushar Mathur * Update zio-http/src/main/scala/zhttp/http/Http.scala Co-authored-by: Tushar Mathur * Fixes. * Fixing tests * Update zio-http/src/main/scala/zhttp/http/HttpData.scala Co-authored-by: Tushar Mathur * Update zio-http/src/main/scala/zhttp/http/HttpData.scala Co-authored-by: Tushar Mathur * Update zio-http/src/main/scala/zhttp/http/Http.scala Co-authored-by: Tushar Mathur * PR is closed * Used Http.fromFile in implementation * Remove unwanted commits * Fixing compile issues. * feature: add `withMediaType` operator in HeaderModifier * refactor: drop `setMediaType` from Response * refactor: inline file creation * refactor: simplify implementation for fromFile and fromFileZIO * doc: add todo comments * refactor: update listFilesHtml implementation * refactor: update scaladoc * test: fix refactored misses * test: use `+` instead of `,` * style: scalafmt * feature: add `mediaType` to HeaderGetters * feature: add Http.attempt` operator * refactor: MediaType works on extensions directly * refactor: add tests for fromResource * test: StaticFileServerSpec add tests for directory * refactor: make `fromPath` use `fromFile` internally * refactor: simplify `fromPath` * refactor: update scala docs for Http operators * refactor: fix type info of `Http.response` * feature: add `listDirectory` operator * feature: add StyledContainerHtml * refactor: drop `Util` * refactor: inline util HTML constructors * test: add test for invalid file size * doc: update comments * style: reorder methods in Http * refactor: improve style of StyledContainerHtml * refactor: drop directory support * refactor: drop directory listing * refactor: add a color to background * feature: add `getResource` and `getResourceAsFile` operators on Http * feature: add `foldCause` to HttpError * feature: add `code` method on Status * feature: StyleContainerHtml max-width updated * feature: Handler uses `HttpError` for empty responses * feature: Response.html now supports taking in Status also * feature: beautify Response.fromHttpError * refactor: rename file to Template * feature: add `dropLast` to Path * feature: add `Http.template` operator * example: update static server example * style: scalafmt updates * feature: use CharSequence in for ServerTime * feature: add `ifModifiedSinceDecoded` * feature: add `ifModifiedSinceDecoded` * feature: add `ifModifiedSinceDecoded` Co-authored-by: ashprakasan * issue 715 Support custom ChannelInitializer (#932) * doc: socket (#1036) * doc: socket * refactor: clean up * refactor: resolve PR comment * fix: resolve PR comment and fix typo in `merge` * feat: added new constructor in http (#1077) * Feature: Add `when` operator in `Http` (#1078) * perf: added when operator in http * build fix * removed jrequest from constructor * fmt * unsafeEncode: HttpRequest * added when primitive * refactor: rename `whenPath` to `whenPathEq` Co-authored-by: Tushar Mathur * Feature: Effectful Auth Middleware (#1079) * added effectful athu middlewares * added tests * refactor: stopped calling tuple of credentials as Header * added credentials * Test: Server Settings Support in `HttpRunnableSpec#serve` (#1053) * feat: server settings support in HttpRunnableSpec#serve * resolve: PR comments * refactor: website docs sidebar pages repositioned (#1049) * Feat: Server Request Decompression (#1095) * feat: server request decompression * test: add test for deflate * Feature: Added helper checkers to `Status` class (#1058) * Added helper checkers to Status class * For status, formatted, removed and made them def Co-authored-by: Roberto Leibman * Build(deps): Bump prismjs from 1.26.0 to 1.27.0 in /docs/website (#1100) Bumps [prismjs](https://github.com/PrismJS/prism) from 1.26.0 to 1.27.0. - [Release notes](https://github.com/PrismJS/prism/releases) - [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md) - [Commits](https://github.com/PrismJS/prism/compare/v1.26.0...v1.27.0) --- updated-dependencies: - dependency-name: prismjs dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Build(deps): Bump url-parse from 1.5.7 to 1.5.10 in /docs/website (#1103) Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.7 to 1.5.10. - [Release notes](https://github.com/unshiftio/url-parse/releases) - [Commits](https://github.com/unshiftio/url-parse/compare/1.5.7...1.5.10) --- updated-dependencies: - dependency-name: url-parse dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * added try catch in handler to catch throwable apps (#1099) * removed flatten from test (#1109) * Feature: Request Streaming (#1048) * introduce `Incoming` and `Outgoing` inHttpData * streaming support * benchmark disable objectAggregator * cleanup * refactor * cleanup + PR comments * cleanup + PR comments * cleanup + PR comments * refactor: rename variable * memory leak * refactor: Handler now extends ChannelInboundHandlerAdapter * refactor: remove unused methods from UnsafeChannel * remove bodyAsCharSequenceStream operator * refactor: remove unnecessary methods on HttpData * refactor: re-implement `bodyAsStream` * refactor: remove unsafe modification of pipeline from HttpData * refactor: rename HttpData types * fix 2.12 build * refactor: remove type param * PR comment * PR comment * refaector: simplify releaseRequest * refactor: reorder methods in ServerResponseHandler * refactor: make methods final * refactor: rename HttpData traits * add `bodyAsByteArray` and derive `body` and `bodyAsString` from it. * add test: should throw error for HttpData.Incoming * Introduce `useAggregator` method on settings and use it everywhere * remove sharable from `ServerResponseHandler` * Update zio-http/src/main/scala/zhttp/http/Request.scala * refactor: remove unnecessary pattern matching * throw exception on unknown message type * simplify test * refactor: change order of ContentHandler. Move it before the RequestHandler * test: update test structure * refactor: move pattern match logic to WebSocketUpgrade * revert addBefore Change because of degrade in performance (#1089) * fix static server issue with streaming * take case of auto read if body is not used * autoRead when needed * Update zio-http/src/main/scala/zhttp/service/RequestBodyHandler.scala * Update zio-http/src/main/scala/zhttp/http/Response.scala * Update zio-http/src/main/scala/zhttp/http/Response.scala * remove test which is not used * Update zio-http/src/main/scala/zhttp/service/Handler.scala Co-authored-by: Shrey Mehta <36622672+smehta91@users.noreply.github.com> * Update zio-http/src/main/scala/zhttp/service/Handler.scala Co-authored-by: Shrey Mehta <36622672+smehta91@users.noreply.github.com> * style: fmt * exclude Head in 404 check Co-authored-by: Tushar Mathur Co-authored-by: Shrey Mehta <36622672+smehta91@users.noreply.github.com> * Fix toByteBuf for streamed HttpData (#1118) * Fix toByteBuf for streamed HttpData * example: more secure string compare for login (#1120) * Feature: Introduced defect channel for `Http` (#1083) * Introduced defect channel for Http and added helpful combinators for error handling * Reformat code * Removed doubtful combinator Http.run * Simplified implementation of catchNonFatalOrDie * Simplified implementation of catchSome * Guarded Http.execute() with try-catch block in order to convert unexpectable exceptions to defects * Added test to check Http.catchSomeDefect catches throws defects * Fixed warnings about shadowing type parameters * Fix: fold over defect cause * Formatted HttpSpec.scala * Perf: catch defects only where they may occur * Fmt Co-authored-by: Nikolay Artamonov * Enhancement: Merge server and client Responses (#1111) * Merge client and server Request * Remove IsResponse * rename getBodyAsString => bodyAsString in doc (#1124) * removed links from scaladoc (#1127) * Feature: Add `version` to `Request` (#1094) * introduce `Incoming` and `Outgoing` inHttpData * streaming support * benchmark disable objectAggregator * cleanup * refactor * cleanup + PR comments * cleanup + PR comments * cleanup + PR comments * refactor: rename variable * memory leak * refactor: Handler now extends ChannelInboundHandlerAdapter * refactor: remove unused methods from UnsafeChannel * remove bodyAsCharSequenceStream operator * refactor: remove unnecessary methods on HttpData * refactor: re-implement `bodyAsStream` * refactor: remove unsafe modification of pipeline from HttpData * refactor: rename HttpData types * fix 2.12 build * refactor: remove type param * PR comment * PR comment * refaector: simplify releaseRequest * refactor: reorder methods in ServerResponseHandler * refactor: make methods final * refactor: rename HttpData traits * add `bodyAsByteArray` and derive `body` and `bodyAsString` from it. * add test: should throw error for HttpData.Incoming * Introduce `useAggregator` method on settings and use it everywhere * remove sharable from `ServerResponseHandler` * Update zio-http/src/main/scala/zhttp/http/Request.scala * refactor: remove unnecessary pattern matching * throw exception on unknown message type * simplify test * refactor: change order of ContentHandler. Move it before the RequestHandler * test: update test structure * refactor: move pattern match logic to WebSocketUpgrade * revert addBefore Change because of degrade in performance (#1089) * fix static server issue with streaming * Introduce version in Request * Delete `Request.make` * add missing scaladoc Co-authored-by: Tushar Mathur * Refactor: Merge client and server `Request` (#1125) * introduce `Incoming` and `Outgoing` inHttpData * streaming support * benchmark disable objectAggregator * cleanup * refactor * cleanup + PR comments * cleanup + PR comments * cleanup + PR comments * refactor: rename variable * memory leak * refactor: Handler now extends ChannelInboundHandlerAdapter * refactor: remove unused methods from UnsafeChannel * remove bodyAsCharSequenceStream operator * refactor: remove unnecessary methods on HttpData * refactor: re-implement `bodyAsStream` * refactor: remove unsafe modification of pipeline from HttpData * refactor: rename HttpData types * fix 2.12 build * refactor: remove type param * PR comment * PR comment * refaector: simplify releaseRequest * refactor: reorder methods in ServerResponseHandler * refactor: make methods final * refactor: rename HttpData traits * add `bodyAsByteArray` and derive `body` and `bodyAsString` from it. * add test: should throw error for HttpData.Incoming * Introduce `useAggregator` method on settings and use it everywhere * remove sharable from `ServerResponseHandler` * Update zio-http/src/main/scala/zhttp/http/Request.scala * refactor: remove unnecessary pattern matching * throw exception on unknown message type * simplify test * refactor: change order of ContentHandler. Move it before the RequestHandler * test: update test structure * refactor: move pattern match logic to WebSocketUpgrade * revert addBefore Change because of degrade in performance (#1089) * fix static server issue with streaming * Introduce version in Request * Merge Client and Server Request * Delete `Request.make` * Gen refactor * rename files * make function private * rename attribute * rename attribute Co-authored-by: Tushar Mathur * Refactor: Http.Status names to Camel Case (#1129) * support custom Status for HtppError * support custom Status for HttpError * updated code based on review. * fixed tests * changed from Capitalized Status names to Camel Case * proper camel case usage Co-authored-by: Gabriel Ciuloaica * Feature: Support Custom statuses code (#1121) * support custom Status for HtppError * support custom Status for HttpError * updated code based on review. * fixed tests * Refactor: Http.Status names to Camel Case (#1128) * changed from Capitalized Status names to Camel Case * proper camel case usage * Update zio-http/src/main/scala/zhttp/http/HttpError.scala Co-authored-by: Tushar Mathur * fixed after conflict * updated tests Co-authored-by: Tushar Mathur * Enhancement: Added combine operator (#1106) * enhancement: added combine operator * added test cases with three apps * refactor: plaintextBenchmarkServer * test case updated * removed flatten from test * test case updated * added test for collectHttp * simplified tests * added failing test * fixed test * refactor: cleaning up the execute method in Http Co-authored-by: Tushar Mathur * Refactor: Added lift in PartialCollect (#1105) * refactor: added lift in partialCollect * added text in path * refactor: reduce iterations of benchmarks Co-authored-by: Tushar Mathur * Feature: Bearer auth middleware (#1097) * added header constructor for bearer authorization * added jwt auth middlewares * updated the Auth examples * added tests * renamed the auth middleware from jwt to barer * refactor: example AuthenticationClient * added scala doc for examples, renamed variables in tests Co-authored-by: Shubham Girdhar Co-authored-by: amitsingh * Update netty-all to 4.1.75.Final (#1130) * Refactor: `Request` and `Response` to extend `HttpDataExtension` (#1112) * Request Streaming Example * refactor: add HttpDataExtension * doc: add documentation * fix: compiler errors Co-authored-by: Tushar Mathur * Refactor: Make `Http.getResource` consistent with `ZStream.fromResource` (#1113) * Fix `Http.getResource` Code based on https://github.com/zio/zio/blob/v1.0.13/streams/jvm/src/main/scala/zio/stream/platform.scala#L385-L398 * Improve `File` creation lazyness: The `File` instance will be created much later in the pipe * Fix tests * Fix tests * Update zio-http/src/main/scala/zhttp/http/Http.scala Co-authored-by: Tushar Mathur * Remove `Blocking` from the type signature as requested Co-authored-by: Tushar Mathur * Update netty-incubator-transport-native-io_uring to 0.0.13.Final (#1133) * Add more useful toString method for Request (#995) * Add more useful toString method for Request * Fmt * Moved toString to Request trait * Added a few tests of Request.toString * Fmt * Improved tests for Request.toString * Added Scala doc for Request.toString * Added protocol version to a string representation of Request * Fmt * fix(request): fix scala doc (#1138) * Performance: Improve HttpData toByteBuf (#1137) * refactor: add blocking layer for file based constructors * refactor: use JavaFile instead of RandomAccessFile * doc: fix StaticServer example * style: scalafmt * refactor: clean up HttpData * refactor: remove blocking * refactor: remove unnecessary overload in ResponseHandler * test: add timeout and use ByteBufConfig to control encoding * refactor: add ByteBufConfig * refactor: rename types internally * style: fmt Co-authored-by: amitsingh * Fix: Handle HttpClient Interruption on connection close (#1039) * the promise has to be uninteruptible to avoid loosing the response message in case the netty chanel is getting closed due to a unhandled exception * added a note about reasons why the promise is made uninterruptible. * New chapter: efficient development process (#1145) The dream11 gitter template already contains the most important sbt plugins to setup an efficient development process. To guide newcomers to zio-http, the newely added chapter gives an introduction on their purpose and how to use them. * do not watch reStop (#1146) Removed the tilde (~) before reStop, as it is not helpful to run this in watch-mode * Feature: add `Middleware.codecHttp` (#1141) * feature: add codecHttp * feature: add `codecMiddleware` to Http * added tests Co-authored-by: shrutiverma97 * Performance: Use `CharSequence` internally wherever possible (#1142) * refactor: add blocking layer for file based constructors * refactor: use JavaFile instead of RandomAccessFile * doc: fix StaticServer example * style: scalafmt * refactor: clean up HttpData * refactor: remove blocking * refactor: remove unnecessary overload in ResponseHandler * test: add timeout and use ByteBufConfig to control encoding * refactor: add ByteBufConfig * performance: use `AsciiString` inside of HttpData * performance: use CharSequence in `Http.template` and `Http.text` * performance: use `CharSequence` in Response.json, Response.text and Response.redirect * performance: use `CharSequence` in Html templates * refactor: add `bodyAsCharSequence` to HttpDataExtension * Update scala-collection-compat to 2.7.0 (#1154) * Update jwt-core to 9.0.5 (#1150) * feature: add `narrow` operator to Http (#1161) * feature: add `toHttp` method to `Response` (#1160) * refactor: DynamicServer is not bound to HttpEnv only (#1159) * fix: Socket.end implementation (#1158) * Update sbt-scala3-migrate to 0.5.1 (#1163) * Fix: update implementation of `FromAsciiString.encode` (#1175) * fix: update implementation of `FromAsciiString.encode` * Add test for toHttp FromASCIIString Co-authored-by: amitsingh * Build(deps): Bump minimist from 1.2.5 to 1.2.6 in /docs/website (#1172) Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6) --- updated-dependencies: - dependency-name: minimist dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Added missing period (#1186) * Update scalafmt-core to 3.5.0 (#1180) * Update sbt-scalafix to 0.10.0 (#1192) * Added workflow for Jmh benchmarks (#1135) * ci: added workflow for jmh benchmarks * workflow should run on label * added if in workflow * main and current in one machine * main and current in one machine * on push * removed java setup * format output * fomatting * added label trigger * Feature: add `delay` `tap` operator to Socket (#1157) * feature: add `delay` `tap` operator to Socket feature: add `tap` operator to Socket * Feature: add `from` and `fromIterable` * feature: add `narrow` operator to Http (#1161) * feature: add `toHttp` method to `Response` (#1160) * refactor: DynamicServer is not bound to HttpEnv only (#1159) * fix: Socket.end implementation (#1158) * Update sbt-scala3-migrate to 0.5.1 (#1163) * Fix: update implementation of `FromAsciiString.encode` (#1175) * fix: update implementation of `FromAsciiString.encode` * Add test for toHttp FromASCIIString Co-authored-by: amitsingh * Build(deps): Bump minimist from 1.2.5 to 1.2.6 in /docs/website (#1172) Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6) --- updated-dependencies: - dependency-name: minimist dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * test: add tests for `tap` and `delay` * test: update test Co-authored-by: amitsingh Co-authored-by: Amit Kumar Singh Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * getting started broken links fixed (#1196) * Update scala3-library to 3.1.2 (#1203) * Update scala3-library to 3.1.2 * Regenerate workflow with sbt-github-actions * Update netty-all to 4.1.76.Final (#1202) * Update zio, zio-streams, zio-test, ... to 1.0.14 (#1201) * Update scala3-library to 3.1.2 (#1197) * Update scala3-library to 3.1.2 * Regenerate workflow with sbt-github-actions * Added implicit conversion from Unit to Html (#1204) * Fix: Handle on-close interruption in websocket (#1156) * feature: add `delay` `tap` operator to Socket feature: add `tap` operator to Socket * fix: for websocket onClose * fix: for websocket onClose * feature: add `delay` `tap` operator to Socket feature: add `tap` operator to Socket * test for the onClose test: add WebSocketAppHandler test for close handler * refactor: use a promise based test * refactor: simplify `writeAndFlush` implementation in service * refactor: use Socket.end in test instead of closing connection * style: scalafmt * wip: add debug logs for upgrade * style: scalafmt * style: scalafmt * test: add debug log * wip: update log * style: scalafmt * wip: update Logging * refactor: fix spec * style: scalafmt * wip: trying to fix the test * revert: remove loggin * Feature: add `from` and `fromIterable` * feature: add `narrow` operator to Http (#1161) * feature: add `toHttp` method to `Response` (#1160) * refactor: DynamicServer is not bound to HttpEnv only (#1159) * fix: Socket.end implementation (#1158) * Update sbt-scala3-migrate to 0.5.1 (#1163) * Fix: update implementation of `FromAsciiString.encode` (#1175) * fix: update implementation of `FromAsciiString.encode` * Add test for toHttp FromASCIIString Co-authored-by: amitsingh * Build(deps): Bump minimist from 1.2.5 to 1.2.6 in /docs/website (#1172) Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6) --- updated-dependencies: - dependency-name: minimist dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * test: add tests for `tap` and `delay` * test: update test Co-authored-by: Tushar Mathur Co-authored-by: Shubham Girdhar Co-authored-by: Gabriel Ciuloaica Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update scalafmt-core to 3.5.1 (#1212) * Update scalafmt-core to 3.5.1 * Reformat with scalafmt 3.5.1 * maintenance: bump version in website docs (#1177) * Refactor: ServerResponseHandler (#1198) * passed as instance * refactor * refactored ServerResponseHandler in zhttp.service * refactor * sealed trait changed to case class * moved ctx to package * private type * revert changes * renaming * removed config * Update scalafmt-core to 3.5.2 (#1218) * Update sbt-bloop to 1.5.0 (#1224) * feat: added channel option to Server (#1210) * feat: added channel option to Server * feat: added unsafe unsafe serverbootstrap * feat: minor change in the fn description * feat: formatting fix * feat: update doc Co-authored-by: Tushar Mathur Co-authored-by: Tushar Mathur * refactor: move logic to ServerResponseWriter (#1223) * refactor: move logic to ServerResponseWriter * refactor: remove redundant parameter * test: update test for NotFound * test: update test for content-length on client * Feature: Add `merge` operator in `Http` (#1232) * feature: add Http.merge * style: use `,` instead of `+` to combine suites * test: add tests for Http.merge * add client server example (#1228) * Build(deps): Bump async from 2.6.3 to 2.6.4 in /docs/website (#1233) Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4. - [Release notes](https://github.com/caolan/async/releases) - [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md) - [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4) --- updated-dependencies: - dependency-name: async dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * refactor: rename property in serverConfig (#1243) * Fix: Request Streaming (#1242) * chore: run tests in parallel * fix: request body issues * test: fix server spec test for decompression * Update netty-all to 4.1.77.Final (#1241) * Fix: Echo `HttpData` directly from `Request` (#1244) * test: add failing test for echoing raw data * fix: fix unsafe cast issue * test: simplify test case * Restore request method on client as public (#1247) * add empty and allow middleware (#1251) * Use zExec.unsafeRunInterruptible where appropriate (#1253) As a fix for #1147 zExec.unsafeRunInterruptible was created, because when using the pattern zExec.unsafeRun(.uninterruptible) it was possible to be interrupted before was started causing callbacks to be missed. Replacing the above with zExec.unsafeRunInterruptible() fixed this. This commit replaces the remaining instances of this pattern with unsafeRunUninterruptible. At least once of these fixes a similar race condition observed in #1252. Co-authored-by: Dennis Noordsij * Update netty-incubator-transport-native-io_uring to 0.0.14.Final (#1248) * Update netty-incubator-transport-native-io_uring to 0.0.14.Final (#1249) * Fix: Request Streaming back pressure (#1188) * using a bounded queue of 1 element instead of ZIO.effectAsync to be able to control reading from channel. * integraiton test added * separated the streaming execution in its own test object * added unit test * fixed test for scala 2.12 * refactored tests after review * removed redundant test * refactor: move logic to ServerResponseWriter * refactor: remove UnsafeReadableChannel * added another example for File Streaming upload * fix tests * fixeed code after merge with main * removed unusefull example * formatting * added a request streaming integration test with larger file * refactor: cleanup UnsafeAsync * refactor: cleaning up the test * revert: serverspec * refactor: simplify the test Co-authored-by: Tushar Mathur * chore remove unused file (#1258) * chore: remove unnecessary dependencies (#1255) * refactor: Client.socket now returns a ZManaged (#1259) * Refactor: Drop `Endpoint` API (#1264) * feature: Provide access to `ChannelHandlerContext` using `Http`. * refactor: Dropping the Endpoint API * Update scalafmt-core to 3.5.4 (#1265) * Update scalafmt-core to 3.5.4 * Reformat with scalafmt 3.5.4 * Fix: Middleware type issues (#1269) * feature: add `IsMono` type-constraint * refactor: add `MonoMiddleware` * fix: type params in middleware.identity and middleware.allow, middleware.when * Update scalafmt-core to 3.5.5 (#1270) * Update scalafmt-core to 3.5.5 * Reformat with scalafmt 3.5.5 * Maintenance: Add internal Logging capabilities using `zhttp.log` (#1115) * implemented logging configuration to be able to invesigate netty layer issues * the low level logger should be off by default. logback config should be set to info to not log during tests. * wip * using internal logging * implemented simple console logging that is not allocated any memory for the log content when it is disabled or the log level is not matching the log level of the content. * improved the logging api * udated workflow and added more logging * removed unused file * removed unused code. * extended loggig settings in configuration to provide ability to set different logging levels. * added more logging and set benchmark in DEBBUG mode. * fixed merge * fixed formatting * changed logging level to INFO * change logging level for confing details * disabled logging from benchmark * doc: add scala dzio-http/src/test/scala/zhttp/internal/HttpGen.scalaoc * remove: support logging of throwable * refactor: rename params * refactor: reorder methods * refactor: Move ConsoleLogger into LogFrontend * refactor: Simplify LoggerFactory * refactor: use `Logger.make` for generating a Logger * refactor: simplify LoggerMacro for 2.13 * refactor: simplify LogFrontend * refactor: move LogLine inside of LogFormat * refactor: add apply method on LogFormat * refactor: add LogTransport * refactor: add Glogger * working version after refactoring * more refactoring * added location information, from where the log has been invoked * fixed after merge * fixed scala 2.12 compilation * migrating scala 3 macro wip * re-implemented based on new design. Single LoggerTransport for now is implemented. * refactor: removed InternalLogger and the macro sbt package * wip adding another transport * updated ci workflow * fixed scala 3 macro * formatted * fixed scala 3 macro * removed compiler warning for scala 3 * more simplifications * added file transport implementation * added integration tests * updated usage of logger in zio-http * updated tests to use api for java 8 only, added tags support in LogFormat * updated test to work also with scala 2.12 * more simplifications * fixed imports * updated server API to allow users of the API to provide a custom logger * more simplifications - the name of the logger was redundant as we have tags and location as part of the log line * updated example and added more log to server implementation * undo the exmple changes * refactor: rename logLevels * refactor: make LoggerFactory.Live private * refactor: clean up the logger a bit * refactor: make LogTransport private to Logger * doc: add scala doc in netty logger * doc: update scala doc * chore: remove unnecessary build changes * refactor: LogFrontend and LoggerTransport * style: fmt * feature: add `autodetectLevel` and `startsWith` operators on Logger * feat: add `detectFromEnv` method to LogLevel * refactor: update Scala doc from Logger.scala * refactor: disable logging by default while reading loglevel from env * refactor: rename file * refactor: simplify LogTransport * feature: add `dispatch` and operators to modify tags * perf: optimize Logger's macro implementation * scala 3 macro fixes and other cosmetic changes * logger transport will discard the log line based on log transport log level. * used SourcePos instead of passing 2 values around, only trace will provide source location information * fixed flaky test * style: reorder methods in Server * feature: add More LogDetection logic * refactor: drop `tags` from LogStatement * feature: add splat operator for withTags * refactor: add `detechLevelFromProps` * chore: update default app to HelloWorld * refactor: cleanup LogFormat * refactor: remove redundant `withTag` operator * refactor: cleanup log statements in Server * performance: optimize scala 3 output * removed logging from API. the Log instance is created with LogLevel read from env. If the ZHTTP_LOG_LEVEL env variable is not defined then the Log instance has LogLevel set to Disable. * formatting * formatting * more tests for logger using an in memory transport * fixed for scala 2.12 * fix for scala 3 * removed logger api documentation * added empty line * update log tests * macro updated to check if the level is enabled for a certain log level * trading maintenability for performance: extended LoggerMacroExtension to provide enable checks for each level. * added isEnabled flag in logger * chore: set default LogLevel to INFO * style: remove unnecessary bracket * refactor: add Logging.scala * refactor: update ChannelInitializer LogStatement * refactor: Update log statement inside of runtime * refactor: Change default LogLevel to `Error` * refactor: use `SimpleName` for class names * formatting * removed debug log statements. * rolled back last change * minor correction in test name * short circuit the logl level cheks when logger is disabled * refactor: add more operators to compare LogLevel * chore: update scala-settings to ignore unused parameters * refactor: add detectedLogLevel * refactor: change dispatch signature * refactor: add tests for LogLevel * refactor: rename formats * refactor: clean-up logLevel * refactor: drop `Disable` level for Logging * refactor: LoggerTransport to become an abstract class * refactor: simplify LoggerSpec * chore: update build * chore: update ScalaSettings * if the env value is not matching any defined log levels, than the log level will be set to Error. Co-authored-by: Tushar Mathur Co-authored-by: amitsingh * Fix: Use `check` instead of `checkAll` (#1266) * replaced with in tests where the generator was non-deterministic, fixed URL spec. * updated on Cookie * updated cookie implementation to fix failing tests. There is one breaking change on the Cookie API - Cookie.decodeRequestCookie is returning LIst[String] * removed debug code * in case of empty AsciiString the ByteBuf should be empty * rolled back previous change. According to http spec header SHOULD be present in case the conntent is empty. * refactor: drop `Http.route` (#1271) * Doc: Middleware (#999) * outline for middleware documentation * addded transforming output middleware * added more * minor changes * minor changes * revert * changes * removed folder * added links * review changes * doc: updated * Added some background about need for Middlewares and handling aspects * arranging some lines * added example of cleaner code using middlewares * correcting statement * rectifying grammatical mistakes * some more grammar clean up * restructuring the doc; explain concepts without using lots of code * presenting middlewares * inserted a new line * A more elaborate introduction to middleware in zio-http * trim example code * added more step by step examples * detailed example of the operator ++ * corrected a sentence * improve description of ++ operator * improve description of ++ operator * simple examples for operators * mention combinators * correcting HTTP definition * incorporate feedback * difference b/w ++ and >>> operators * cover only selective composition operators in detail * concise description of operators * clarifying middleware creation * more cleaner and easy to understand definition of an aspect * more cleaner and easy to understand definition of an aspect * more cleaner and easy to understand definition of an aspect * clarifying identity function * clarifying aspects a little more * clarifying aspects a little more * clarifying aspects a little more * elaborate benefits of using middlewares * elaborate benefits of using middlewares * elaborate benefits of using middlewares * explain @@ operator * more explanation for identity middleware * more explanation for middleware type parameters * more explanation for middleware type parameters * more explanation for middleware type parameters * more explanation for middleware type parameters * more explanation for middleware type parameters * Re-structuring the document based on non-scala programmer's feedback * Correcting code snippets * Correcting code snippets * grammar correction * grammar correction * Added advanced example to show middleware transformations * incorporating review comments * grammarly corrections * improve explanation of codec middleware Co-authored-by: Sumant Awasthi Co-authored-by: shrutiverma97 * Update scalafmt-core to 3.5.7 (#1273) * Performance: Improve Scheme.decode performance (#1107) * scheme: added unsafeDecode * removed extra code * refactor * added decode back * added scala doc * unsafe inside decode * removed unwanted changes * workflow update * unsafe method private * added private zhttp in method * Refactor: Re-write `Path` using `Vector` (#1260) * --wip-- [skip ci] * implementation for apply * trailing slash fix * refactor: split path file * test: restructure tests * test: add failing test for Path * refactor: re-write path using Vector * refactor: remove LeadingSlash * fix: encoding error in Http * feat: implement `toString` using `Encode` * fix: update cookie spec Co-authored-by: Tushar Mathur * Don't depend on scala-collection-compat when using Scala >= 2.13 (#1276) * re-implemented mapM for zio 2.x Queue * fixed examples * fixed examples * fixed SocketSpec * more fixes on the tests * test: update zio 2 format * fixed tests * formatting * fixed ci * Fix for Scala 3 compilation issue (#1285) * Fix type in ZIO.scoped call for deployWS * Apply formatting 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: Javier Goday Co-authored-by: Tushar Mathur Co-authored-by: Brendan McKee Co-authored-by: kaushik143 Co-authored-by: zsfVishnu-d11 <66246684+zsfVishnu-d11@users.noreply.github.com> Co-authored-by: Scala Steward <43047562+scala-steward@users.noreply.github.com> Co-authored-by: sumawa Co-authored-by: Sumant Awasthi Co-authored-by: James Beem Co-authored-by: Dino Babu John <66246799+dinojohn@users.noreply.github.com> Co-authored-by: Dino John Co-authored-by: AshPrakasan Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: RAJKUMAR NATARAJAN Co-authored-by: Roberto Leibman Co-authored-by: Roberto Leibman Co-authored-by: Shrey Mehta <36622672+smehta91@users.noreply.github.com> Co-authored-by: ex0ns Co-authored-by: Guillaume Massé Co-authored-by: Nikolay Artamonov Co-authored-by: Nikolay Artamonov Co-authored-by: Gabriel Ciuloaica Co-authored-by: Jules Ivanic Co-authored-by: Dani Rey Co-authored-by: shrutiverma97 Co-authored-by: Olive Iosello <67493878+oliveiosello@users.noreply.github.com> Co-authored-by: zsfVishnu <34836841+zsfVishnu@users.noreply.github.com> Co-authored-by: James Ward Co-authored-by: Quentin Co-authored-by: Dennis4b Co-authored-by: Dennis Noordsij Co-authored-by: juan pablo romero Co-authored-by: Andreas Gies --- .github/workflows/ci.yml | 638 ++++++++++++- .scalafmt.conf | 2 +- build.sbt | 38 +- docs/website/docs/v1.x/dsl/cookies.md | 2 +- docs/website/docs/v1.x/dsl/headers.md | 8 +- docs/website/docs/v1.x/dsl/http.md | 19 +- docs/website/docs/v1.x/dsl/middleware.md | 515 ++++++++++- docs/website/docs/v1.x/dsl/server.md | 10 +- .../examples/advanced-examples/stream-file.md | 6 +- .../advanced-examples/stream-response.md | 8 +- .../advanced-examples/web-socket-advanced.md | 6 +- .../zio-http-basic-examples/http_client.md | 6 +- .../zio-http-basic-examples/https_server.md | 4 +- docs/website/docs/v1.x/getting-started.md | 16 +- docs/website/docs/v1.x/index.md | 2 +- docs/website/yarn.lock | 6 +- .../src/main/scala/example/ClientServer.scala | 22 + .../main/scala/example/ConcreteEntity.scala | 2 +- .../src/main/scala/example/Endpoints.scala | 24 - .../example/HelloWorldWithMiddlewares.scala | 7 +- .../scala/example/WebSocketSimpleClient.scala | 5 +- project/BuildHelper.scala | 57 +- project/Dependencies.scala | 17 +- project/JmhBenchmarkWorkflow.scala | 203 ++++ project/ScalaSettings.scala | 63 +- project/plugins.sbt | 4 +- .../CookieDecodeBenchmark.scala | 2 +- .../SchemeDecodeBenchmark.scala | 18 + .../macros/LoggerMacroExtensions.scala | 21 + .../logging/macros/LoggerMacroImpl.scala | 62 ++ .../macros/LoggerMacroExtensions.scala | 24 + .../logging/macros/LoggerMacroImpl.scala | 75 ++ .../main/scala/zhttp/logging/LogFormat.scala | 188 ++++ .../main/scala/zhttp/logging/LogLevel.scala | 72 ++ .../main/scala/zhttp/logging/LogLine.scala | 15 + .../src/main/scala/zhttp/logging/Logger.scala | 118 +++ .../scala/zhttp/logging/LoggerTransport.scala | 117 +++ .../scala/zhttp/logging/LogLevelSpec.scala | 24 + .../test/scala/zhttp/logging/LoggerSpec.scala | 42 + .../scala/zhttp/endpoint/CanCombine.scala | 35 - .../scala/zhttp/endpoint/CanConstruct.scala | 52 -- .../scala/zhttp/endpoint/CanExtract.scala | 18 - .../main/scala/zhttp/endpoint/Endpoint.scala | 52 -- .../main/scala/zhttp/endpoint/Parameter.scala | 19 - .../scala/zhttp/endpoint/ParameterList.scala | 43 - .../scala/zhttp/endpoint/TupleBuilder.scala | 37 - .../main/scala/zhttp/endpoint/package.scala | 20 - zio-http/src/main/scala/zhttp/html/Html.scala | 2 + .../src/main/scala/zhttp/http/Cookie.scala | 54 +- zio-http/src/main/scala/zhttp/http/Http.scala | 53 +- .../src/main/scala/zhttp/http/HttpData.scala | 53 +- .../src/main/scala/zhttp/http/IsMono.scala | 37 + .../main/scala/zhttp/http/Middleware.scala | 102 ++- zio-http/src/main/scala/zhttp/http/Path.scala | 56 ++ .../main/scala/zhttp/http/PathModule.scala | 101 -- .../main/scala/zhttp/http/PathSyntax.scala | 28 + .../src/main/scala/zhttp/http/Request.scala | 103 +-- .../src/main/scala/zhttp/http/Response.scala | 16 +- .../src/main/scala/zhttp/http/Scheme.scala | 23 +- zio-http/src/main/scala/zhttp/http/URL.scala | 14 +- .../zhttp/http/headers/HeaderGetters.scala | 5 +- .../scala/zhttp/http/middleware/Csrf.scala | 2 +- .../scala/zhttp/http/middleware/Web.scala | 16 +- .../scala/zhttp/http/middleware/package.scala | 3 +- .../src/main/scala/zhttp/http/package.scala | 32 +- .../src/main/scala/zhttp/service/Client.scala | 8 +- .../main/scala/zhttp/service/Handler.scala | 133 +-- .../main/scala/zhttp/service/Logging.scala | 27 + .../zhttp/service/RequestBodyHandler.scala | 8 +- .../src/main/scala/zhttp/service/Server.scala | 255 +++--- .../zhttp/service/ServerResponseWriter.scala | 161 ++++ .../zhttp/service/WebSocketAppHandler.scala | 20 +- .../service/client/ClientInboundHandler.scala | 4 +- .../service/logging/NettyLoggerFactory.scala | 49 + .../main/scala/zhttp/service/package.scala | 20 +- .../service/server/LogLevelTransform.scala | 15 + .../server/ServerChannelInitializer.scala | 18 +- .../handlers/ServerResponseHandler.scala | 127 --- .../src/main/scala/zhttp/socket/Socket.scala | 23 +- .../main/scala/zhttp/socket/SocketApp.scala | 2 +- .../scala/zhttp/socket/WebSocketFrame.scala | 2 +- .../scala/zhttp/endpoint/EndpointSpec.scala | 84 -- .../src/test/scala/zhttp/html/DomSpec.scala | 4 +- .../src/test/scala/zhttp/html/HtmlSpec.scala | 8 +- .../test/scala/zhttp/http/CookieSpec.scala | 12 +- .../src/test/scala/zhttp/http/HttpSpec.scala | 867 +++++++++--------- .../scala/zhttp/http/MiddlewareSpec.scala | 58 +- .../src/test/scala/zhttp/http/PathSpec.scala | 419 ++++++--- .../test/scala/zhttp/http/RequestSpec.scala | 13 +- .../src/test/scala/zhttp/http/URLSpec.scala | 10 +- .../scala/zhttp/http/middleware/WebSpec.scala | 2 +- .../test/scala/zhttp/internal/HttpGen.scala | 20 +- .../zhttp/internal/HttpRunnableSpec.scala | 11 +- .../test/scala/zhttp/service/ClientSpec.scala | 6 +- .../service/RequestStreamingServerSpec.scala | 49 + .../test/scala/zhttp/service/SSLSpec.scala | 10 +- .../test/scala/zhttp/service/ServerSpec.scala | 43 +- .../zhttp/service/WebSocketServerSpec.scala | 37 +- .../test/scala/zhttp/socket/SocketSpec.scala | 20 +- 99 files changed, 4188 insertions(+), 1801 deletions(-) create mode 100644 example/src/main/scala/example/ClientServer.scala create mode 100644 project/JmhBenchmarkWorkflow.scala create mode 100644 zio-http-benchmarks/src/main/scala/zhttp.benchmarks/SchemeDecodeBenchmark.scala create mode 100644 zio-http-logging/src/main/scala-2/zhttp/logging/macros/LoggerMacroExtensions.scala create mode 100644 zio-http-logging/src/main/scala-2/zhttp/logging/macros/LoggerMacroImpl.scala create mode 100644 zio-http-logging/src/main/scala-3/zhttp/logging/macros/LoggerMacroExtensions.scala create mode 100644 zio-http-logging/src/main/scala-3/zhttp/logging/macros/LoggerMacroImpl.scala create mode 100644 zio-http-logging/src/main/scala/zhttp/logging/LogFormat.scala create mode 100644 zio-http-logging/src/main/scala/zhttp/logging/LogLevel.scala create mode 100644 zio-http-logging/src/main/scala/zhttp/logging/LogLine.scala create mode 100644 zio-http-logging/src/main/scala/zhttp/logging/Logger.scala create mode 100644 zio-http-logging/src/main/scala/zhttp/logging/LoggerTransport.scala create mode 100644 zio-http-logging/src/test/scala/zhttp/logging/LogLevelSpec.scala create mode 100644 zio-http-logging/src/test/scala/zhttp/logging/LoggerSpec.scala delete mode 100644 zio-http/src/main/scala/zhttp/endpoint/CanCombine.scala delete mode 100644 zio-http/src/main/scala/zhttp/endpoint/CanExtract.scala delete mode 100644 zio-http/src/main/scala/zhttp/endpoint/Endpoint.scala delete mode 100644 zio-http/src/main/scala/zhttp/endpoint/Parameter.scala delete mode 100644 zio-http/src/main/scala/zhttp/endpoint/ParameterList.scala delete mode 100644 zio-http/src/main/scala/zhttp/endpoint/TupleBuilder.scala delete mode 100644 zio-http/src/main/scala/zhttp/endpoint/package.scala create mode 100644 zio-http/src/main/scala/zhttp/http/IsMono.scala create mode 100644 zio-http/src/main/scala/zhttp/http/Path.scala delete mode 100644 zio-http/src/main/scala/zhttp/http/PathModule.scala create mode 100644 zio-http/src/main/scala/zhttp/http/PathSyntax.scala create mode 100644 zio-http/src/main/scala/zhttp/service/Logging.scala create mode 100644 zio-http/src/main/scala/zhttp/service/ServerResponseWriter.scala create mode 100644 zio-http/src/main/scala/zhttp/service/logging/NettyLoggerFactory.scala create mode 100644 zio-http/src/main/scala/zhttp/service/server/LogLevelTransform.scala create mode 100644 zio-http/src/test/scala/zhttp/service/RequestStreamingServerSpec.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c4d2c9f2..cda5efdcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ name: Continuous Integration on: pull_request: branches: ['**'] - types: [opened, synchronize, reopened, edited] + types: [opened, synchronize, reopened, edited, labeled] push: branches: ['**'] tags: [v*] @@ -24,7 +24,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.15, 2.13.8, 3.1.1] + scala: [2.12.15, 2.13.8, 3.1.2] java: [graal_21.1.0@11, temurin@8] runs-on: ${{ matrix.os }} steps: @@ -74,7 +74,7 @@ jobs: 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 + run: tar cf targets.tar target zio-http-test/target zio-http/target zio-http-benchmarks/target zio-http-logging/target example/target project/target - name: Upload target directories uses: actions/upload-artifact@v2 @@ -144,12 +144,12 @@ jobs: tar xf targets.tar rm targets.tar - - name: Download target directories (3.1.1) + - name: Download target directories (3.1.2) uses: actions/download-artifact@v2 with: - name: target-${{ matrix.os }}-3.1.1-${{ matrix.java }} + name: target-${{ matrix.os }}-3.1.2-${{ matrix.java }} - - name: Inflate target directories (3.1.1) + - name: Inflate target directories (3.1.2) run: | tar xf targets.tar rm targets.tar @@ -277,3 +277,629 @@ jobs: ${{steps.result.outputs.concurrency_result}} ${{steps.result.outputs.request_result}} + + Jmh_CookieDecodeBenchmark: + name: Jmh CookieDecodeBenchmark + if: ${{ github.event.label.name == 'run jmh' && github.event_name == 'pull_request' }} + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.8] + java: [temurin@11] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + path: zio-http + + - uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 8 + + - name: Benchmark_Current + id: Benchmark_Current + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Current_CookieDecodeBenchmark.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 CookieDecodeBenchmark" | grep "thrpt" >> ../Current_CookieDecodeBenchmark.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Current_CookieDecodeBenchmark + path: Current_CookieDecodeBenchmark.txt + + - uses: actions/checkout@v2 + with: + path: zio-http + ref: main + + - name: Benchmark_Main + id: Benchmark_Main + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Main_CookieDecodeBenchmark.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 CookieDecodeBenchmark" | grep "thrpt" >> ../Main_CookieDecodeBenchmark.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Main_CookieDecodeBenchmark + path: Main_CookieDecodeBenchmark.txt + + Jmh_HttpCollectEval: + name: Jmh HttpCollectEval + if: ${{ github.event.label.name == 'run jmh' && github.event_name == 'pull_request' }} + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.8] + java: [temurin@11] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + path: zio-http + + - uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 8 + + - name: Benchmark_Current + id: Benchmark_Current + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Current_HttpCollectEval.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 HttpCollectEval" | grep "thrpt" >> ../Current_HttpCollectEval.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Current_HttpCollectEval + path: Current_HttpCollectEval.txt + + - uses: actions/checkout@v2 + with: + path: zio-http + ref: main + + - name: Benchmark_Main + id: Benchmark_Main + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Main_HttpCollectEval.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 HttpCollectEval" | grep "thrpt" >> ../Main_HttpCollectEval.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Main_HttpCollectEval + path: Main_HttpCollectEval.txt + + Jmh_HttpCombineEval: + name: Jmh HttpCombineEval + if: ${{ github.event.label.name == 'run jmh' && github.event_name == 'pull_request' }} + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.8] + java: [temurin@11] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + path: zio-http + + - uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 8 + + - name: Benchmark_Current + id: Benchmark_Current + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Current_HttpCombineEval.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 HttpCombineEval" | grep "thrpt" >> ../Current_HttpCombineEval.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Current_HttpCombineEval + path: Current_HttpCombineEval.txt + + - uses: actions/checkout@v2 + with: + path: zio-http + ref: main + + - name: Benchmark_Main + id: Benchmark_Main + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Main_HttpCombineEval.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 HttpCombineEval" | grep "thrpt" >> ../Main_HttpCombineEval.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Main_HttpCombineEval + path: Main_HttpCombineEval.txt + + Jmh_HttpNestedFlatMapEval: + name: Jmh HttpNestedFlatMapEval + if: ${{ github.event.label.name == 'run jmh' && github.event_name == 'pull_request' }} + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.8] + java: [temurin@11] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + path: zio-http + + - uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 8 + + - name: Benchmark_Current + id: Benchmark_Current + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Current_HttpNestedFlatMapEval.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 HttpNestedFlatMapEval" | grep "thrpt" >> ../Current_HttpNestedFlatMapEval.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Current_HttpNestedFlatMapEval + path: Current_HttpNestedFlatMapEval.txt + + - uses: actions/checkout@v2 + with: + path: zio-http + ref: main + + - name: Benchmark_Main + id: Benchmark_Main + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Main_HttpNestedFlatMapEval.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 HttpNestedFlatMapEval" | grep "thrpt" >> ../Main_HttpNestedFlatMapEval.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Main_HttpNestedFlatMapEval + path: Main_HttpNestedFlatMapEval.txt + + Jmh_HttpRouteTextPerf: + name: Jmh HttpRouteTextPerf + if: ${{ github.event.label.name == 'run jmh' && github.event_name == 'pull_request' }} + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.8] + java: [temurin@11] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + path: zio-http + + - uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 8 + + - name: Benchmark_Current + id: Benchmark_Current + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Current_HttpRouteTextPerf.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 HttpRouteTextPerf" | grep "thrpt" >> ../Current_HttpRouteTextPerf.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Current_HttpRouteTextPerf + path: Current_HttpRouteTextPerf.txt + + - uses: actions/checkout@v2 + with: + path: zio-http + ref: main + + - name: Benchmark_Main + id: Benchmark_Main + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Main_HttpRouteTextPerf.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 HttpRouteTextPerf" | grep "thrpt" >> ../Main_HttpRouteTextPerf.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Main_HttpRouteTextPerf + path: Main_HttpRouteTextPerf.txt + + Jmh_ProbeContentTypeBenchmark: + name: Jmh ProbeContentTypeBenchmark + if: ${{ github.event.label.name == 'run jmh' && github.event_name == 'pull_request' }} + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.8] + java: [temurin@11] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + path: zio-http + + - uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 8 + + - name: Benchmark_Current + id: Benchmark_Current + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Current_ProbeContentTypeBenchmark.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 ProbeContentTypeBenchmark" | grep "thrpt" >> ../Current_ProbeContentTypeBenchmark.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Current_ProbeContentTypeBenchmark + path: Current_ProbeContentTypeBenchmark.txt + + - uses: actions/checkout@v2 + with: + path: zio-http + ref: main + + - name: Benchmark_Main + id: Benchmark_Main + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Main_ProbeContentTypeBenchmark.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 ProbeContentTypeBenchmark" | grep "thrpt" >> ../Main_ProbeContentTypeBenchmark.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Main_ProbeContentTypeBenchmark + path: Main_ProbeContentTypeBenchmark.txt + + Jmh_SchemeDecodeBenchmark: + name: Jmh SchemeDecodeBenchmark + if: ${{ github.event.label.name == 'run jmh' && github.event_name == 'pull_request' }} + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.8] + java: [temurin@11] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + path: zio-http + + - uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 8 + + - name: Benchmark_Current + id: Benchmark_Current + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Current_SchemeDecodeBenchmark.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 SchemeDecodeBenchmark" | grep "thrpt" >> ../Current_SchemeDecodeBenchmark.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Current_SchemeDecodeBenchmark + path: Current_SchemeDecodeBenchmark.txt + + - uses: actions/checkout@v2 + with: + path: zio-http + ref: main + + - name: Benchmark_Main + id: Benchmark_Main + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")' project/plugins.sbt + cat > Main_SchemeDecodeBenchmark.txt + sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 SchemeDecodeBenchmark" | grep "thrpt" >> ../Main_SchemeDecodeBenchmark.txt + + - uses: actions/upload-artifact@v3 + with: + name: Jmh_Main_SchemeDecodeBenchmark + path: Main_SchemeDecodeBenchmark.txt + + Jmh_publish: + name: Jmh Publish + needs: [Jmh_CookieDecodeBenchmark, Jmh_HttpCollectEval, Jmh_HttpCombineEval, Jmh_HttpNestedFlatMapEval, Jmh_HttpRouteTextPerf, Jmh_ProbeContentTypeBenchmark, Jmh_SchemeDecodeBenchmark] + if: ${{ github.event.label.name == 'run jmh' && github.event_name == 'pull_request' }} + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.8] + java: [temurin@11] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/download-artifact@v3 + with: + name: Jmh_Current_CookieDecodeBenchmark + + - name: Result Current CookieDecodeBenchmark + id: Result_Current_CookieDecodeBenchmark + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Current.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Current.txt + done < Current_CookieDecodeBenchmark.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Current_HttpCollectEval + + - name: Result Current HttpCollectEval + id: Result_Current_HttpCollectEval + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Current.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Current.txt + done < Current_HttpCollectEval.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Current_HttpCombineEval + + - name: Result Current HttpCombineEval + id: Result_Current_HttpCombineEval + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Current.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Current.txt + done < Current_HttpCombineEval.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Current_HttpNestedFlatMapEval + + - name: Result Current HttpNestedFlatMapEval + id: Result_Current_HttpNestedFlatMapEval + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Current.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Current.txt + done < Current_HttpNestedFlatMapEval.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Current_HttpRouteTextPerf + + - name: Result Current HttpRouteTextPerf + id: Result_Current_HttpRouteTextPerf + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Current.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Current.txt + done < Current_HttpRouteTextPerf.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Current_ProbeContentTypeBenchmark + + - name: Result Current ProbeContentTypeBenchmark + id: Result_Current_ProbeContentTypeBenchmark + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Current.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Current.txt + done < Current_ProbeContentTypeBenchmark.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Current_SchemeDecodeBenchmark + + - name: Result Current SchemeDecodeBenchmark + id: Result_Current_SchemeDecodeBenchmark + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Current.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Current.txt + done < Current_SchemeDecodeBenchmark.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Main_CookieDecodeBenchmark + + - name: Result Main CookieDecodeBenchmark + id: Result_Main_CookieDecodeBenchmark + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Main.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Main.txt + done < Main_CookieDecodeBenchmark.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Main_HttpCollectEval + + - name: Result Main HttpCollectEval + id: Result_Main_HttpCollectEval + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Main.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Main.txt + done < Main_HttpCollectEval.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Main_HttpCombineEval + + - name: Result Main HttpCombineEval + id: Result_Main_HttpCombineEval + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Main.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Main.txt + done < Main_HttpCombineEval.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Main_HttpNestedFlatMapEval + + - name: Result Main HttpNestedFlatMapEval + id: Result_Main_HttpNestedFlatMapEval + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Main.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Main.txt + done < Main_HttpNestedFlatMapEval.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Main_HttpRouteTextPerf + + - name: Result Main HttpRouteTextPerf + id: Result_Main_HttpRouteTextPerf + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Main.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Main.txt + done < Main_HttpRouteTextPerf.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Main_ProbeContentTypeBenchmark + + - name: Result Main ProbeContentTypeBenchmark + id: Result_Main_ProbeContentTypeBenchmark + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Main.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Main.txt + done < Main_ProbeContentTypeBenchmark.txt + + - uses: actions/download-artifact@v3 + with: + name: Jmh_Main_SchemeDecodeBenchmark + + - name: Result Main SchemeDecodeBenchmark + id: Result_Main_SchemeDecodeBenchmark + run: | + while IFS= read -r line; do + IFS=' ' read -ra PARSED_RESULT <<< "$line" + echo ${PARSED_RESULT[1]} >> parsed_Main.txt + B_VALUE=$(echo ${PARSED_RESULT[1]}": "${PARSED_RESULT[4]}" ops/sec") + echo $B_VALUE >> Main.txt + done < Main_SchemeDecodeBenchmark.txt + + - name: Format Output + id: fomat_output + run: | + cat parsed_Current.txt parsed_Main.txt | sort -u > c.txt + while IFS= read -r line; do + if grep -q "$line" Current.txt + then + grep "$line" Current.txt | sed 's/^.*: //' >> finalCurrent.txt; + else + echo "" >> finalCurrent.txt; + fi + if grep -q "$line" Main.txt + then + grep "$line" Main.txt | sed 's/^.*: //' >> finalMain.txt; + else + echo "" >> finalMain.txt; + fi + done < c.txt + paste -d '|' c.txt finalCurrent.txt finalMain.txt > FinalOutput.txt + sed -i -e 's/^/|/' FinalOutput.txt + sed -i -e 's/$/|/' FinalOutput.txt + body=$(cat FinalOutput.txt) + body="${body//'%'/'%25'}" + body="${body//$'\n'/'%0A'}" + body="${body//$'\r'/'%0D'}" + echo $body + echo ::set-output name=body::$(echo $body) + + + - uses: peter-evans/commit-comment@v1 + with: + sha: ${{github.sha}} + body: | + + **🚀 Jmh Benchmark:** + + |Name |Current| Main| + |-----|----| ----| + ${{steps.fomat_output.outputs.body}} + diff --git a/.scalafmt.conf b/.scalafmt.conf index b5eec06bf..9d367ef9c 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.4.3 +version = 3.5.7 maxColumn = 120 align.preset = more diff --git a/build.sbt b/build.sbt index 88dbc1d07..a91e316a7 100644 --- a/build.sbt +++ b/build.sbt @@ -1,17 +1,22 @@ import BuildHelper._ import Dependencies._ +import sbt.librarymanagement.ScalaArtifacts.isScala3 val releaseDrafterVersion = "5" +// Setting default log level to INFO +val _ = sys.props += ("ZHttpLogLevel" -> "INFO") + // CI Configuration -ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec.graalvm("21.1.0", "11"), JavaSpec.temurin("8")) -ThisBuild / githubWorkflowPREventTypes := Seq( +ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec.graalvm("21.1.0", "11"), JavaSpec.temurin("8")) +ThisBuild / githubWorkflowPREventTypes := Seq( PREventType.Opened, PREventType.Synchronize, PREventType.Reopened, PREventType.Edited, + PREventType.Labeled, ) -ThisBuild / githubWorkflowAddedJobs := +ThisBuild / githubWorkflowAddedJobs := Seq( WorkflowJob( id = "update_release_draft", @@ -37,7 +42,7 @@ ThisBuild / githubWorkflowAddedJobs := ), cond = Option("${{ github.ref == 'refs/heads/main' }}"), ), - ) ++ ScoverageWorkFlow(50, 60) ++ BenchmarkWorkFlow() + ) ++ ScoverageWorkFlow(50, 60) ++ BenchmarkWorkFlow() ++ JmhBenchmarkWorkflow(1) ThisBuild / githubWorkflowTargetTags ++= Seq("v*") ThisBuild / githubWorkflowPublishTargetBranches := Seq( @@ -60,7 +65,7 @@ ThisBuild / githubWorkflowPublish := ) //scala fix isn't available for scala 3 so ensure we only run the fmt check //using the latest scala 2.13 -ThisBuild / githubWorkflowBuildPreamble := Seq( +ThisBuild / githubWorkflowBuildPreamble := Seq( WorkflowStep.Run( name = Some("Check formatting"), commands = List(s"sbt ++${Scala213} fmtCheck"), @@ -68,7 +73,7 @@ ThisBuild / githubWorkflowBuildPreamble := Seq( ), ) -ThisBuild / githubWorkflowBuildPostamble := +ThisBuild / githubWorkflowBuildPostamble := WorkflowJob( "checkDocGeneration", "Check doc generation", @@ -89,6 +94,7 @@ lazy val root = (project in file(".")) zhttp, zhttpBenchmarks, zhttpTest, + zhttpLogging, example, ) @@ -98,8 +104,7 @@ lazy val zhttp = (project in file("zio-http")) .settings(meta) .settings( testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"), - libraryDependencies ++= Seq( - netty, + libraryDependencies ++= netty ++ Seq( `zio`, `zio-streams`, `zio-test`, @@ -113,6 +118,7 @@ lazy val zhttp = (project in file("zio-http")) } }, ) + .dependsOn(zhttpLogging) lazy val zhttpBenchmarks = (project in file("zio-http-benchmarks")) .enablePlugins(JmhPlugin) @@ -126,9 +132,23 @@ lazy val zhttpTest = (project in file("zio-http-test")) .settings(stdSettings("zhttp-test")) .settings(publishSetting(true)) +lazy val zhttpLogging = (project in file("zio-http-logging")) + .settings(stdSettings("zhttp-logging")) + .settings(publishSetting(false)) + .settings( + libraryDependencies ++= { + if (isScala3(scalaVersion.value)) Seq.empty + else Seq(reflect.value % Provided) + }, + ) + .settings( + testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"), + libraryDependencies ++= Seq(`zio-test`, `zio-test-sbt`), + ) + lazy val example = (project in file("./example")) .settings(stdSettings("example")) .settings(publishSetting(false)) - .settings(runSettings("example.Main")) + .settings(runSettings("example.HelloWorld")) .settings(libraryDependencies ++= Seq(`jwt-core`)) .dependsOn(zhttp) diff --git a/docs/website/docs/v1.x/dsl/cookies.md b/docs/website/docs/v1.x/dsl/cookies.md index 2399f023d..d0d9ead8b 100644 --- a/docs/website/docs/v1.x/dsl/cookies.md +++ b/docs/website/docs/v1.x/dsl/cookies.md @@ -82,7 +82,7 @@ To sign all the cookies in your `HttpApp`, you can use `signCookies` middleware: } // Run it like any simple app - override def run(args: List[String]): UIO[ExitCode] = + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = Server.start(8090, app @@ signCookies("secret")).exitCode ``` diff --git a/docs/website/docs/v1.x/dsl/headers.md b/docs/website/docs/v1.x/dsl/headers.md index 2cd797830..3a912d0ae 100644 --- a/docs/website/docs/v1.x/dsl/headers.md +++ b/docs/website/docs/v1.x/dsl/headers.md @@ -51,7 +51,7 @@ On the Server-side you can read Request headers as given below import zio.stream.ZStream object SimpleResponseDispatcher extends App { - override def run(args: List[String]): UIO[ExitCode] = { + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { // Starting the server (for more advanced startup configuration checkout `HelloWorldAdvanced`) Server.start(8090, app.silent).exitCode @@ -145,17 +145,17 @@ val responseHeaders: Task[Headers] = Client.request(url).map(_.headers) // Pass headers to request res <- Client.request(url, headers) // List all response headers - _ <- Console.printLine(res.headers.toList.mkString("\n")) + _ <- console.putStrLn(res.headers.toList.mkString("\n")) data <- // Check if response contains a specified header with a specified value. if (res.hasHeader(HeaderNames.contentType, HeaderValues.applicationJson)) res.bodyAsString else res.bodyAsString - _ <- Console.printLine { data } + _ <- console.putStrLn { data } } yield () - override def run(args: List[String]): UIO[ExitCode] = program.exitCode.provideLayer(env) + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = program.exitCode.provideCustomLayer(env) } ``` diff --git a/docs/website/docs/v1.x/dsl/http.md b/docs/website/docs/v1.x/dsl/http.md index c179e7b8f..13b1efffc 100644 --- a/docs/website/docs/v1.x/dsl/http.md +++ b/docs/website/docs/v1.x/dsl/http.md @@ -195,6 +195,17 @@ application. There are many operators to provide the HTTP application with its required environment. +### provideCustomLayer + +Provides the HTTP application with the part of the environment that is not part of the ZEnv, leaving an effect that only depends on the ZEnv. + +```scala + val a: Http[Clock, DateTimeException, String, OffsetDateTime] = Http.collectZIO[String] { + case "case 1" => clock.currentDateTime + } + val app: Http[zio.ZEnv, DateTimeException, String, OffsetDateTime] = a.provideCustomLayer(Clock.live) +``` + ## Attaching Middleware Middlewares are essentially transformations that one can apply to any `Http` to produce a new one. To attach middleware to the HTTP application, you can use `middleware` operator. `@@` is an alias for `middleware`. @@ -215,12 +226,12 @@ The below snippet tests an app that takes `Int` as input and responds by adding import zio.test.Assertion.equalTo import zio.test._ - object Spec extends ZIOSpecDefault { + object Spec extends DefaultRunnableSpec { def spec = suite("http")( - testM("1 + 1 = 2") { + test("1 + 1 = 2") { val app: Http[Any, Nothing, Int, Int] = Http.fromFunction[Int](_ + 1) - assertZIO(app(1))(equalTo(2)) + assert(app(1))(equalTo(2)) } ) } @@ -354,6 +365,6 @@ We can use `Server.app()` method to bootstrap the server with an `HttpApp[R,E]` object HelloWorld extends App { val app: HttpApp[Any, Nothing] = Http.ok - override def run(args: List[String]): UIO[ExitCode] = Server.start(8090, app).exitCode + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = Server.start(8090, app).exitCode } ``` \ No newline at end of file diff --git a/docs/website/docs/v1.x/dsl/middleware.md b/docs/website/docs/v1.x/dsl/middleware.md index e55192be9..8185cacfb 100644 --- a/docs/website/docs/v1.x/dsl/middleware.md +++ b/docs/website/docs/v1.x/dsl/middleware.md @@ -3,4 +3,517 @@ sidebar_position: "8" --- # Middleware -WIP \ No newline at end of file +## What is a "Middleware"? +Before introducing middleware, let us understand why they are needed. + +Consider the following example where we have two endpoints within HttpApp +* GET a single user by id +* GET all users +```scala + private val app = Http.collectZIO[Request] { + case Method.GET -> !! / "users" / id => + // core business logic + dbService.lookupUsersById(id).map(Response.json(_.json)) + case Method.GET -> !! / "users" => + // core business logic + dbService.paginatedUsers(pageNum).map(Response.json(_.json)) + } +``` +#### The polluted code violates the principle of "Separation of concerns" + +As our application grows, we want to code the following aspects like +* Basic Auth +* Request logging +* Response logging +* Timeout and retry + +For both of our example endpoints, our core business logic gets buried under boilerplate like this + +```scala + (for { + // validate user + _ <- MyAuthService.doAuth(request) + // log request + _ <- logRequest(request) + // core business logic + user <- dbService.lookupUsersById(id).map(Response.json(_.json)) + resp <- Response.json(user.toJson) + // log response + _ <- logResponse(resp) + } yield resp) + .timeout(2.seconds) + .retryN(5) +``` +Imagine repeating this for all our endpoints!!! + +So there are two problems with this approach +* We are dangerously coupling our business logic with cross-cutting concerns (like applying timeouts) +* Also, addressing these concerns will require updating code for every single route in the system. For 100 routes we will need to repeat 100 timeouts!!! +* For example, any change related to a concern like the logging mechanism from logback to log4j2 may cause changing signature of `log(..)` function in 100 places. +* On the other hand, this also makes testing core business logic more cumbersome. + + +This can lead to a lot of boilerplate clogging our neatly written endpoints affecting readability, thereby leading to increased maintenance costs. + +## Need for middlewares and handling "aspects" + +If we refer to Wikipedia for the definition of an "[Aspect](https://en.wikipedia.org/wiki/Aspect_(computer_programming))" we can glean the following points. + +* An aspect of a program is a feature linked to many other parts of the program (**_most common example, logging_**)., +* But it is not related to the program's primary function (**_core business logic_**) +* An aspect crosscuts the program's core concerns (**_for example logging code intertwined with core business logic_**), +* Therefore, it can violate the principle of "separation of concerns" which tries to encapsulate unrelated functions. (**_Code duplication and maintenance nightmare_**) + +Or in short, aspect is a common concern required throughout the application, and its implementation could lead to repeated boilerplate code and in violation of the principle of separation of concerns. +There is a paradigm in the programming world called [aspect-oriented programming](https://en.wikipedia.org/wiki/Aspect-oriented_programming) that aims for modular handling of these common concerns in an application. + +Some examples of common "aspects" required throughout the application +- logging, +- timeouts (preventing long-running code) +- retries (or handling flakiness for example while accessing third party APIs) +- authenticating a user before using the REST resource (basic, or custom ones like OAuth / single sign-on, etc). + +This is where middleware comes to the rescue. +Using middlewares we can compose out-of-the-box middlewares (or our custom middlewares) to address the above-mentioned concerns using ++ and @@ operators as shown below. + +#### Cleaned up code using middleware to address cross-cutting concerns like auth, request/response logging, etc. +Observe, how we can address multiple cross-cutting concerns using neatly composed middlewares, in a single place. + +```scala +// compose basic auth, request/response logging, timeouts middlewares +val composedMiddlewares = Middleware.basicAuth("user","pw") ++ + Middleware.debug ++ + Middleware.timeout(5 seconds) +```` +And then we can attach our composed bundle of middlewares to an Http using `@@` +```scala +private val app = Http.collectZIO[Request] { + case Method.GET -> !! / "users" / id => + // core business logic + dbService.lookupUsersById(id).map(Response.json(_.json)) + case Method.GET -> !! / "users" => + // core business logic + dbService.paginatedUsers(pageNum).map(Response.json(_.json)) +} @@ composedMiddlewares // attach composedMiddlewares to the app using @@ +``` +Observe how we gained the following benefits by using middlewares +* **Readability**: de-cluttering business logic. +* **Modularity**: we can manage aspects independently without making changes in 100 places. For example, + * replacing the logging mechanism from logback to log4j2 will require a change in one place, the logging middleware. + * replacing the authentication mechanism from OAuth to single sign-on will require changing the auth middleware +* **Testability**: we can test our aspects independently. + +## Middleware in zio-http + +A middleware helps in addressing common crosscutting concerns without duplicating boilerplate code. + +#### Revisiting HTTP +[`Http`](https://dream11.github.io/zio-http/docs/v1.x/dsl/http) is the most fundamental type for modeling Http applications + +```Http[-R, +E, -A, +B]``` is equivalent to ```(A) => ZIO[R, Option[E], B]``` where + +* `R` type of Environment +* `E` type of the Error when the function fails with Some[E] +* `A` is the type of input given to the Http +* `B` type of the output produced by the Http + +Middleware is simply a function that takes one `Http` as a parameter and returns a new `Http` with some enhanced capabilities., + +```Http => Http``` + +So, a middleware represents function transformation f1 => f2 + +```scala +type Middleware[R, E, AIn, BIn, AOut, BOut] = Http[R, E, AIn, BIn] => Http[R, E, AOut, BOut] +``` +* `AIn` and `BIn` are type params of the input `Http` +* `AOut` and `BOut` are type params of the output `Http` + +**HttpApp** is a specialized Http with `Request` and `Response` as input and output +```scala +type HttpApp[-R,+E] = Http[R, E, Request, Response] +``` +In the ```HttpApp``` context, a middleware can modify requests and responses and also transform them into more concrete domain entities. + +#### Attaching middleware to Http +`@@` operator is used to attach a middleware to an Http. Example below shows a middleware attached to an HttpApp +```scala +val app = Http.collect[Request] { + case Method.GET -> !! / name => Response.text(s"Hello $name") +} +val appWithMiddleware = app @@ Middleware.debug +``` +Logically the code above translates to `Middleware.debug(app)` +#### A simple middleware example +Let us consider a simple example using out-of-the-box middleware called ```addHeader``` +We will write a middleware that will attach a custom header to the response. + +Start with imports +```scala +import zhttp.http._ +import zhttp.service.Server +import zio.console.{putStrLn} +import zio.{App, ExitCode, URIO} +``` +We create a middleware that appends an additional header to the response indicating whether it is a Dev/Prod/Staging environment. +```scala +lazy val patchEnv = Middleware.addHeader("X-Environment", "Dev") +``` +A test HttpApp with attached middleware +```scala +val app = Http.collect[Request] { + case Method.GET -> !! / name => Response.text(s"Hello $name") +} +val appWithMiddleware = app @@ patchEnv +``` +Start the server +```scala +val server = Server.start(8090, appWithMiddleware).exitCode +zio.Runtime.default.unsafeRunSync(server) +``` +Fire a curl request and we see an additional header added to the response indicating the "Dev" environment +``` +curl -i http://localhost:8090/Bob + +HTTP/1.1 200 OK +content-type: text/plain +X-Environment: Dev +content-length: 12 + +Hello Bob +``` +### Advanced example showing the transformative power of a middleware (Optional) + +
+ +Here is a slightly longer example to explore how powerful middleware transformation can be. It shows +
    +
  • How we can define a service purely in terms of our domain types
  • +
  • Define a "codec" middleware for decoding requests and encoding responses to and from domain types
  • +
  • Transform our regular `Http` service to an HttpApp by applying our codec middleware
  • +
+Note: In real life, we will be using REST endpoints for defining routes. Although you can skip this section, this example gives a deeper insight into middleware. +
+ +Start with imports +```scala +import zhttp.http.{Http, HttpError, Middleware, Request, Response} +import zio.{ExitCode, URIO, ZEnv, ZIO} +import zio.json._ +import zhttp.service._ +``` +Define some "User" domain types +```scala +final case class User(name: String, email: String, id: String) +object User { + implicit val codec: JsonCodec[User] = DeriveJsonCodec.gen[User] +} +sealed trait UsersRequest +object UsersRequest { + final case class Get(id: Int) extends UsersRequest + final case class Create(name: String, email: String) extends UsersRequest + implicit val decoder: JsonDecoder[UsersRequest] = DeriveJsonDecoder.gen[UsersRequest] +} +sealed trait UsersResponse +object UsersResponse { + final case class Got(user: User) extends UsersResponse + final case class Created(user: User) extends UsersResponse + + implicit val encoder: JsonEncoder[UsersResponse] = DeriveJsonEncoder.gen[UsersResponse] +} +``` +Define a Users service _**purely in terms of our types**_ +```scala + val usersService: Http[Any, Nothing, UsersRequest, UsersResponse] = Http.collect { + case UsersRequest.Create(name, email) => { + UsersResponse.Created(User(name, email, "abc123")) + } + case UsersRequest.Get(id) => { + UsersResponse.Got(User(id.toString, "", "")) + } + } +``` +A codec middleware which transforms an `Http` with different input/output types to `HttpApp` with **Request and Response** +```scala + def codecMiddleware[In: JsonDecoder, Out: JsonEncoder]: Middleware[Any,Nothing,In,Out,Request,Response] = + Middleware.codecZIO[Request,Out]( + // deserialize the request into In type or fail with some message + request => + for{ + body <- request.getBodyAsString + in <- ZIO.fromEither(JsonDecoder[In].decodeJson(body)) + } yield in, + out => { + for { + charSeq <- ZIO(JsonEncoder[Out].encodeJson(out, None)) + } yield Response.json(charSeq.toString) + } + ) <> Middleware.succeed(Response.fromHttpError(HttpError.BadRequest("Invalid JSON"))) +``` + +Let us transform our regular Http with UserService to an HttpApp using our codec middleware + +```scala +val transformedHttpApp: Http[Any, Nothing, Request, Response] = + usersService @@ codecMiddleware[UsersRequest,UsersResponse] +``` +Just observe how our service `Http` got transformed to `HttpApp` by applying middleware + +`Http[Any, Nothing, UsersRequest, UsersResponse]` ===> `Http[Any, Nothing, Request, Response]` (HttpApp) + +Start the server +```scala +val server = Server.start(8090, transformedHttpApp) +zio.Runtime.default.unsafeRunSync(server) +``` +Fire a curl request along with the request body +``` +curl -i -d '{ "Create":{ "name":"Sum", "email": "s@d1.com" }}' http://localhost:8090/ +``` +Observe the response +``` +HTTP/1.1 200 OK +content-type: application/json +content-length: 68 + +{"Created":{"user":{"name":"Sum","email":"s@d1.com","id":"abc123"}} +``` +
+ +## Creating Middleware + +Refer to [Middleware.scala](https://github.com/dream11/zio-http/blob/main/zio-http/src/main/scala/zhttp/http/Middleware.scala) for various ways of creating a middleware. + +Again remember that a "middleware" is just a **_transformative function_**. There are ways of creating such transformative functions: +* **identity**: works like an [identity function](https://en.wikipedia.org/wiki/Identity_function) in mathematics + `f(x) = x`. + It returns the same `Http` as input without doing any modification +```scala +val identityMW: Middleware[Any, Nothing, Nothing, Any, Any, Nothing] = Middleware.identity +app @@ identityMW // no effect on the http app. +``` +* **succeed** creates a middleware that always returns the output `Http` that succeeds with the given value and never fails. + +```scala +val middleware: Middleware[Any, Nothing, Nothing, Any, Any, Int] = Middleware.succeed(1) +``` +* **fail** creates a middleware that always returns the output `Http` that always fails. + +```scala +val middleware: Middleware[Any, String, Nothing, Any, Any, Nothing] = Middleware.fail("error") +``` +* **collect** creates middleware using a specified function + +```scala +val middleware: Middleware[Any, Nothing, Request, Response, Request, Response] = Middleware.collect[Request](_ => Middleware.addHeaders(Headers("a", "b"))) +``` +* **collectZIO** creates middleware using a specified effect function + +```scala +val middleware: Middleware[Any, Nothing, Request, Response, Request, Response] = Middleware.collectZIO[Request](_ => ZIO.succeed(Middleware.addHeaders(Headers("a", "b")))) +``` +* **codec** takes two functions `decoder: AOut => Either[E, AIn]` and `encoder: BIn => Either[E, BOut]` + +The below snippet takes two functions: + - decoder function to decode Request to String + - encoder function to encode String to Response + +```scala +val middleware: Middleware[Any, Nothing, String, String, Request, Response] = Middleware.codec[Request,String](r => Right(r.method.toString()), s => Right(Response.text(s))) +``` +* **fromHttp** creates a middleware with output `Http` as specified `http` + +```scala +val app: Http[Any, Nothing, Any, String] = Http.succeed("Hello World!") +val middleware: Middleware[Any, Nothing, Nothing, Any, Request, Response] = Middleware.fromHttp(app) +``` + +## Combining middlewares + +Middlewares can be combined using several special operators like `++`, `<>` and `>>>` + +### Using `++` combinator + +`++` is an alias for `combine`. It combines two middlewares **_without changing their input/output types (`AIn` = `AOut` / `BIn` = `BOut`)_** + +For example, if we have three middlewares f1, f2, f3 + +f1 ++ f2 ++ f3 applies on an `http`, from left to right with f1 first followed by others, like this +```scala + f3(f2(f1(http))) +``` +#### A simple example using `++` combinator +Start with imports +```scala +import zhttp.http.Middleware.basicAuth +import zhttp.http._ +import zhttp.service.Server +import zio.console.putStrLn +import zio.{App, ExitCode, URIO} +``` +A user app with single endpoint that welcomes a user +```scala +val userApp: UHttpApp = Http.collect[Request] { case Method.GET -> !! / "user" / name / "greet" => + Response.text(s"Welcome to the ZIO party! ${name}") +} +``` +A basicAuth middleware with hardcoded user pw and another patches response with environment value +```scala +val basicAuthMW = basicAuth("admin", "admin") +lazy val patchEnv = Middleware.addHeader("X-Environment", "Dev") +// apply combined middlewares to the userApp +val appWithMiddleware = userApp @@ (basicAuthMW ++ patchEnv) +``` +Start the server +```scala +val server = Server.start(8090, appWithMiddleware).exitCode +zio.Runtime.default.unsafeRunSync(server) +``` +Fire a curl request with an incorrect user/password combination +``` +curl -i --user admin:wrong http://localhost:8090/user/admin/greet + +HTTP/1.1 401 Unauthorized +www-authenticate: Basic +X-Environment: Dev +content-length: 0 +``` +We notice in the response that first basicAuth middleware responded `HTTP/1.1 401 Unauthorized` and then patch middleware attached a `X-Environment: Dev` header. + +### Using `>>>` +`>>>` is an alias for `andThen` and similar to `++` with one BIG difference **_input/output types can be different (`AIn`≠ `AOut` / `BIn`≠ `BOut`)_** +Whereas, in the case of `++` types remain the same (horizontal composition). + +For example, if we have three middlewares f1, f2, f3 + +f1 >>> f2 >>> f3 applies on an `http`, sequentially feeding an http to f1 first followed by f2 and f3. + +f1(http) => http1 +f2(http1) => http2 + +### Using `<>` combinator +`<>` is an alias for `orElse`. While using `<>`, if the output `Http` of the first middleware fails, the second middleware will be evaluated, ignoring the result from the first. +#### A simple example using `<>` +```scala +val middleware: Middleware[Any, Nothing, Request, Response, Request, Response] = Middleware.fail("error") <> Middleware.addHeader("X-Environment", "Dev") +``` +#### other operators +* **contraMap**,**contraMapZIO**,**delay**,**flatMap**,**flatten**,**map**: which are obvious as their name implies. + +* **race** to race middlewares +* **runAfter** and **runBefore** to run effect before and after +* **when** to conditionally run a middleware (input of output Http meets some criteria) + +## Transforming Middlewares (some advanced examples) + +### Transforming the output of the output `Http` + +- We can use `flatMap` or `map` or `mapZIO` for transforming the output type of output Http + +```scala +val middleware: Middleware[Any, Nothing, Nothing, Any, Any, Int] = Middleware.succeed(3) + +val mid1: Middleware[Any, Nothing, Nothing, Any, Any, String] = middleware.map((i: Int) => i.toString) +val mid2: Middleware[Any, Nothing, Nothing, Any, Any, String] = middleware.mapZIO((i: Int) => ZIO.succeed(s"$i")) +val mid3: Middleware[Any, Nothing, Nothing, Any, Any, String] = middleware.flatMap((m: Int) => Middleware.succeed(m.toString)) +``` + +- We can use `intercept` or `interceptZIO` to create a new middleware using transformation functions, which changes the output type of the output `Http` keeping input the same. + + The below snippet takes two functions: + - (incoming: A => S) + - (outgoing: (B, S) => BOut) + +```scala +val middleware: Middleware[Any, Nothing, String, String, String, Int] = Middleware.intercept[String, String](_.toInt + 2)((_, a) => a + 3) + +val mid: Middleware[Any, Nothing, Int, Int, Int, Int] = Middleware.interceptZIO[Int, Int](i => UIO(i * 10))((i, j) => UIO(i + j)) +``` + +### Transforming the Input of the output `Http` + +We can use `contramap` or `contramapZIO` for transforming the input type of the output `Http` + +```scala +val middleware: Middleware[Any, Nothing, Int, Int, Int, Int] = Middleware.codec[Int, Int](decoder = a => Right(a + 1), encoder = b => Right(b + 1)) + +val mid1: Middleware[Any, Nothing, Int, Int, String, Int] = middleware.contramap[String](_.toInt) +val mid2: Middleware[Any, Nothing, Int, Int, String, Int] = middleware.contramapZIO[String](a => UIO(a.toInt)) +``` + +## Conditional application of middlewares + +- `when` applies middleware only if the condition function evaluates to true + +```scala +val middleware: Middleware[Any, Nothing, Nothing, Any, Any, String] = Middleware.succeed("yes") +val mid: Middleware[Any, Nothing, Nothing, Any, String, String] = middleware.when[String]((str: String) => str.length > 2) +``` + +-`whenZIO` applies middleware only if the condition function(with effect) evaluates + +```scala +val middleware: Middleware[Any, Nothing, Nothing, Any, Any, String] = Middleware.succeed("yes") +val mid: Middleware[Any, Nothing, Nothing, Any, String, String] = middleware.whenZIO[Any, Nothing, String]((str: String) => UIO(str.length > 2)) +``` + +Logical operators to decide which middleware to select based on the predicate: + +- Using `ifThenElse` + +```scala +val mid: Middleware[Any, Nothing, Nothing, Any, Int, Int] = Middleware.ifThenElse[Int](_ > 5)( + isTrue = i => Middleware.succeed(i + 1), + isFalse = i => Middleware.succeed(i - 1) + ) +``` +- Using `ifThenElseZIO` + +```scala +val mid: Middleware[Any, Nothing, Nothing, Any, Int, Int] = Middleware.ifThenElseZIO[Int](i => UIO(i > 5))( + isTrue = i => Middleware.succeed(i + 1), + isFalse = i => Middleware.succeed(i - 1), + ) +``` + +## A complete example of a middleware + +
+Detailed example showing "debug" and "addHeader" middlewares + +```scala + import zhttp.http._ + import zhttp.http.middleware.HttpMiddleware + import zhttp.service.Server + import zio.clock.{Clock, currentTime} + import zio.console.Console + import zio.duration._ + import zio.{App, ExitCode, URIO, ZIO} + + import java.io.IOException + import java.util.concurrent.TimeUnit + + val app: HttpApp[Clock, Nothing] = Http.collectZIO[Request] { + // this will return result instantly + case Method.GET -> !! / "text" => ZIO.succeed(Response.text("Hello World!")) + // this will return result after 5 seconds, so with 3 seconds timeout it will fail + case Method.GET -> !! / "long-running" => ZIO.succeed(Response.text("Hello World!")).delay(5 seconds) + } + + val middlewares: HttpMiddleware[Console with Clock, IOException] = + // print debug info about request and response + Middleware.debug ++ + // add static header + Middleware.addHeader("X-Environment", "Dev") ++ + + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + Server.start(8090, (app @@ middlewares)).exitCode +``` + +
+ +### A few "Out of the box" middlewares +- [Basic Auth](https://dream11.github.io/zio-http/docs/v1.x/examples/advanced-examples/middleware_basic_auth) +- [CORS](https://dream11.github.io/zio-http/docs/v1.x/examples/advanced-examples/middleware_cors) +- [CSRF](https://dream11.github.io/zio-http/docs/v1.x/examples/advanced-examples/middleware_csrf) + diff --git a/docs/website/docs/v1.x/dsl/server.md b/docs/website/docs/v1.x/dsl/server.md index d642e0de7..33310d099 100644 --- a/docs/website/docs/v1.x/dsl/server.md +++ b/docs/website/docs/v1.x/dsl/server.md @@ -7,7 +7,7 @@ This section describes, ZIO HTTP Server and different configurations you can pro ## Start a ZIO HTTP Server with default configurations ```scala - override def run(args: List[String]): UIO[ExitCode] = + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = Server.start(8090, app.silent).exitCode ``` ## Start a ZIO HTTP Server with custom configurations. @@ -28,12 +28,12 @@ This section describes, ZIO HTTP Server and different configurations you can pro ``` 3. And then use ```Server.make``` to get a "managed" instance use it to run a server forever ```scala - override def run(args: List[String]): UIO[ExitCode] = { + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { server.make .use(start => console.putStrLn(s"Server started on port ${start.port}") *> ZIO.never, - ).provideLayer(ServerChannelFactory.auto ++ EventLoopGroup.auto(2)) + ).provideCustomLayer(ServerChannelFactory.auto ++ EventLoopGroup.auto(2)) .exitCode ``` **Tip :** `ServerChannelFactory.auto ++ EventLoopGroup.auto(num Threads)` is supplied as an external dependency to choose netty transport type. One can leave it as `auto` to let the application handle it for you. @@ -76,7 +76,7 @@ object HelloWorldAdvanced extends App { Server.paranoidLeakDetection ++ // Paranoid leak detection (affects performance) Server.app(fooBar +++ app) // Setup the Http app - override def run(args: List[String]): UIO[ExitCode] = { + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { // Configure thread count using CLI val nThreads: Int = args.headOption.flatMap(x => Try(x.toInt).toOption).getOrElse(0) @@ -89,7 +89,7 @@ object HelloWorldAdvanced extends App { // Ensures the server doesn't die after printing *> ZIO.never, ) - .provideLayer(ServerChannelFactory.auto ++ EventLoopGroup.auto(nThreads)) + .provideCustomLayer(ServerChannelFactory.auto ++ EventLoopGroup.auto(nThreads)) .exitCode } } diff --git a/docs/website/docs/v1.x/examples/advanced-examples/stream-file.md b/docs/website/docs/v1.x/examples/advanced-examples/stream-file.md index 03cefe503..feb935df3 100644 --- a/docs/website/docs/v1.x/examples/advanced-examples/stream-file.md +++ b/docs/website/docs/v1.x/examples/advanced-examples/stream-file.md @@ -8,7 +8,7 @@ import zio._ import java.io.File import java.nio.file.Paths -object FileStreaming extends ZIOAppDefault { +object FileStreaming extends App { // Create HTTP route val app = Http.collectHttp[Request] { @@ -26,8 +26,8 @@ object FileStreaming extends ZIOAppDefault { } // Run it like any simple app - override val run = - Server.start(8090, app.silent) + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + Server.start(8090, app.silent).exitCode } ``` \ No newline at end of file diff --git a/docs/website/docs/v1.x/examples/advanced-examples/stream-response.md b/docs/website/docs/v1.x/examples/advanced-examples/stream-response.md index 2e4514217..fd9a61919 100644 --- a/docs/website/docs/v1.x/examples/advanced-examples/stream-response.md +++ b/docs/website/docs/v1.x/examples/advanced-examples/stream-response.md @@ -9,10 +9,12 @@ import zio._ /** * Example to encode content using a ZStream */ -object StreamingResponse extends ZIOAppDefault { - override val run = +object StreamingResponse extends App { + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { + // Starting the server (for more advanced startup configuration checkout `HelloWorldAdvanced`) - Server.start(8090, app.silent) + Server.start(8090, app.silent).exitCode + } // Create a message as a Chunk[Byte] val message = Chunk.fromArray("Hello world !\r\n".getBytes(HTTP_CHARSET)) diff --git a/docs/website/docs/v1.x/examples/advanced-examples/web-socket-advanced.md b/docs/website/docs/v1.x/examples/advanced-examples/web-socket-advanced.md index 60eb6bb4c..f1f790722 100644 --- a/docs/website/docs/v1.x/examples/advanced-examples/web-socket-advanced.md +++ b/docs/website/docs/v1.x/examples/advanced-examples/web-socket-advanced.md @@ -36,10 +36,10 @@ object WebSocketAdvanced extends App { .onOpen(open) // Called after the connection is closed - .onClose(_ => Console.printLine("Closed!").ignore) + .onClose(_ => console.putStrLn("Closed!").ignore) // Called whenever there is an error on the socket channel - .onError(_ => Console.printLine("Error!").ignore) + .onError(_ => console.putStrLn("Error!").ignore) // Setup websocket decoder config .withDecoder(decoder) @@ -54,7 +54,7 @@ object WebSocketAdvanced extends App { case Method.GET -> !! / "subscriptions" => socketApp.toResponse } - override def run(args: List[String]): UIO[ExitCode] = + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = Server.start(8090, app).exitCode } diff --git a/docs/website/docs/v1.x/examples/zio-http-basic-examples/http_client.md b/docs/website/docs/v1.x/examples/zio-http-basic-examples/http_client.md index 4b67b38d1..c91d76c75 100644 --- a/docs/website/docs/v1.x/examples/zio-http-basic-examples/http_client.md +++ b/docs/website/docs/v1.x/examples/zio-http-basic-examples/http_client.md @@ -12,11 +12,11 @@ object SimpleClient extends App { val program = for { res <- Client.request(url, headers) data <- res.bodyAsString - _ <- Console.printLine { data } + _ <- console.putStrLn { data } } yield () - override def run(args: List[String]): UIO[ExitCode] = - program.exitCode.provideLayer(env) + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + program.exitCode.provideCustomLayer(env) } ``` \ No newline at end of file diff --git a/docs/website/docs/v1.x/examples/zio-http-basic-examples/https_server.md b/docs/website/docs/v1.x/examples/zio-http-basic-examples/https_server.md index 7e80477df..e645fe586 100644 --- a/docs/website/docs/v1.x/examples/zio-http-basic-examples/https_server.md +++ b/docs/website/docs/v1.x/examples/zio-http-basic-examples/https_server.md @@ -29,9 +29,9 @@ object HttpsHelloWorld extends App { ServerSSLOptions(sslctx, SSLHttpBehaviour.Accept), ) - override def run(args: List[String]): UIO[ExitCode] = { + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { server.make.useForever - .provideLayer(ServerChannelFactory.auto ++ EventLoopGroup.auto(0)) + .provideCustomLayer(ServerChannelFactory.auto ++ EventLoopGroup.auto(0)) .exitCode } } diff --git a/docs/website/docs/v1.x/getting-started.md b/docs/website/docs/v1.x/getting-started.md index 1cdc26e4d..1d53ffaea 100644 --- a/docs/website/docs/v1.x/getting-started.md +++ b/docs/website/docs/v1.x/getting-started.md @@ -105,13 +105,13 @@ Since `Http` is a function of the form `A => ZIO[R, Option[E], B]` to test it yo import zio.test._ import zhttp.http._ -object Spec extends ZIOSpecDefault { +object Spec extends DefaultRunnableSpec { def spec = suite("http")( test("should be ok") { val app = Http.ok val req = Request() - assertZIO(app(req))(equalTo(Response.ok)) + assert(app(req))(equalTo(Response.ok)) } ) } @@ -156,20 +156,20 @@ import zhttp.http._ import zhttp.service.Server import zio._ -object HelloWorld extends ZIOAppDefault { +object HelloWorld extends App { val app = Http.ok - override val run = - Server.start(8090, app) + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + Server.start(8090, app).exitCode } ``` ## Examples -- [Simple Server](https://dream11.github.io/zio-http/docs/v1.x/examples/zio-http-basic-examples/hello-world) -- [Advanced Server](https://dream11.github.io/zio-http/docs/v1.x/examples/advanced-examples/hello-world-advanced) +- [HTTP Server](https://dream11.github.io/zio-http/docs/v1.x/examples/zio-http-basic-examples/http_server) +- [Advanced Server](https://dream11.github.io/zio-http/docs/v1.x/examples/advanced-examples/advanced_server) - [WebSocket Server](https://dream11.github.io/zio-http/docs/v1.x/examples/zio-http-basic-examples/web-socket) - [Streaming Response](https://dream11.github.io/zio-http/docs/v1.x/examples/advanced-examples/stream-response) -- [Simple Client](https://dream11.github.io/zio-http/docs/v1.x/examples/zio-http-basic-examples/simple-client) +- [HTTP Client](https://dream11.github.io/zio-http/docs/v1.x/examples/zio-http-basic-examples/http_client) - [File Streaming](https://dream11.github.io/zio-http/docs/v1.x/examples/advanced-examples/stream-file) - [Authentication](https://dream11.github.io/zio-http/docs/v1.x/examples/advanced-examples/authentication) diff --git a/docs/website/docs/v1.x/index.md b/docs/website/docs/v1.x/index.md index 6937c5745..8a19ef4d3 100644 --- a/docs/website/docs/v1.x/index.md +++ b/docs/website/docs/v1.x/index.md @@ -17,7 +17,7 @@ Before we dive in, make sure that you have the following on your computer: To use zio-http, add the following dependencies in your project: ```scala -val ZHTTPVersion = "1.0.0.0-RC24" +val ZHTTPVersion = "1.0.0.0-RC27" libraryDependencies ++= Seq( "io.d11" %% "zhttp" % ZHTTPVersion, diff --git a/docs/website/yarn.lock b/docs/website/yarn.lock index b190a4a8b..266c2a2b0 100644 --- a/docs/website/yarn.lock +++ b/docs/website/yarn.lock @@ -2274,9 +2274,9 @@ async-limiter@~1.0.0: integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== async@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== dependencies: lodash "^4.17.14" diff --git a/example/src/main/scala/example/ClientServer.scala b/example/src/main/scala/example/ClientServer.scala new file mode 100644 index 000000000..94230090b --- /dev/null +++ b/example/src/main/scala/example/ClientServer.scala @@ -0,0 +1,22 @@ +package example + +import zhttp.http._ +import zhttp.service.{ChannelFactory, Client, EventLoopGroup, Server} +import zio.{ZIO, ZIOAppDefault} + +object ClientServer extends ZIOAppDefault { + + val app = Http.collectZIO[Request] { + case Method.GET -> !! / "hello" => + ZIO.succeed(Response.text("hello")) + + case Method.GET -> !! => + val url = "http://localhost:8080/hello" + Client.request(url) + } + + val run = { + val clientLayers = ChannelFactory.auto ++ EventLoopGroup.auto() + Server.start(8080, app).provideLayer(clientLayers).exitCode + } +} diff --git a/example/src/main/scala/example/ConcreteEntity.scala b/example/src/main/scala/example/ConcreteEntity.scala index fa2f04e86..0b576b2dc 100644 --- a/example/src/main/scala/example/ConcreteEntity.scala +++ b/example/src/main/scala/example/ConcreteEntity.scala @@ -21,7 +21,7 @@ object ConcreteEntity extends ZIOAppDefault { val app: HttpApp[Any, Nothing] = user - .contramap[Request](req => CreateUser(req.path.toString)) // Http[Any, Nothing, Request, UserCreated] + .contramap[Request](req => CreateUser(req.path.encode)) // Http[Any, Nothing, Request, UserCreated] .map(userCreated => Response.text(userCreated.id.toString)) // Http[Any, Nothing, Request, Response] // Run it like any simple app diff --git a/example/src/main/scala/example/Endpoints.scala b/example/src/main/scala/example/Endpoints.scala index 1f2a02e82..8b1378917 100644 --- a/example/src/main/scala/example/Endpoints.scala +++ b/example/src/main/scala/example/Endpoints.scala @@ -1,25 +1 @@ -package example -import zhttp.endpoint._ -import zhttp.http.Method.GET -import zhttp.http.Response -import zhttp.service.Server -import zio._ - -object Endpoints extends ZIOAppDefault { - def h1 = GET / "a" / *[Int] / "b" / *[Boolean] to { a => - Response.text(a.params.toString) - } - - def h2 = GET / "b" / *[Int] / "b" / *[Boolean] to { a => - Response.text(a.params.toString) - } - - def h3 = GET / "b" / *[Int] / "c" / *[Boolean] to { a => - ZIO.succeed(Response.text(a.params.toString)) - } - - // Run it like any simple app - val run = - Server.start(8091, (h3 ++ h2 ++ h1)).exitCode -} diff --git a/example/src/main/scala/example/HelloWorldWithMiddlewares.scala b/example/src/main/scala/example/HelloWorldWithMiddlewares.scala index 95fc4f5fc..a1aa5f845 100644 --- a/example/src/main/scala/example/HelloWorldWithMiddlewares.scala +++ b/example/src/main/scala/example/HelloWorldWithMiddlewares.scala @@ -17,14 +17,14 @@ object HelloWorldWithMiddlewares extends ZIOAppDefault { case Method.GET -> !! / "long-running" => ZIO.succeed(Response.text("Hello World!")).delay(5 seconds) } - val serverTime: HttpMiddleware[Any, Nothing] = Middleware.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: HttpMiddleware[Any, IOException] = + val middlewares: HttpMiddleware[Console with Clock, IOException] = // print debug info about request and response Middleware.debug ++ // close connection if request takes more than 3 seconds @@ -35,6 +35,5 @@ object HelloWorldWithMiddlewares extends ZIOAppDefault { serverTime // Run it like any simple app - override val run = - Server.start(8090, (app @@ middlewares)) + val run = Server.start(8090, app @@ middlewares).provide(Console.live, Clock.live) } diff --git a/example/src/main/scala/example/WebSocketSimpleClient.scala b/example/src/main/scala/example/WebSocketSimpleClient.scala index 66fe4179e..c54ae71b3 100644 --- a/example/src/main/scala/example/WebSocketSimpleClient.scala +++ b/example/src/main/scala/example/WebSocketSimpleClient.scala @@ -1,5 +1,6 @@ package example +import zhttp.http.Response import zhttp.service.{ChannelFactory, EventLoopGroup} import zhttp.socket.{Socket, WebSocketFrame} import zio._ @@ -8,11 +9,11 @@ import zio.stream.ZStream object WebSocketSimpleClient extends ZIOAppDefault { // Setup client envs - val env = EventLoopGroup.auto() ++ ChannelFactory.auto + val env = EventLoopGroup.auto() ++ ChannelFactory.auto ++ Scope.default val url = "ws://localhost:8090/subscriptions" - val app = Socket + val app: ZIO[EventLoopGroup with ChannelFactory with Scope, Throwable, Response] = Socket .collect[WebSocketFrame] { case WebSocketFrame.Text("BAZ") => ZStream.succeed(WebSocketFrame.close(1000)) case frame => ZStream.succeed(frame) diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 35550de1b..101d8a676 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -4,11 +4,11 @@ import scalafix.sbt.ScalafixPlugin.autoImport._ import xerial.sbt.Sonatype.autoImport._ object BuildHelper extends ScalaSettings { - val Scala212 = "2.12.15" - val Scala213 = "2.13.8" - val ScalaDotty = "3.1.1" - val ScoverageVersion = "1.9.3" - + val Scala212 = "2.12.15" + val Scala213 = "2.13.8" + val ScalaDotty = "3.1.2" + val ScoverageVersion = "1.9.3" + val JmhVersion = "0.4.3" private val stdOptions = Seq( "-deprecation", "-encoding", @@ -24,40 +24,11 @@ object BuildHelper extends ScalaSettings { } } - private val std2xOptions = Seq( - "-language:higherKinds", - "-language:existentials", - "-explaintypes", - "-Yrangepos", - "-Xlint:_,-missing-interpolator,-type-parameter-shadow", - "-Ywarn-numeric-widen", - "-Ywarn-macros:after", - ) - - private def optimizerOptions(optimize: Boolean) = - if (optimize) - Seq( - "-opt:l:inline", - ) - else Nil - - def extraOptions(scalaVersion: String, optimize: Boolean) = + def extraOptions(scalaVersion: String) = CrossVersion.partialVersion(scalaVersion) match { - case Some((3, 0)) => - Seq( - "-language:implicitConversions", - "-Xignore-scala2-macros", - "-noindent", - ) - case Some((2, 12)) => - Seq("-Ywarn-unused:params,-implicits") ++ std2xOptions - case Some((2, 13)) => - Seq( - "-Ywarn-unused:params,-implicits", - "-Ywarn-macros:after", - "-Ywarn-value-discard", - ) ++ std2xOptions ++ tpoleCatSettings ++ - optimizerOptions(optimize) + case Some((3, 0)) => scala3Settings + case Some((2, 12)) => scala212Settings + case Some((2, 13)) => scala213Settings case _ => Seq.empty } @@ -78,10 +49,10 @@ object BuildHelper extends ScalaSettings { } def stdSettings(prjName: String) = Seq( - name := s"$prjName", - ThisBuild / crossScalaVersions := Seq(Scala212, Scala213, ScalaDotty), - ThisBuild / scalaVersion := Scala213, - scalacOptions := stdOptions ++ extraOptions(scalaVersion.value, optimize = !isSnapshot.value), + name := s"$prjName", + ThisBuild / crossScalaVersions := Seq(Scala212, Scala213, ScalaDotty), + ThisBuild / scalaVersion := Scala213, + scalacOptions := stdOptions ++ extraOptions(scalaVersion.value), semanticdbVersion := scalafixSemanticdb.revision, // use Scalafix compatible version ThisBuild / scalafixScalaBinaryVersion := CrossVersion.binaryScalaVersion(scalaVersion.value), ThisBuild / scalafixDependencies ++= @@ -92,7 +63,7 @@ object BuildHelper extends ScalaSettings { Test / parallelExecution := true, incOptions ~= (_.withLogRecompileOnMacro(false)), autoAPIMappings := true, - ThisBuild / javaOptions := Seq("-Dio.netty.leakDetectionLevel=paranoid"), + ThisBuild / javaOptions := Seq("-Dio.netty.leakDetectionLevel=paranoid", "-DZHttpLogLevel=INFO"), ThisBuild / fork := true, ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 11100697a..15694c73a 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -1,9 +1,10 @@ +import sbt.Keys.scalaVersion import sbt._ object Dependencies { val JwtCoreVersion = "9.0.5" - val NettyVersion = "4.1.75.Final" - val NettyIncubatorVersion = "0.0.13.Final" + val NettyVersion = "4.1.77.Final" + val NettyIncubatorVersion = "0.0.14.Final" val ScalaCompactCollectionVersion = "2.7.0" val ZioVersion = "2.0.0-RC6" val SttpVersion = "3.3.18" @@ -11,7 +12,15 @@ object Dependencies { val `jwt-core` = "com.github.jwt-scala" %% "jwt-core" % JwtCoreVersion val `scala-compact-collection` = "org.scala-lang.modules" %% "scala-collection-compat" % ScalaCompactCollectionVersion - val netty = "io.netty" % "netty-all" % NettyVersion + val netty = + Seq( + "netty-codec-http", + "netty-transport-native-epoll", + "netty-transport-native-kqueue", + ).map { name => + "io.netty" % name % NettyVersion + } + val `netty-incubator` = "io.netty.incubator" % "netty-incubator-transport-native-io_uring" % NettyIncubatorVersion classifier "linux-x86_64" @@ -19,4 +28,6 @@ object Dependencies { val `zio-streams` = "dev.zio" %% "zio-streams" % ZioVersion val `zio-test` = "dev.zio" %% "zio-test" % ZioVersion % "test" val `zio-test-sbt` = "dev.zio" %% "zio-test-sbt" % ZioVersion % "test" + + val reflect = Def.map(scalaVersion)("org.scala-lang" % "scala-reflect" % _) } diff --git a/project/JmhBenchmarkWorkflow.scala b/project/JmhBenchmarkWorkflow.scala new file mode 100644 index 000000000..776fc1667 --- /dev/null +++ b/project/JmhBenchmarkWorkflow.scala @@ -0,0 +1,203 @@ +import BuildHelper.{JmhVersion, Scala213} +import sbt.nio.file.FileTreeView +import sbt.{**, Glob, PathFilter} +import sbtghactions.GenerativePlugin.autoImport.{UseRef, WorkflowJob, WorkflowStep} + +object JmhBenchmarkWorkflow { + + val jmhPlugin = s"""addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "$JmhVersion")""" + val scalaSources: PathFilter = ** / "*.scala" + val files = FileTreeView.default.list(Glob("./zio-http-benchmarks/src/main/scala/zhttp.benchmarks/**"), scalaSources) + + /** + * Get zhttpBenchmark file names + */ + def getFilenames = files + .map(file => { + val path = file._1.toString + path.replaceAll("^.*[\\/\\\\]", "").replaceAll(".scala", "") + }) + .sorted + + /** + * Run jmh benchmarks and store result + */ + def runSBT(list: Seq[String], branch: String) = list.map(str => + s"""sbt -no-colors -v "zhttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 $str" | grep "thrpt" >> ../${branch}_${list.head}.txt""".stripMargin, + ) + + /** + * Group benchmarks into batches + */ + def groupedBenchmarks(batchSize: Int) = getFilenames.grouped(batchSize).toList + + /** + * Get dependent jobs for publishing the result + */ + def dependencies(batchSize: Int) = groupedBenchmarks(batchSize).flatMap((l: Seq[String]) => List(s"Jmh_${l.head}")) + + /** + * Download Artifacts and parse result + */ + def downloadArtifacts(branch: String, batchSize: Int) = groupedBenchmarks(batchSize).flatMap(l => { + Seq( + WorkflowStep.Use( + ref = UseRef.Public("actions", "download-artifact", "v3"), + Map( + "name" -> s"Jmh_${branch}_${l.head}", + ), + ), + WorkflowStep.Run( + commands = List(s"""while IFS= read -r line; do + | IFS=' ' read -ra PARSED_RESULT <<< "$$line" + | echo $${PARSED_RESULT[1]} >> parsed_$branch.txt + | B_VALUE=$$(echo $${PARSED_RESULT[1]}": "$${PARSED_RESULT[4]}" ops/sec") + | echo $$B_VALUE >> $branch.txt + | done < ${branch}_${l.head}.txt""".stripMargin), + id = Some(s"Result_${branch}_${l.head}"), + name = Some(s"Result $branch ${l.head}"), + ), + ) + }) + + /** + * Format result and set output + */ + def formatOutput() = WorkflowStep.Run( + commands = List( + s"""cat parsed_Current.txt parsed_Main.txt | sort -u > c.txt + | while IFS= read -r line; do + | if grep -q "$$line" Current.txt + | then + | grep "$$line" Current.txt | sed 's/^.*: //' >> finalCurrent.txt; + | else + | echo "" >> finalCurrent.txt; + | fi + | if grep -q "$$line" Main.txt + | then + | grep "$$line" Main.txt | sed 's/^.*: //' >> finalMain.txt; + | else + | echo "" >> finalMain.txt; + | fi + | done < c.txt + |paste -d '|' c.txt finalCurrent.txt finalMain.txt > FinalOutput.txt + | sed -i -e 's/^/|/' FinalOutput.txt + | sed -i -e 's/$$/|/' FinalOutput.txt + | body=$$(cat FinalOutput.txt) + | body="$${body//'%'/'%25'}" + | body="$${body//$$'\\n'/'%0A'}" + | body="$${body//$$'\\r'/'%0D'}" + | echo $$body + | echo ::set-output name=body::$$(echo $$body) + | """.stripMargin, + ), + id = Some(s"fomat_output"), + name = Some(s"Format Output"), + ) + + /** + * Workflow Job to publish benchmark results in the comment + */ + def publish(batchSize: Int) = Seq( + WorkflowJob( + id = "Jmh_publish", + name = "Jmh Publish", + scalas = List(Scala213), + cond = Some( + "${{ github.event.label.name == 'run jmh' && github.event_name == 'pull_request' }}", + ), + needs = dependencies(batchSize), + steps = downloadArtifacts("Current", batchSize) ++ downloadArtifacts("Main", batchSize) ++ + Seq( + formatOutput(), + WorkflowStep.Use( + ref = UseRef.Public("peter-evans", "commit-comment", "v1"), + params = Map( + "sha" -> "${{github.sha}}", + "body" -> + """ + |**\uD83D\uDE80 Jmh Benchmark:** + | + ||Name |Current| Main| + ||-----|----| ----| + | ${{steps.fomat_output.outputs.body}} + | """.stripMargin, + ), + ), + ), + ), + ) + + /** + * Workflow Job to run jmh benchmarks in batches parallelly + */ + def run(batchSize: Int) = groupedBenchmarks(batchSize).map(l => { + WorkflowJob( + id = s"Jmh_${l.head}", + name = s"Jmh ${l.head}", + scalas = List(Scala213), + cond = Some( + "${{ github.event.label.name == 'run jmh' && github.event_name == 'pull_request' }}", + ), + steps = List( + WorkflowStep.Use( + UseRef.Public("actions", "checkout", "v2"), + Map( + "path" -> "zio-http", + ), + ), + WorkflowStep.Use( + UseRef.Public("actions", "setup-java", "v2"), + Map( + "distribution" -> "temurin", + "java-version" -> "8", + ), + ), + WorkflowStep.Run( + env = Map("GITHUB_TOKEN" -> "${{secrets.ACTIONS_PAT}}"), + commands = List( + "cd zio-http", + s"sed -i -e '$$a${jmhPlugin}' project/plugins.sbt", + s"cat > Current_${l.head}.txt", + ) ++ runSBT(l, "Current"), + id = Some("Benchmark_Current"), + name = Some("Benchmark_Current"), + ), + WorkflowStep.Use( + UseRef.Public("actions", "upload-artifact", "v3"), + Map( + "name" -> s"Jmh_Current_${l.head}", + "path" -> s"Current_${l.head}.txt", + ), + ), + WorkflowStep.Use( + UseRef.Public("actions", "checkout", "v2"), + Map( + "path" -> "zio-http", + "ref" -> "main", + ), + ), + WorkflowStep.Run( + env = Map("GITHUB_TOKEN" -> "${{secrets.ACTIONS_PAT}}"), + commands = List( + "cd zio-http", + s"sed -i -e '$$a${jmhPlugin}' project/plugins.sbt", + s"cat > Main_${l.head}.txt", + ) ++ runSBT(l, "Main"), + id = Some("Benchmark_Main"), + name = Some("Benchmark_Main"), + ), + WorkflowStep.Use( + UseRef.Public("actions", "upload-artifact", "v3"), + Map( + "name" -> s"Jmh_Main_${l.head}", + "path" -> s"Main_${l.head}.txt", + ), + ), + ), + ) + }) + + def apply(batchSize: Int): Seq[WorkflowJob] = run(batchSize) ++ publish(batchSize) + +} diff --git a/project/ScalaSettings.scala b/project/ScalaSettings.scala index c1c00caeb..633b574ad 100644 --- a/project/ScalaSettings.scala +++ b/project/ScalaSettings.scala @@ -1,6 +1,5 @@ trait ScalaSettings { - // RECOMMENDED SETTINGS: https://tpolecat.github.io/2017/04/25/scalac-flags.html - val tpoleCatSettings = Seq( + private val baseSettings = Seq( "-language:postfixOps", // Added by @tusharmath "-deprecation", // Emit warning and location for usages of deprecated APIs. "-encoding", @@ -25,27 +24,14 @@ trait ScalaSettings { "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field. "-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component. "-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope. - "-Xlint:unused", // TODO check if we still need -Wunused below - "-Xlint:nonlocal-return", // A return statement used an exception for flow control. - "-Xlint:implicit-not-found", // Check @implicitNotFound and @implicitAmbiguous messages. - "-Xlint:serial", // @SerialVersionUID on traits and non-serializable classes. - "-Xlint:valpattern", // Enable pattern checks in val definitions. - "-Xlint:eta-zero", // Warn on eta-expansion (rather than auto-application) of zero-ary method. - "-Xlint:eta-sam", // Warn on eta-expansion to meet a Java-defined functional interface that is not explicitly annotated with @FunctionalInterface. - "-Xlint:deprecation", // Enable linted deprecations. - "-Wdead-code", // Warn when dead code is identified. - "-Wextra-implicit", // Warn when more than one implicit parameter section is defined. - "-Wmacros:both", // Lints code before and after applying a macro - "-Wnumeric-widen", // Warn when numerics are widened. - "-Woctal-literal", // Warn on obsolete octal syntax. - "-Wunused:imports", // Warn if an import selector is not referenced. - "-Wunused:patvars", // Warn if a variable bound in a pattern is unused. - "-Wunused:privates", // Warn if a private member is unused. - "-Wunused:locals", // Warn if a local definition is unused. - "-Wunused:explicits", // Warn if an explicit parameter is unused. - "-Wunused:params", // Enable -Wunused:explicits,implicits. - "-Wunused:linted", - "-Wvalue-discard", // Warn when non-Unit expression results are unused. + // "-Xlint:unused", // TODO check if we still need -Wunused below + + "-Xlint:deprecation", // Enable linted deprecations. + + // "-Wunused:explicits", // Warn if an explicit parameter is unused. + // "-Wunused:params", // Enable -Wunused:explicits,implicits. + // "-Wunused:linted", + "-Ybackend-parallelism", "8", // Enable paralellisation — change to desired number! "-Ycache-plugin-class-loader:last-modified", // Enables caching of classloaders for compiler plugins @@ -57,4 +43,35 @@ trait ScalaSettings { // "-language:experimental.macros", // Allow macro definition (besides implementation and application). Disabled, as this will significantly change in Scala 3 // "-language:implicitConversions", // Allow definition of implicit functions called views. Disabled, as it might be dropped in Scala 3. Instead use extension methods (implemented as implicit class Wrapper(val inner: Foo) extends AnyVal {} ) + + val scala3Settings: Seq[String] = Seq("-Xignore-scala2-macros", "-noindent") + + // RECOMMENDED SETTINGS: https://tpolecat.github.io/2017/04/25/scalac-flags.html + val scala213Settings: Seq[String] = baseSettings ++ Seq( + "-Xlint:nonlocal-return", // A return statement used an exception for flow control. + "-Xlint:implicit-not-found", // Check @implicitNotFound and @implicitAmbiguous messages. + "-Xlint:serial", // @SerialVersionUID on traits and non-serializable classes. + "-Xlint:valpattern", // Enable pattern checks in val definitions. + "-Xlint:eta-zero", // Warn on eta-expansion (rather than auto-application) of zero-ary method. + "-Xlint:eta-sam", // Warn on eta-expansion to meet a Java-defined functional interface that is not explicitly annotated with @FunctionalInterface. + "-Wdead-code", // Warn when dead code is identified. + "-Wextra-implicit", // Warn when more than one implicit parameter section is defined. + "-Wmacros:after", // Lints code before and after applying a macro + "-Wnumeric-widen", // Warn when numerics are widened. + "-Woctal-literal", // Warn on obsolete octal syntax. + "-Wunused:imports", // Warn if an import selector is not referenced. + "-Wunused:patvars", // Warn if a variable bound in a pattern is unused. + "-Wunused:privates", // Warn if a private member is unused. + "-Wunused:locals", // Warn if a local definition is unused. + "-Wvalue-discard", // Warn when non-Unit expression results are unused. + ) + + val scala212Settings: Seq[String] = baseSettings ++ Seq( + "-explaintypes", + "-Yrangepos", + "-Xlint:_,-missing-interpolator,-type-parameter-shadow", + "-Ywarn-numeric-widen", + "-Ywarn-macros:after", + "-Ywarn-unused:-implicits", + ) } diff --git a/project/plugins.sbt b/project/plugins.sbt index e6a85d62a..8fef05993 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.13") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.34") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.0") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.0") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.2") diff --git a/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/CookieDecodeBenchmark.scala b/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/CookieDecodeBenchmark.scala index d634f8631..b05a1b48c 100644 --- a/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/CookieDecodeBenchmark.scala +++ b/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/CookieDecodeBenchmark.scala @@ -14,7 +14,7 @@ class CookieDecodeBenchmark { val name = random.alphanumeric.take(100).mkString("") val value = random.alphanumeric.take(100).mkString("") val domain = random.alphanumeric.take(100).mkString("") - val path = Path((0 to 10).map { _ => random.alphanumeric.take(10).mkString("") }.mkString("")) + val path = Path.decode((0 to 10).map { _ => random.alphanumeric.take(10).mkString("") }.mkString("")) val maxAge = random.nextLong() private val cookie = Cookie( diff --git a/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/SchemeDecodeBenchmark.scala b/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/SchemeDecodeBenchmark.scala new file mode 100644 index 000000000..fd6baefb4 --- /dev/null +++ b/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/SchemeDecodeBenchmark.scala @@ -0,0 +1,18 @@ +package zhttp.benchmarks +import org.openjdk.jmh.annotations._ +import zhttp.http._ + +import java.util.concurrent.TimeUnit + +@State(Scope.Thread) +@BenchmarkMode(Array(Mode.Throughput)) +@OutputTimeUnit(TimeUnit.SECONDS) +class SchemeDecodeBenchmark { + private val MAX = 1000000 + + @Benchmark + def benchmarkSchemeDecode(): Unit = { + (0 to MAX).foreach(_ => Scheme.decode("HTTP")) + () + } +} diff --git a/zio-http-logging/src/main/scala-2/zhttp/logging/macros/LoggerMacroExtensions.scala b/zio-http-logging/src/main/scala-2/zhttp/logging/macros/LoggerMacroExtensions.scala new file mode 100644 index 000000000..1653ff1df --- /dev/null +++ b/zio-http-logging/src/main/scala-2/zhttp/logging/macros/LoggerMacroExtensions.scala @@ -0,0 +1,21 @@ +package zhttp.logging.macros + +import zhttp.logging.macros.LoggerMacroImpl._ + +trait LoggerMacroExtensions { self => + import scala.language.experimental.macros + + val isDebugEnabled: Boolean + val isErrorEnabled: Boolean + val isInfoEnabled: Boolean + val isTraceEnabled: Boolean + val isWarnEnabled: Boolean + + final def trace(msg: String): Unit = macro logTraceImpl + final def debug(msg: String): Unit = macro logDebugImpl + final def info(msg: String): Unit = macro logInfoImpl + final def warn(msg: String): Unit = macro logWarnImpl + final def error(msg: String): Unit = macro logErrorImpl + final def error(msg: String, throwable: Throwable): Unit = + macro logErrorWithCauseImpl +} diff --git a/zio-http-logging/src/main/scala-2/zhttp/logging/macros/LoggerMacroImpl.scala b/zio-http-logging/src/main/scala-2/zhttp/logging/macros/LoggerMacroImpl.scala new file mode 100644 index 000000000..b908deea6 --- /dev/null +++ b/zio-http-logging/src/main/scala-2/zhttp/logging/macros/LoggerMacroImpl.scala @@ -0,0 +1,62 @@ +package zhttp.logging.macros + +import zhttp.logging.{LogLevel, Logger} + +import scala.reflect.macros.whitebox + +/** + * Macro inspired from log4s. + */ +private[zhttp] object LoggerMacroImpl { + + /** A macro context that represents a method call on a Logger instance. */ + private[this] type LogCtx = whitebox.Context { type PrefixType = Logger } + + /** + * Log a message reflectively at a given level. + */ + private[this] def reflectiveLog( + c: LogCtx, + )(msg: c.Expr[String], error: Option[c.Expr[Throwable]])(logLevel: LogLevel) = { + import c.universe._ + type Tree = c.universe.Tree + + val cname: Tree = q"${c.internal.enclosingOwner.owner.fullName}" + val lno: Tree = q"${c.enclosingPosition.line}" + val sourceLocation: Tree = + if (logLevel == LogLevel.Trace) + q"Some(_root_.zhttp.logging.Logger.SourcePos($cname, $lno))" + else + q"None" + val logLevelName = logLevel.name.toLowerCase.capitalize + val level: Tree = q"_root_.zhttp.logging.LogLevel.${TermName(logLevelName)}" + val isEnabled: Tree = q"""${c.prefix.tree}.${TermName(s"is${logLevelName}Enabled")}""" + + if (logLevel >= Logger.detectedLevel) + q""" + if($isEnabled) { + val logMsg = ${msg.tree} + ${c.prefix.tree}.dispatch(logMsg, $level, $error, ${sourceLocation}) + } + """ + else q"()" + } + + def logTraceImpl(c: LogCtx)(msg: c.Expr[String]): c.universe.Tree = + reflectiveLog(c)(msg, None)(LogLevel.Trace) + + def logInfoImpl(c: LogCtx)(msg: c.Expr[String]): c.universe.Tree = + reflectiveLog(c)(msg, None)(LogLevel.Info) + + def logDebugImpl(c: LogCtx)(msg: c.Expr[String]): c.universe.Tree = + reflectiveLog(c)(msg, None)(LogLevel.Debug) + + def logWarnImpl(c: LogCtx)(msg: c.Expr[String]): c.universe.Tree = + reflectiveLog(c)(msg, None)(LogLevel.Warn) + + def logErrorImpl(c: LogCtx)(msg: c.Expr[String]): c.universe.Tree = + reflectiveLog(c)(msg, None)(LogLevel.Error) + + def logErrorWithCauseImpl(c: LogCtx)(msg: c.Expr[String], throwable: c.Expr[Throwable]): c.universe.Tree = + reflectiveLog(c)(msg, Some(throwable))(LogLevel.Error) +} diff --git a/zio-http-logging/src/main/scala-3/zhttp/logging/macros/LoggerMacroExtensions.scala b/zio-http-logging/src/main/scala-3/zhttp/logging/macros/LoggerMacroExtensions.scala new file mode 100644 index 000000000..a3f5aca43 --- /dev/null +++ b/zio-http-logging/src/main/scala-3/zhttp/logging/macros/LoggerMacroExtensions.scala @@ -0,0 +1,24 @@ +package zhttp.logging.macros + +import zhttp.logging.Logger +import zhttp.logging.macros.LoggerMacroImpl._ + +/** + * Core Logger class. + */ +trait LoggerMacroExtensions{ self: Logger => + import scala.language.experimental.macros + + val isDebugEnabled: Boolean + val isErrorEnabled: Boolean + val isInfoEnabled: Boolean + val isTraceEnabled: Boolean + val isWarnEnabled: Boolean + + inline def trace(inline msg: String): Unit = $ { logTraceImpl('self, 'msg) } + inline def debug(inline msg: String): Unit = $ { logDebugImpl('self, 'msg) } + inline def info(inline msg: String): Unit = $ { logInfoImpl('self, 'msg) } + inline def warn(inline msg: String): Unit = $ { logWarnImpl('self, 'msg) } + inline def error(inline msg: String): Unit = $ { logErrorImpl('self, 'msg) } + inline def error(inline msg: String, throwable: Throwable): Unit = ${logErrorWithCauseImpl('self, 'throwable, 'msg)} +} diff --git a/zio-http-logging/src/main/scala-3/zhttp/logging/macros/LoggerMacroImpl.scala b/zio-http-logging/src/main/scala-3/zhttp/logging/macros/LoggerMacroImpl.scala new file mode 100644 index 000000000..0c1d8f1a8 --- /dev/null +++ b/zio-http-logging/src/main/scala-3/zhttp/logging/macros/LoggerMacroImpl.scala @@ -0,0 +1,75 @@ +package zhttp.logging.macros + +import zhttp.logging.Logger.SourcePos +import zhttp.logging.{LogLevel, Logger} + +import scala.language.experimental.macros +import scala.quoted._ + +/** Macros that support the logging system. + * inspired from log4s macro + */ +private[zhttp] object LoggerMacroImpl { + inline def sourcePos(using qctx: Quotes): Expr[SourcePos] = { + val rootPosition = qctx.reflect.Position.ofMacroExpansion + val file = Expr(rootPosition.sourceFile.path.toString) + val line = Expr(rootPosition.startLine + 1) + '{SourcePos($file, $line)} + } + + + def logTraceImpl(logger: Expr[Logger], msg: Expr[String])(using qctx: Quotes): quoted.Expr[Any] = { + if(LogLevel.Trace >= Logger.detectedLevel) { + val pos = sourcePos(using qctx) + '{ + if ($logger.isTraceEnabled) $logger.dispatch($msg, LogLevel.Trace, None, Some($pos)) + } + } else { + '{} + } + } + + def logDebugImpl(logger: Expr[Logger], msg: Expr[String])(using qctx: Quotes): quoted.Expr[Any] = + if(LogLevel.Debug >= Logger.detectedLevel) { + '{ + if ($logger.isDebugEnabled) $logger.dispatch($msg, LogLevel.Debug, None, None) + } + }else { + '{} + } + + def logInfoImpl(logger: Expr[Logger], msg: Expr[String])(using qctx: Quotes): quoted.Expr[Any] = + if(LogLevel.Info >= Logger.detectedLevel) { + '{ + if ($logger.isInfoEnabled) $logger.dispatch($msg, LogLevel.Info, None, None) + } + }else { + '{} + } + + def logWarnImpl(logger: Expr[Logger], msg: Expr[String])(using qctx: Quotes): quoted.Expr[Any] = + if(LogLevel.Warn >= Logger.detectedLevel) { + '{ + if ($logger.isWarnEnabled) $logger.dispatch($msg, LogLevel.Warn, None, None) + } + }else { + '{} + } + + def logErrorWithCauseImpl(logger: Expr[Logger], t: Expr[Throwable], msg: Expr[String])(using qctx: Quotes): quoted.Expr[Any] = + if(LogLevel.Error >= Logger.detectedLevel) { + '{ + if ($logger.isErrorEnabled) $logger.dispatch($msg, LogLevel.Error, Some($t), None) + } + }else { + '{} + } + + + def logErrorImpl(logger: Expr[Logger], msg: Expr[String])(using qctx: Quotes): quoted.Expr[Any] = + if(LogLevel.Error >= Logger.detectedLevel) { + '{if ($logger.isErrorEnabled) $logger.dispatch($msg, LogLevel.Error, None, None)} + }else { + '{} + } +} diff --git a/zio-http-logging/src/main/scala/zhttp/logging/LogFormat.scala b/zio-http-logging/src/main/scala/zhttp/logging/LogFormat.scala new file mode 100644 index 000000000..48c659a5f --- /dev/null +++ b/zio-http-logging/src/main/scala/zhttp/logging/LogFormat.scala @@ -0,0 +1,188 @@ +package zhttp.logging + +import zhttp.logging.LogFormat.DateFormat.ISODateTime + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +object Setup { + final case class DefaultFormat(format: LogLine => String) { + def apply(line: LogLine): String = format(line) + } + +} + +sealed trait LogFormat { self => + import LogFormat._ + + def <+>(that: LogFormat): LogFormat = self combine that + + def combine(that: LogFormat): LogFormat = LogFormat.Combine(self, that) + + final def color(color: Color): LogFormat = ColorWrap(color, self) + + final def wrap(wrapper: TextWrapper): LogFormat = TextWrappers(wrapper, self) + + final def fixed(size: Int): LogFormat = Fixed(size, self) + + final def |-|(other: LogFormat): LogFormat = Spaced(self, other) + + final def -(other: LogFormat): LogFormat = Dash(self, other) + + final def \\(other: LogFormat): LogFormat = NewLine(self, other) + + final def trim: LogFormat = Trim(self) + + def apply(line: LogLine): String = LogFormat.run(self, line) + +} + +object LogFormat { + + sealed trait DateFormat + object DateFormat { + case object ISODateTime extends DateFormat + } + + sealed trait Color + object Color { + case object RED extends Color + case object BLUE extends Color + case object YELLOW extends Color + case object CYAN extends Color + case object GREEN extends Color + case object MAGENTA extends Color + case object WHITE extends Color + case object RESET extends Color + case object DEFAULT extends Color + + def asConsole(color: Color): String = color match { + case RED => Console.RED + case BLUE => Console.BLUE + case YELLOW => Console.YELLOW + case CYAN => Console.CYAN + case GREEN => Console.GREEN + case MAGENTA => Console.MAGENTA + case WHITE => Console.WHITE + case RESET => Console.RESET + case DEFAULT => "" + } + } + + sealed trait TextWrapper + object TextWrapper { + case object BRACKET extends TextWrapper + case object QUOTED extends TextWrapper + case object EMPTY extends TextWrapper + } + + final case class FormatDate(dateFormat: DateFormat) extends LogFormat + final case class ThreadName(includeThreadName: Boolean) extends LogFormat + final case class ThreadId(includeThreadId: Boolean) extends LogFormat + case object LoggerLevel extends LogFormat + final case class Combine(left: LogFormat, right: LogFormat) extends LogFormat + final case class ColorWrap(color: Color, configuration: LogFormat) extends LogFormat + final case class LineColor(info: Color, error: Color, debug: Color, trace: Color, warn: Color) extends LogFormat + final case class TextWrappers(wrapper: TextWrapper, configuration: LogFormat) extends LogFormat + final case class Fixed(size: Int, configuration: LogFormat) extends LogFormat + final case class Spaced(left: LogFormat, right: LogFormat) extends LogFormat + final case class Dash(left: LogFormat, right: LogFormat) extends LogFormat + final case class NewLine(left: LogFormat, right: LogFormat) extends LogFormat + final case class Trim(logFmt: LogFormat) extends LogFormat + case object SourceLocation extends LogFormat + case object Msg extends LogFormat + case object Tags extends LogFormat + + def logLevel: LogFormat = LoggerLevel + def date(dateFormat: DateFormat): LogFormat = FormatDate(dateFormat) + def threadName: LogFormat = ThreadName(true) + def threadId: LogFormat = ThreadId(true) + def msg: LogFormat = Msg + def sourceLocation: LogFormat = SourceLocation + def tags: LogFormat = Tags + + def color(info: Color, error: Color, debug: Color, trace: Color, warn: Color): LogFormat = + LineColor(info, error, debug, trace, warn) + + private def run(logFormat: LogFormat, logLine: LogLine): String = { + + logFormat match { + case SourceLocation => logLine.sourceLocation.map(sp => s"${sp.file} ${sp.line}").getOrElse("") + case FormatDate(dateFormat) => formatDate(dateFormat, logLine.timestamp) + case ThreadName(includeThreadName) => if (includeThreadName) logLine.thread.getName else "" + case ThreadId(includeThreadId) => if (includeThreadId) logLine.thread.getId.toString else "" + case LoggerLevel => logLine.level.name + case Combine(left, right) => left(logLine) ++ right(logLine) + case ColorWrap(color, conf) => + colorText(color, conf(logLine)) + case TextWrappers(wrapper, conf) => wrap(wrapper, conf(logLine)) + case Fixed(_, conf) => conf(logLine) + case Spaced(left, right) => left(logLine) + " " + right(logLine) + case Dash(left, right) => left(logLine) + " - " + right(logLine) + case NewLine(left, right) => left(logLine) + "\n" + right(logLine) + case Msg => logLine.message + case Trim(conf) => conf(logLine).trim + case LineColor(info, error, debug, trace, warn) => + logLine.level match { + case LogLevel.Trace => Color.asConsole(trace) + case LogLevel.Debug => Color.asConsole(debug) + case LogLevel.Info => Color.asConsole(info) + case LogLevel.Warn => Color.asConsole(warn) + case LogLevel.Error => Color.asConsole(error) + } + case Tags => logLine.tags.mkString(",") + } + } + + private def formatDate(format: LogFormat.DateFormat, time: LocalDateTime): String = format match { + case DateFormat.ISODateTime => time.format(DateTimeFormatter.ISO_TIME) + } + + /** + * Wrap a text if the text is not empty. + */ + private def wrap(wrapper: TextWrapper, value: String): String = { + if (value.isEmpty) "" + else + wrapper match { + case TextWrapper.BRACKET => s"[$value]" + case TextWrapper.QUOTED => s"{$value}" + case TextWrapper.EMPTY => value + } + } + + private def colorText(color: Color, value: String): String = { + val consoleColor = Color.asConsole(color) + color match { + case Color.RED => s"$consoleColor$value${Console.RESET}" + case Color.BLUE => s"$consoleColor$value${Console.RESET}" + case Color.YELLOW => s"$consoleColor$value${Console.RESET}" + case Color.CYAN => s"$consoleColor$value${Console.RESET}" + case Color.GREEN => s"$consoleColor$value${Console.RESET}" + case Color.MAGENTA => s"$consoleColor$value${Console.RESET}" + case Color.WHITE => s"$consoleColor$value${Console.RESET}" + case Color.RESET => Console.RESET + case Color.DEFAULT => value + } + } + + val minimal: LogFormat = + LogFormat.Tags.wrap(TextWrapper.BRACKET) |-| + LogFormat.sourceLocation.wrap(TextWrapper.BRACKET) |-| + LogFormat.msg + + val maximus: LogFormat = + LogFormat.Tags.wrap(TextWrapper.BRACKET) |-| + LogFormat.date(ISODateTime) |-| + LogFormat.threadName.wrap(TextWrapper.BRACKET) |-| + LogFormat.sourceLocation.wrap(TextWrapper.BRACKET) |-| + LogFormat.logLevel - LogFormat.msg + + val colored: LogFormat = LogFormat.color( + info = Color.GREEN, + error = Color.RED, + debug = Color.CYAN, + warn = Color.YELLOW, + trace = Color.WHITE, + ) <+> minimal +} diff --git a/zio-http-logging/src/main/scala/zhttp/logging/LogLevel.scala b/zio-http-logging/src/main/scala/zhttp/logging/LogLevel.scala new file mode 100644 index 000000000..87fbf0a13 --- /dev/null +++ b/zio-http-logging/src/main/scala/zhttp/logging/LogLevel.scala @@ -0,0 +1,72 @@ +package zhttp.logging + +sealed abstract class LogLevel(val id: Int) extends Product with Serializable { self => + final def >(other: LogLevel): Boolean = self.id > other.id + + final def >=(other: LogLevel): Boolean = self.id >= other.id + + final def <(other: LogLevel): Boolean = self.id < other.id + + final def <=(other: LogLevel): Boolean = self.id <= other.id + + final def name: String = self match { + case LogLevel.Trace => "Trace" + case LogLevel.Debug => "Debug" + case LogLevel.Info => "Info" + case LogLevel.Warn => "Warn" + case LogLevel.Error => "Error" + } + + final override def toString: String = name +} + +/** + * Defines standard log levels. + */ +object LogLevel { + + /** + * Lists all the possible log levels + */ + val all: List[LogLevel] = List( + Debug, + Error, + Info, + Trace, + Warn, + ) + + /** + * Automatically detects the level from the environment variable + */ + def detectFromEnv(name: String): Option[LogLevel] = + sys.env.get(name).map(fromString) + + /** + * Automatically detects the level from the system properties + */ + def detectFromProps(name: String): Option[LogLevel] = + sys.props.get(name).map(fromString) + + /** + * Detects the LogLevel given any random string + */ + def fromString(string: String): LogLevel = string.toUpperCase match { + case "TRACE" => Trace + case "DEBUG" => Debug + case "INFO" => Info + case "WARN" => Warn + case "ERROR" => Error + case _ => Error + } + + case object Trace extends LogLevel(1) + + case object Debug extends LogLevel(2) + + case object Info extends LogLevel(3) + + case object Warn extends LogLevel(4) + + case object Error extends LogLevel(5) +} diff --git a/zio-http-logging/src/main/scala/zhttp/logging/LogLine.scala b/zio-http-logging/src/main/scala/zhttp/logging/LogLine.scala new file mode 100644 index 000000000..d68658c63 --- /dev/null +++ b/zio-http-logging/src/main/scala/zhttp/logging/LogLine.scala @@ -0,0 +1,15 @@ +package zhttp.logging + +import zhttp.logging.Logger.SourcePos + +import java.time.LocalDateTime + +final case class LogLine( + timestamp: LocalDateTime, + thread: Thread, + level: LogLevel, + message: String, + tags: List[String], + error: Option[Throwable], + sourceLocation: Option[SourcePos], +) diff --git a/zio-http-logging/src/main/scala/zhttp/logging/Logger.scala b/zio-http-logging/src/main/scala/zhttp/logging/Logger.scala new file mode 100644 index 000000000..e8738afe5 --- /dev/null +++ b/zio-http-logging/src/main/scala/zhttp/logging/Logger.scala @@ -0,0 +1,118 @@ +package zhttp.logging + +import zhttp.logging.Logger.SourcePos +import zhttp.logging.macros.LoggerMacroExtensions + +import java.nio.file.Path + +/** + * This is the base class for all logging operations. Logger is a collection of + * LoggerTransports. Internally whenever a message needs to be logged, it is + * broadcasted to all the available transports. The transports can internally + * decide what to do with the mssage and discard it if the message or the level + * is not relevant to the transport. + */ +final case class Logger(transports: List[LoggerTransport]) extends LoggerMacroExtensions { self => + + val isDebugEnabled: Boolean = transports.exists(_.isDebugEnabled) + val isErrorEnabled: Boolean = transports.exists(_.isErrorEnabled) + val isInfoEnabled: Boolean = transports.exists(_.isInfoEnabled) + val isTraceEnabled: Boolean = transports.exists(_.isTraceEnabled) + val isWarnEnabled: Boolean = transports.exists(_.isWarnEnabled) + + /** + * Modifies each transport + */ + private def foreach(f: LoggerTransport => LoggerTransport): Logger = Logger(transports.map(f(_))) + + /** + * Combines to loggers into one + */ + def ++(other: Logger): Logger = self combine other + + /** + * Combines to loggers into one + */ + def combine(other: Logger): Logger = Logger(self.transports ++ other.transports) + + /** + * Modifies the transports to read the log level from the passed environment + * variable. + */ + def detectLevelFromEnv(env: String): Logger = withLevel(LogLevel.detectFromEnv(env).getOrElse(LogLevel.Error)) + + /** + * Modifies the transports to read the log level from the set system property. + */ + def detectLevelFromProps(env: String): Logger = withLevel(LogLevel.detectFromProps(env).getOrElse(LogLevel.Error)) + + /** + * Dispatches the parameters to all the transports. Internally invoked by the + * macro. + */ + def dispatch( + msg: String, + level: LogLevel, + cause: Option[Throwable], + sourceLocation: Option[SourcePos], + ): Unit = transports.foreach(_.dispatch(msg, cause, level, sourceLocation)) + + /** + * Dispatches the parameters to all the transports. Internally invoked by the + * macro. + */ + def dispatch(msg: String, level: LogLevel): Unit = dispatch(msg, level, None, None) + + /** + * Creates a new logger that will log messages that start with the given + * prefix. + */ + def startsWith(prefix: String): Logger = withFilter(_.startsWith(prefix)) + + /** + * Creates a new logger that only log messages that are accepted by the + * provided filter. + */ + def withFilter(filter: String => Boolean): Logger = foreach(_.withFilter(filter)) + + /** + * Modifies all the transports to support the given log format + */ + def withFormat(format: LogFormat): Logger = foreach(_.withFormat(format)) + + /** + * Modifies the level for each transport. Messages that don't meet that level + * will not be logged by any of the transports + */ + def withLevel(level: LogLevel): Logger = foreach(_.withLevel(level)) + + /** + * Creates a new Logger with the provided tags + */ + def withTags(tags: Iterable[String]): Logger = foreach(_.addTags(tags)) + + /** + * Creates a new Logger with the provided tags + */ + def withTags(tags: String*): Logger = foreach(_.addTags(tags)) + + /** + * Adds a new transport to the logger + */ + def withTransport(transport: LoggerTransport): Logger = copy(transports = transport :: self.transports) + +} + +object Logger { + private[zhttp] val detectedLevel: LogLevel = LogLevel.detectFromProps("ZHttpLogLevel").getOrElse(LogLevel.Error) + + def console: Logger = Logger(List(LoggerTransport.console)) + + def file(path: Path): Logger = Logger(List(LoggerTransport.file(path))) + + def make(transport: LoggerTransport): Logger = transport.toLogger + + def make: Logger = Logger(Nil) + + final case class SourcePos(file: String, line: Int) +} diff --git a/zio-http-logging/src/main/scala/zhttp/logging/LoggerTransport.scala b/zio-http-logging/src/main/scala/zhttp/logging/LoggerTransport.scala new file mode 100644 index 000000000..91ff3baf4 --- /dev/null +++ b/zio-http-logging/src/main/scala/zhttp/logging/LoggerTransport.scala @@ -0,0 +1,117 @@ +package zhttp.logging + +import zhttp.logging.Logger.SourcePos + +import java.io.{PrintWriter, StringWriter} +import java.nio.file.{Files, Path, StandardOpenOption} +import java.time.LocalDateTime +import java.util + +/** + * Provides a way to build and configure transports for logging. Transports are + * used to, format and serialize LogLines and them to a backend. + */ +private[logging] abstract class LoggerTransport( + format: LogFormat = LogFormat.minimal, + val level: LogLevel = LogLevel.Error, + filter: String => Boolean = _ => true, + tags: List[String] = Nil, +) { self => + + def run(charSequence: CharSequence): Unit + + final private[zhttp] val isDebugEnabled: Boolean = self.level >= LogLevel.Debug + final private[zhttp] val isErrorEnabled: Boolean = self.level >= LogLevel.Error + final private[zhttp] val isInfoEnabled: Boolean = self.level >= LogLevel.Info + final private[zhttp] val isTraceEnabled: Boolean = self.level >= LogLevel.Trace + final private[zhttp] val isWarnEnabled: Boolean = self.level >= LogLevel.Warn + + final private def buildLines( + msg: String, + throwable: Option[Throwable], + logLevel: LogLevel, + tags: List[String], + sourceLocation: Option[SourcePos], + ): List[LogLine] = { + throwable.fold( + List(LogLine(LocalDateTime.now(), thread, logLevel, msg, tags, throwable, sourceLocation)), + ) { t => + List( + LogLine(LocalDateTime.now(), thread, logLevel, msg, tags, throwable, sourceLocation), + LogLine( + LocalDateTime.now(), + thread, + logLevel, + stackTraceAsString(t), + tags, + throwable, + sourceLocation, + ), + ) + } + } + + final def copy( + format: LogFormat = self.format, + level: LogLevel = self.level, + filter: String => Boolean = self.filter, + tags: List[String] = self.tags, + ): LoggerTransport = new LoggerTransport(format, level, filter, tags) { + override def run(charSequence: CharSequence): Unit = self.run(charSequence) + } + + final private def stackTraceAsString(throwable: Throwable): String = { + val sw = new StringWriter + throwable.printStackTrace(new PrintWriter(sw)) + sw.toString + } + + final private def thread = Thread.currentThread() + + final def addTags(tags: Iterable[String]): LoggerTransport = self.copy(tags = self.tags ++ tags) + + final def dispatch( + msg: String, + cause: Option[Throwable], + level: LogLevel, + sourceLocation: Option[SourcePos], + ): Unit = + if (self.level <= level) { + buildLines(msg, cause, level, self.tags, sourceLocation).foreach { line => + if (filter(format(line))) run(format(line)) + } + } + + /** + * Converts the current LoggerTransport to a Logger. + */ + final def toLogger: Logger = Logger(List(self)) + + final def withFilter(filter: String => Boolean): LoggerTransport = self.copy(filter = filter) + + final def withFormat(format: LogFormat): LoggerTransport = self.copy(format = format) + + final def withLevel(level: LogLevel): LoggerTransport = self.copy(level = level) + + final def withTags(tags: List[String]): LoggerTransport = self.copy(tags = tags) + +} + +object LoggerTransport { + val empty: LoggerTransport = new LoggerTransport() { + override def run(charSequence: CharSequence): Unit = () + } + + def console: LoggerTransport = new LoggerTransport(format = LogFormat.colored) { + override def run(charSequence: CharSequence): Unit = println(charSequence) + } + + def file(path: Path): LoggerTransport = new LoggerTransport() { + override def run(charSequence: CharSequence): Unit = Files.write( + path, + util.Arrays.asList(charSequence), + StandardOpenOption.APPEND, + StandardOpenOption.CREATE, + ): Unit + } +} diff --git a/zio-http-logging/src/test/scala/zhttp/logging/LogLevelSpec.scala b/zio-http-logging/src/test/scala/zhttp/logging/LogLevelSpec.scala new file mode 100644 index 000000000..17bab1a82 --- /dev/null +++ b/zio-http-logging/src/test/scala/zhttp/logging/LogLevelSpec.scala @@ -0,0 +1,24 @@ +package zhttp.logging + +import zio.test._ + +object LogLevelSpec extends ZIOSpecDefault { + def spec = suite("LogLevelSpec")( + test("log level order") { + val act = LogLevel.all + val exp = List(LogLevel.Trace, LogLevel.Debug, LogLevel.Info, LogLevel.Warn, LogLevel.Error) + val sorted = act.sortBy(_.id) + assertTrue(sorted == exp) + }, + test("encode decode") { + checkAll(Gen.fromIterable(LogLevel.all)) { level => + assertTrue(LogLevel.fromString(level.toString) == level) + } + }, + test("any value with the exception of defined values for LogLevel should be set to Error log level.") { + checkAll(Gen.fromIterable(List("not defined", "unknown", "disable"))) { level => + assertTrue(LogLevel.fromString(level) == LogLevel.Error) + } + }, + ) +} diff --git a/zio-http-logging/src/test/scala/zhttp/logging/LoggerSpec.scala b/zio-http-logging/src/test/scala/zhttp/logging/LoggerSpec.scala new file mode 100644 index 000000000..0237fb22b --- /dev/null +++ b/zio-http-logging/src/test/scala/zhttp/logging/LoggerSpec.scala @@ -0,0 +1,42 @@ +package zhttp.logging +import zio.test._ + +import scala.collection.mutable.ListBuffer + +object LoggerSpec extends ZIOSpecDefault { + + override def spec = suite("LoggerSpec")( + test("logs nothing") { + val message = "ABC" + val logger = Logger.make.withLevel(LogLevel.Error) + + checkAll(Gen.fromIterable(LogLevel.all.filter(_ != LogLevel.Error))) { level => + val transport = MemoryTransport.make + logger.withTransport(transport).dispatch(message, level) + assertTrue(transport.stdout == "") + } + }, + test("logs message") { + val format = LogFormat.logLevel |-| LogFormat.msg + val message = "ABC" + checkAll(Gen.fromIterable(LogLevel.all)) { level => + val transport = MemoryTransport.make + Logger.make.withTransport(transport).withLevel(LogLevel.Trace).withFormat(format).dispatch(message, level) + assertTrue(transport.stdout == s"${level} ABC") + } + }, + ) + + final class MemoryTransport extends LoggerTransport() { + val buffer: ListBuffer[String] = ListBuffer.empty[String] + def reset(): Unit = buffer.clear() + def stdout: String = buffer.mkString("\n") + + override def run(charSequence: CharSequence): Unit = buffer += charSequence.toString + + } + + object MemoryTransport { + def make: MemoryTransport = new MemoryTransport + } +} diff --git a/zio-http/src/main/scala/zhttp/endpoint/CanCombine.scala b/zio-http/src/main/scala/zhttp/endpoint/CanCombine.scala deleted file mode 100644 index 6686dfc31..000000000 --- a/zio-http/src/main/scala/zhttp/endpoint/CanCombine.scala +++ /dev/null @@ -1,35 +0,0 @@ -package zhttp.endpoint - -sealed trait CanCombine[A, B] { - type Out -} - -object CanCombine { - type Aux[A, B, C] = CanCombine[A, B] { - type Out = C - } - - // scalafmt: { maxColumn = 1200 } - implicit def combine0[A, B](implicit ev: A =:= Unit): CanCombine.Aux[A, B, B] = null - implicit def combine1[A, B](implicit evA: CanExtract[A], evB: CanExtract[B]): CanCombine.Aux[A, B, (A, B)] = null - implicit def combine2[A, B, T1, T2](implicit evA: A =:= (T1, T2), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, B)] = null - implicit def combine3[A, B, T1, T2, T3](implicit evA: A =:= (T1, T2, T3), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, B)] = null - implicit def combine4[A, B, T1, T2, T3, T4](implicit evA: A =:= (T1, T2, T3, T4), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, B)] = null - implicit def combine5[A, B, T1, T2, T3, T4, T5](implicit evA: A =:= (T1, T2, T3, T4, T5), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, B)] = null - implicit def combine6[A, B, T1, T2, T3, T4, T5, T6](implicit evA: A =:= (T1, T2, T3, T4, T5, T6), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, B)] = null - implicit def combine7[A, B, T1, T2, T3, T4, T5, T6, T7](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, B)] = null - implicit def combine8[A, B, T1, T2, T3, T4, T5, T6, T7, T8](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, B)] = null - implicit def combine9[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, B)] = null - implicit def combine10[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, B)] = null - implicit def combine11[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, B)] = null - implicit def combine12[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, B)] = null - implicit def combine13[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, B)] = null - implicit def combine14[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, B)] = null - implicit def combine15[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, B)] = null - implicit def combine16[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, B)] = null - implicit def combine17[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, B)] = null - implicit def combine18[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, B)] = null - implicit def combine19[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, B)] = null - implicit def combine20[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, B)] = null - implicit def combine21[A, B, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21](implicit evA: A =:= (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21), evB: CanExtract[B]): CanCombine.Aux[A, B, (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, B)] = null -} diff --git a/zio-http/src/main/scala/zhttp/endpoint/CanConstruct.scala b/zio-http/src/main/scala/zhttp/endpoint/CanConstruct.scala index c3e276b82..8b1378917 100644 --- a/zio-http/src/main/scala/zhttp/endpoint/CanConstruct.scala +++ b/zio-http/src/main/scala/zhttp/endpoint/CanConstruct.scala @@ -1,53 +1 @@ -package zhttp.endpoint -import zhttp.http.{Http, HttpApp, Request, Response} -import zio.ZIO - -/** - * Constructors to make an HttpApp using an Endpoint - */ -sealed trait CanConstruct[A, B] { - type ROut - type EOut - def make(route: Endpoint[A], f: Request.ParameterizedRequest[A] => B): HttpApp[ROut, EOut] -} - -object CanConstruct { - type Aux[R, E, A, B] = CanConstruct[A, B] { - type ROut = R - type EOut = E - } - - implicit def response[A]: Aux[Any, Nothing, A, Response] = new CanConstruct[A, Response] { - override type ROut = Any - override type EOut = Nothing - - override def make(route: Endpoint[A], f: Request.ParameterizedRequest[A] => Response): HttpApp[Any, Nothing] = - Http - .collectHttp[Request] { case req => - route.extract(req) match { - case Some(value) => Http.succeed(f(Request.ParameterizedRequest(req, value))) - case None => Http.empty - } - } - } - - implicit def responseZIO[R, E, A]: Aux[R, E, A, ZIO[R, E, Response]] = - new CanConstruct[A, ZIO[R, E, Response]] { - override type ROut = R - override type EOut = E - - override def make( - route: Endpoint[A], - f: Request.ParameterizedRequest[A] => ZIO[R, E, Response], - ): HttpApp[R, E] = { - Http - .collectHttp[Request] { case req => - route.extract(req) match { - case Some(value) => Http.fromZIO(f(Request.ParameterizedRequest(req, value))) - case None => Http.empty - } - } - } - } -} diff --git a/zio-http/src/main/scala/zhttp/endpoint/CanExtract.scala b/zio-http/src/main/scala/zhttp/endpoint/CanExtract.scala deleted file mode 100644 index fdde97182..000000000 --- a/zio-http/src/main/scala/zhttp/endpoint/CanExtract.scala +++ /dev/null @@ -1,18 +0,0 @@ -package zhttp.endpoint - -import scala.util.Try - -trait CanExtract[+A] { - def parse(data: String): Option[A] -} -object CanExtract { - implicit object IntImpl extends CanExtract[Int] { - override def parse(data: String): Option[Int] = Try(data.toInt).toOption - } - implicit object StringImpl extends CanExtract[String] { - override def parse(data: String): Option[String] = Option(data) - } - implicit object BooleanImpl extends CanExtract[Boolean] { - override def parse(data: String): Option[Boolean] = Try(data.toBoolean).toOption - } -} diff --git a/zio-http/src/main/scala/zhttp/endpoint/Endpoint.scala b/zio-http/src/main/scala/zhttp/endpoint/Endpoint.scala deleted file mode 100644 index 5263a78ef..000000000 --- a/zio-http/src/main/scala/zhttp/endpoint/Endpoint.scala +++ /dev/null @@ -1,52 +0,0 @@ -package zhttp.endpoint - -import zhttp.http._ - -/** - * Description of an Http endpoint containing a Method and a ParameterList - */ -final case class Endpoint[A](method: Method, params: ParameterList[A]) { self => - - /** - * Appends a string literal to the endpoint - */ - def /(name: String): Endpoint[A] = Endpoint(self.method, name :: self.params) - - /** - * Appends the parameter to the endpoint - */ - def /[B, C](other: Parameter[B])(implicit ev: CanCombine.Aux[A, B, C]): Endpoint[C] = - Endpoint(self.method, other :: self.params) - - /** - * Creates an HttpApp from a Request to Response function - */ - def to[B](f: Request.ParameterizedRequest[A] => B)(implicit ctor: CanConstruct[A, B]): HttpApp[ctor.ROut, ctor.EOut] = - ctor.make(self, f) - - private[zhttp] def extract(path: Path): Option[A] = Endpoint.extract(path, self) - private[zhttp] def extract(request: Request): Option[A] = Endpoint.extract(request, self) -} - -private[zhttp] object Endpoint { - - def unit[A]: Option[A] = Option(().asInstanceOf[A]) - - /** - * Create Route[Unit] from a Method - */ - def fromMethod(method: Method): Endpoint[Unit] = Endpoint(method, ParameterList.Empty) - - /** - * Extracts the parameter list from the given path - */ - private[zhttp] def extract[A](path: Path, route: Endpoint[A]): Option[A] = route.params.extract(path) - - /** - * Extracts the parameter list from the given request - */ - private[zhttp] def extract[A](request: Request, self: Endpoint[A]): Option[A] = - if (self.method == request.method) { self.extract(request.path) } - else None - -} diff --git a/zio-http/src/main/scala/zhttp/endpoint/Parameter.scala b/zio-http/src/main/scala/zhttp/endpoint/Parameter.scala deleted file mode 100644 index 5c1ca4ee1..000000000 --- a/zio-http/src/main/scala/zhttp/endpoint/Parameter.scala +++ /dev/null @@ -1,19 +0,0 @@ -package zhttp.endpoint - -sealed trait Parameter[+A] { self => - def parse(string: String): Option[A] = Parameter.extract(self, string) -} -object Parameter { - private[zhttp] final case class Literal(s: String) extends Parameter[Unit] - private[zhttp] final case class Param[A](r: CanExtract[A]) extends Parameter[A] - - private[zhttp] def extract[A](rt: Parameter[A], string: String): Option[A] = rt match { - case Parameter.Literal(s) => if (s == string) Endpoint.unit else None - case Parameter.Param(r) => r.parse(string) - } - - /** - * Creates a new Parameter placeholder for an Endpoint - */ - def apply[A](implicit ev: CanExtract[A]): Parameter[A] = Parameter.Param(ev) -} diff --git a/zio-http/src/main/scala/zhttp/endpoint/ParameterList.scala b/zio-http/src/main/scala/zhttp/endpoint/ParameterList.scala deleted file mode 100644 index 11383a453..000000000 --- a/zio-http/src/main/scala/zhttp/endpoint/ParameterList.scala +++ /dev/null @@ -1,43 +0,0 @@ -package zhttp.endpoint - -import zhttp.endpoint.Parameter.Literal -import zhttp.http.Path - -import scala.annotation.tailrec - -/** - * A special type-safe data structure that holds a list of Endpoint Parameters. - */ -sealed trait ParameterList[+A] { self => - private[zhttp] def extract(path: Path): Option[A] = ParameterList.extract(self, path.toList.reverse) - - def ::[A1 >: A, B, C](head: Parameter[B])(implicit ev: CanCombine.Aux[A1, B, C]): ParameterList[C] = - ParameterList.Cons(head, self) - - def ::(literal: String): ParameterList[A] = ParameterList.Cons(Literal(literal), self) -} -object ParameterList { - private[zhttp] case object Empty extends ParameterList[Unit] - private[zhttp] final case class Cons[A, B, C](head: Parameter[B], tail: ParameterList[A]) extends ParameterList[C] - - def empty: ParameterList[Unit] = Empty - - private def extract[A](r: ParameterList[A], p: List[String]): Option[A] = { - @tailrec - def loop(r: ParameterList[Any], p: List[String], output: List[Any]): Option[Any] = { - r match { - case Empty => TupleBuilder(output) - case Cons(head, tail) => - if (p.isEmpty) None - else { - head.parse(p.head) match { - case Some(value) => - if (value.isInstanceOf[Unit]) loop(tail, p.tail, output) else loop(tail, p.tail, value :: output) - case None => None - } - } - } - } - loop(r.asInstanceOf[ParameterList[Any]], p, List.empty[Any]).asInstanceOf[Option[A]] - } -} diff --git a/zio-http/src/main/scala/zhttp/endpoint/TupleBuilder.scala b/zio-http/src/main/scala/zhttp/endpoint/TupleBuilder.scala deleted file mode 100644 index 9874ea679..000000000 --- a/zio-http/src/main/scala/zhttp/endpoint/TupleBuilder.scala +++ /dev/null @@ -1,37 +0,0 @@ -package zhttp.endpoint - -object TupleBuilder { - - /** - * Utility to create Tuple from a list - */ - def apply(input: List[Any]): Option[Any] = - input match { - // scalafmt: { maxColumn = 1200 } - case List() => Some(()) - case List(a0) => Some(a0) - case List(a0, a1) => Some((a0, a1)) - case List(a0, a1, a2) => Some((a0, a1, a2)) - case List(a0, a1, a2, a3) => Some((a0, a1, a2, a3)) - case List(a0, a1, a2, a3, a4) => Some((a0, a1, a2, a3, a4)) - case List(a0, a1, a2, a3, a4, a5) => Some((a0, a1, a2, a3, a4, a5)) - case List(a0, a1, a2, a3, a4, a5, a6) => Some((a0, a1, a2, a3, a4, a5, a6)) - case List(a0, a1, a2, a3, a4, a5, a6, a7) => Some((a0, a1, a2, a3, a4, a5, a6, a7)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)) - case List(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21) => Some((a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21)) - case _ => None - } - // scalafmt: { maxColumn = 120 } -} diff --git a/zio-http/src/main/scala/zhttp/endpoint/package.scala b/zio-http/src/main/scala/zhttp/endpoint/package.scala deleted file mode 100644 index 0f976923d..000000000 --- a/zio-http/src/main/scala/zhttp/endpoint/package.scala +++ /dev/null @@ -1,20 +0,0 @@ -package zhttp - -import zhttp.http.Method - -package object endpoint { - - /** - * Extends Http Method to support syntax to create endpoints. - */ - implicit class EndpointSyntax(method: Method) { - def /(name: String): Endpoint[Unit] = Endpoint.fromMethod(method) / name - def /[A](token: Parameter[A])(implicit ev: CanCombine.Aux[Unit, A, A]): Endpoint[A] = - Endpoint.fromMethod(method) / token - } - - /** - * Alias to `Parameter[A]` - */ - final def *[A](implicit ev: CanExtract[A]): Parameter[A] = Parameter[A] -} diff --git a/zio-http/src/main/scala/zhttp/html/Html.scala b/zio-http/src/main/scala/zhttp/html/Html.scala index 2f004a750..e58e398e3 100644 --- a/zio-http/src/main/scala/zhttp/html/Html.scala +++ b/zio-http/src/main/scala/zhttp/html/Html.scala @@ -22,6 +22,8 @@ object Html { implicit def fromDomElement(element: Dom): Html = Html.Single(element) + implicit def fromUnit(unit: Unit): Html = Html.Empty + private[zhttp] case class Single(element: Dom) extends Html private[zhttp] final case class Multiple(children: Seq[Dom]) extends Html diff --git a/zio-http/src/main/scala/zhttp/http/Cookie.scala b/zio-http/src/main/scala/zhttp/http/Cookie.scala index 8388313bb..a13f58ca3 100644 --- a/zio-http/src/main/scala/zhttp/http/Cookie.scala +++ b/zio-http/src/main/scala/zhttp/http/Cookie.scala @@ -124,16 +124,16 @@ final case class Cookie( */ def encode: String = { val c = secret match { - case Some(sec) => content + "." + signContent(sec) - case None => content + case Some(sec) if sec.nonEmpty => content + "." + signContent(sec) + case _ => content } val cookie = List( Some(s"$name=$c"), expires.map(e => s"Expires=$e"), maxAge.map(a => s"Max-Age=${a.toString}"), - domain.map(d => s"Domain=$d"), - path.map(p => s"Path=${p.encode}"), + domain.filter(_.nonEmpty).map(d => s"Domain=$d"), + path.filter(_.nonEmpty).map(p => s"Path=${p.encode}"), if (isSecure) Some("Secure") else None, if (isHttpOnly) Some("HttpOnly") else None, sameSite.map(s => s"SameSite=${s.asString}"), @@ -233,8 +233,8 @@ object Cookie { domain = headerValue.substring(curr + 7, next) } else if (headerValue.regionMatches(true, curr, fieldPath, 0, fieldPath.length)) { val v = headerValue.substring(curr + 5, next) - if (!v.isEmpty) { - path = Path(v) + if (v.nonEmpty) { + path = Path.decode(v) } } else if (headerValue.regionMatches(true, curr, fieldSecure, 0, fieldSecure.length)) { secure = true @@ -267,10 +267,20 @@ object Cookie { sameSite = Option(sameSite), ) else - null + Cookie( + "", + "", + expires = Option(expires), + maxAge = maxAge, + domain = Option(domain), + path = Option(path), + isSecure = secure, + isHttpOnly = httpOnly, + sameSite = Option(sameSite), + ) secret match { - case Some(s) => { + case Some(s) if s.nonEmpty => { if (decodedCookie != null) { val index = decodedCookie.content.lastIndexOf('.') val signature = decodedCookie.content.slice(index + 1, decodedCookie.content.length) @@ -281,7 +291,7 @@ object Cookie { else null } else decodedCookie } - case None => decodedCookie + case _ => decodedCookie.copy(secret = secret) } } @@ -289,17 +299,19 @@ object Cookie { /** * Decodes from `Cookie` header value inside of Request into a cookie */ - def decodeRequestCookie(headerValue: String): Option[List[Cookie]] = { - val cookies: Array[String] = headerValue.split(';').map(_.trim) - val x: List[Option[Cookie]] = cookies.toList.map(a => { - val (name, content) = splitNameContent(a) - if (name.isEmpty && content.isEmpty) None - else Some(Cookie(name, content)) - }) - - if (x.contains(None)) - None - else Some(x.map(_.get)) + def decodeRequestCookie(headerValue: String): List[Cookie] = { + if (headerValue.nonEmpty) { + val cookies: Array[String] = headerValue.split(';').map(_.trim) + val x: List[Option[Cookie]] = cookies.toList.map(a => { + val (name, content) = splitNameContent(a) + if (name.isEmpty && content.isEmpty) Some(Cookie("", "")) + else Some(Cookie(name, content)) + }) + + if (x.contains(None)) + List.empty + else x.map(_.get) + } else List.empty } @inline @@ -308,7 +320,7 @@ object Cookie { if (i >= 0) { (str.substring(0, i).trim, str.substring(i + 1).trim) } else { - (str.trim, null) + (str.trim, "") } } diff --git a/zio-http/src/main/scala/zhttp/http/Http.scala b/zio-http/src/main/scala/zhttp/http/Http.scala index b0ca17514..d63da6b2b 100644 --- a/zio-http/src/main/scala/zhttp/http/Http.scala +++ b/zio-http/src/main/scala/zhttp/http/Http.scala @@ -1,18 +1,18 @@ package zhttp.http import io.netty.buffer.{ByteBuf, ByteBufUtil} -import io.netty.channel.ChannelHandler +import io.netty.channel.{ChannelHandler, ChannelHandlerContext} import io.netty.handler.codec.http.HttpHeaderNames import zhttp.html._ import zhttp.http.headers.HeaderModifier -import zhttp.service.server.ServerTime -import zhttp.service.{Handler, HttpRuntime, Server} +import zhttp.service.{Handler, HttpRuntime, Server, ServerResponseWriter} import zio.ZIO.attemptBlocking import zio._ import zio.stream.ZStream -import java.io.File +import java.io.{File, IOException} import java.net +import java.net.{InetAddress, InetSocketAddress} import java.nio.charset.Charset import java.nio.file.Paths import scala.annotation.unused @@ -395,6 +395,13 @@ sealed trait Http[-R, +E, -A, +B] extends (A => ZIO[R, Option[E], B]) { self => final def mapZIO[R1 <: R, E1 >: E, C](bFc: B => ZIO[R1, E1, C]): Http[R1, E1, A, C] = self >>> Http.fromFunctionZIO(bFc) + /** + * Returns a new Http where the error channel has been merged into the success + * channel to their common combined type. + */ + final def merge[E1 >: E, B1 >: B](implicit ev: E1 =:= B1): Http[R, Nothing, A, B1] = + self.catchAll(Http.succeed(_)) + /** * Named alias for @@ */ @@ -631,11 +638,11 @@ object Http { private[zhttp] def compile[R1 <: R]( zExec: HttpRuntime[R1], settings: Server.Config[R1, Throwable], - serverTimeGenerator: ServerTime, + resWriter: ServerResponseWriter[R1], )(implicit evE: E <:< Throwable, ): ChannelHandler = - Handler(http.asInstanceOf[HttpApp[R1, Throwable]], zExec, settings, serverTimeGenerator) + Handler(http.asInstanceOf[HttpApp[R1, Throwable]], zExec, settings, resWriter) /** * Patches the response produced by the app @@ -670,7 +677,7 @@ object Http { /** * Applies Http based on the path */ - def whenPathEq(p: Path): HttpApp[R, E] = http.whenPathEq(p.toString) + def whenPathEq(p: Path): HttpApp[R, E] = http.whenPathEq(p.encode) /** * Applies Http based on the path as string @@ -727,6 +734,11 @@ object Http { def combine[R, E, A, B](i: Iterable[Http[R, E, A, B]]): Http[R, E, A, B] = i.reduce(_.defaultWith(_)) + /** + * Provides access to the request's ChannelHandlerContext + */ + def context: Http[Any, Nothing, Request, ChannelHandlerContext] = Http.fromFunction[Request](_.unsafeContext) + /** * Returns an http app that dies with the specified `Throwable`. This method * can be used for terminating an app because a defect has been detected in @@ -936,6 +948,17 @@ object Http { */ def ok: HttpApp[Any, Nothing] = status(Status.Ok) + /** + * Provides access to the request's remote address + */ + def remoteAddress: Http[Any, IOException, Request, InetAddress] = + context flatMap { ctx => + ctx.channel().remoteAddress() match { + case m: InetSocketAddress => Http.succeed(m.getAddress) + case _ => Http.fail(new IOException("Unable to get remote address")) + } + } + /** * Creates an Http app which always responds with the same value. */ @@ -946,11 +969,6 @@ object Http { */ 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.PartialRoute[A] = Http.PartialRoute(()) - /** * Creates an HTTP app which always responds with the same status code and * empty data. @@ -986,6 +1004,12 @@ object Http { */ def tooLarge: HttpApp[Any, Nothing] = Http.status(Status.RequestEntityTooLarge) + /** + * Provides low level access to an HttpApp to perform unsafe operations using + * the request's ChannelHandlerContext. + */ + def usingContext[R, E](f: ChannelHandlerContext => HttpApp[R, E]): HttpApp[R, E] = context.flatMap(f(_)) + // Ctor Help 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] = @@ -1016,11 +1040,6 @@ object Http { FromFunctionHExit(a => if (pf.isDefinedAt(a)) pf(a) else HExit.empty) } - 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 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 diff --git a/zio-http/src/main/scala/zhttp/http/HttpData.scala b/zio-http/src/main/scala/zhttp/http/HttpData.scala index 66e3c3255..aa2d89343 100644 --- a/zio-http/src/main/scala/zhttp/http/HttpData.scala +++ b/zio-http/src/main/scala/zhttp/http/HttpData.scala @@ -5,8 +5,8 @@ import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.{HttpContent, LastHttpContent} import io.netty.util.AsciiString import zhttp.http.HttpData.ByteBufConfig +import zio._ import zio.stream.ZStream -import zio.{Chunk, Task, ZIO} import java.io.FileInputStream import java.nio.charset.Charset @@ -126,19 +126,26 @@ object HttpData { } } - private[zhttp] final class UnsafeContent(private val httpContent: HttpContent) extends AnyVal { - def content: ByteBuf = httpContent.content() - - def isLast: Boolean = httpContent.isInstanceOf[LastHttpContent] - } - - private[zhttp] final class UnsafeChannel(private val ctx: ChannelHandlerContext) extends AnyVal { - def read(): Unit = ctx.read(): Unit - } - - private[zhttp] final case class UnsafeAsync(unsafeRun: (UnsafeChannel => UnsafeContent => Unit) => Unit) + private[zhttp] final case class UnsafeAsync(unsafeRun: (ChannelHandlerContext => HttpContent => Any) => Unit) extends HttpData { + private def isLast(msg: HttpContent): Boolean = msg.isInstanceOf[LastHttpContent] + + private def toQueue: ZIO[Any, Nothing, Queue[HttpContent]] = { + for { + queue <- Queue.bounded[HttpContent](1) + ctxPromise <- Promise.make[Nothing, ChannelHandlerContext] + runtime <- ZIO.runtime[Any] + _ <- ZIO.succeed( + unsafeRun { ch => + runtime.unsafeRun(ctxPromise.succeed(ch)) + msg => runtime.unsafeRun(queue.offer(msg)) + }, + ) + ch <- ctxPromise.await + } yield queue.mapM(msg => ZIO.succeed(ch.read()).unless(isLast(msg)).as(msg)) + } + /** * Encodes the HttpData into a ByteBuf. */ @@ -148,7 +155,7 @@ object HttpData { val buffer = Unpooled.compositeBuffer() msg => { buffer.addComponent(true, msg.content) - if (msg.isLast) cb(ZIO.succeed(buffer)) else ch.read() + if (isLast(msg)) cb(ZIO.succeed(buffer)) else ch.read(): Unit } }), ) @@ -158,15 +165,12 @@ object HttpData { * Encodes the HttpData into a Stream of ByteBufs */ override def toByteBufStream(config: ByteBufConfig): ZStream[Any, Throwable, ByteBuf] = - ZStream - .async[Any, Nothing, ByteBuf](cb => - unsafeRun(ch => - msg => { - cb(ZIO.succeed(Chunk(msg.content))) - if (msg.isLast) cb(ZIO.fail(None)) else ch.read() - }, - ), - ) + ZStream.unwrap { + for { + queue <- toQueue + stream = ZStream.fromQueueWithShutdown(queue).takeUntil(isLast(_)).map(_.content()) + } yield stream + } override def toHttp(config: ByteBufConfig): Http[Any, Throwable, Any, ByteBuf] = Http.fromZIO(toByteBuf(config)) @@ -180,8 +184,7 @@ object HttpData { * Encodes the HttpData into a ByteBuf. Takes in ByteBufConfig to have a * more fine grained control over the encoding. */ - override def toByteBuf(config: ByteBufConfig): Task[ByteBuf] = - ZIO.attempt(encode) + override def toByteBuf(config: ByteBufConfig): Task[ByteBuf] = ZIO.attempt(encode) /** * Encodes the HttpData into a Stream of ByteBufs. Takes in ByteBufConfig to @@ -200,7 +203,7 @@ object HttpData { private[zhttp] final case class BinaryChunk(data: Chunk[Byte]) extends Complete { - private def encode: ByteBuf = Unpooled.wrappedBuffer(data.toArray) + private def encode = Unpooled.wrappedBuffer(data.toArray) /** * Encodes the HttpData into a ByteBuf. diff --git a/zio-http/src/main/scala/zhttp/http/IsMono.scala b/zio-http/src/main/scala/zhttp/http/IsMono.scala new file mode 100644 index 000000000..9d761e9c7 --- /dev/null +++ b/zio-http/src/main/scala/zhttp/http/IsMono.scala @@ -0,0 +1,37 @@ +package zhttp.http + +import scala.annotation.implicitNotFound + +/** + * IsMono is a type-constraint that is used by the middleware api for allowing + * some operators only when the following condition is met. + * + * Condition: Since a middleware takes in an Http and returns a new Http, + * IsMono, makes sure that the type parameters of the incoming Http and the ones + * for the outgoing Http is the same. + * + * For Eg: IsMono will be defined for a middleware that looks as follows + * ``` + * val mid: Middleware[Any, Nothing, Request, Response, Request, Response] + * ``` + * + * This is because both the middleware is defined from (Request, Response) => + * (Request, Response). Consider another example: + * + * ``` + * val mid: Middleware[Any, Nothing, Request, Response, UserRequest, UserResponse] + * ``` + * + * In this case, the incoming and outgoing types are different viz. (Request, + * Response) => (UserRequest, UserResponse), hence there is no IsMono defined + * for such middlewares. + */ +@implicitNotFound( + "This operation is only valid if the incoming and outgoing type of Http are same.", +) +sealed trait IsMono[-AIn, +BIn, +AOut, -BOut] {} + +object IsMono extends IsMono[Any, Nothing, Nothing, Any] { + implicit def mono[AIn, BIn, AOut, BOut](implicit a: AIn =:= AOut, b: BIn =:= BOut): IsMono[AIn, BIn, AOut, BOut] = + IsMono +} diff --git a/zio-http/src/main/scala/zhttp/http/Middleware.scala b/zio-http/src/main/scala/zhttp/http/Middleware.scala index 89c12efd8..06e57c346 100644 --- a/zio-http/src/main/scala/zhttp/http/Middleware.scala +++ b/zio-http/src/main/scala/zhttp/http/Middleware.scala @@ -1,7 +1,7 @@ package zhttp.http -import zhttp.http.middleware.Web -import zio.{Duration, ZIO} +import zhttp.http.middleware.{MonoMiddleware, Web} +import zio._ /** * Middlewares are essentially transformations that one can apply on any Http to @@ -91,7 +91,7 @@ trait Middleware[-R, +E, +AIn, -BIn, -AOut, +BOut] { self => /** * Delays the production of Http output for the specified duration */ - final def delay(duration: Duration): Middleware[R, E, AIn, BIn, AOut, BOut] = + final def delay(duration: Duration): Middleware[R with Clock, E, AIn, BIn, AOut, BOut] = self.mapZIO(b => ZIO.succeed(b).delay(duration)) /** @@ -157,8 +157,9 @@ trait Middleware[-R, +E, +AIn, -BIn, -AOut, +BOut] { self => /** * Applies Middleware based only if the condition function evaluates to true */ - final def when[AOut0 <: AOut](cond: AOut0 => Boolean): Middleware[R, E, AIn, BIn, AOut0, BOut] = - whenZIO(a => ZIO.succeed(cond(a))) + final def when[AOut0 <: AOut](cond: AOut0 => Boolean)(implicit + ev: IsMono[AIn, BIn, AOut0, BOut], + ): Middleware[R, E, AIn, BIn, AOut0, BOut] = self.whenZIO(a => ZIO.succeed(cond(a))) /** * Applies Middleware based only if the condition effectful function evaluates @@ -166,15 +167,29 @@ trait Middleware[-R, +E, +AIn, -BIn, -AOut, +BOut] { self => */ final def whenZIO[R1 <: R, E1 >: E, AOut0 <: AOut]( cond: AOut0 => ZIO[R1, E1, Boolean], - ): Middleware[R1, E1, AIn, BIn, AOut0, BOut] = + )(implicit ev: IsMono[AIn, BIn, AOut0, BOut]): Middleware[R1, E1, AIn, BIn, AOut0, BOut] = { Middleware.ifThenElseZIO[AOut0](cond(_))( isTrue = _ => self, - isFalse = _ => Middleware.identity, + isFalse = _ => Middleware.identity[AIn, BIn, AOut, BOut], ) + } } object Middleware extends Web { + /** + * Creates a middleware which can allow or disallow access to an http based on + * the predicate + */ + def allow[A, B](cond: A => Boolean): Middleware[Any, Nothing, A, B, A, B] = + allowZIO(a => ZIO.succeed(cond(a))) + + /** + * Creates a middleware which can allow or disallow access to an http based on + * the predicate effect + */ + def allowZIO[A, B]: PartialAllowZIO[A, B] = new PartialAllowZIO[A, B](()) + /** * Creates a middleware using the specified encoder and decoder functions */ @@ -200,6 +215,11 @@ object Middleware extends Web { */ def collectZIO[A]: PartialCollectZIO[A] = new PartialCollectZIO[A](()) + /** + * Creates a middleware which returns an empty http value + */ + def empty: Middleware[Any, Nothing, Nothing, Any, Any, Nothing] = fromHttp(Http.empty) + /** * Creates a middleware which always fail with specified error */ @@ -218,13 +238,19 @@ object Middleware extends Web { } /** - * An empty middleware that doesn't do anything + * An empty middleware that doesn't do perform any operations on the provided + * Http and returns it as it is. */ - def identity: Middleware[Any, Nothing, Nothing, Any, Any, Nothing] = - new Middleware[Any, Nothing, Nothing, Any, Any, Nothing] { - override def apply[R1 <: Any, E1 >: Nothing](http: Http[R1, E1, Nothing, Any]): Http[R1, E1, Any, Nothing] = - http.asInstanceOf[Http[R1, E1, Any, Nothing]] - } + def identity[A, B]: MonoMiddleware[Any, Nothing, A, B] = Identity + + /** + * An empty middleware that doesn't do perform any operations on the provided + * Http and returns it as it is. + */ + def identity[AIn, BIn, AOut, BOut](implicit + ev: IsMono[AIn, BIn, AOut, BOut], + ): Middleware[Any, Nothing, AIn, BIn, AOut, BOut] = + Identity /** * Logical operator to decide which middleware to select based on the @@ -253,18 +279,59 @@ object Middleware extends Web { */ def succeed[B](b: B): Middleware[Any, Nothing, Nothing, Any, Any, B] = fromHttp(Http.succeed(b)) + /** + * Creates a new middleware using two transformation functions, one that's + * applied to the incoming type of the Http and one that applied to the + * outgoing type of the Http. + */ + def transform[AOut, BIn]: PartialMono[AOut, BIn] = new PartialMono[AOut, BIn]({}) + + /** + * Creates a new middleware using two transformation functions, one that's + * applied to the incoming type of the Http and one that applied to the + * outgoing type of the Http. + */ + def transformZIO[AOut, BIn]: PartialMonoZIO[AOut, BIn] = new PartialMonoZIO[AOut, BIn]({}) + + final class PartialAllowZIO[A, B](val unit: Unit) extends AnyVal { + def apply[R, E](cond: A => ZIO[R, E, Boolean]): MonoMiddleware[R, E, A, B] = + Middleware.ifThenElseZIO[A](cond(_))( + isTrue = _ => Middleware.identity[A, B], + isFalse = _ => Middleware.empty, + ) + } + + final class PartialMono[AOut, BIn](val unit: Unit) extends AnyVal { + def apply[AIn, BOut]( + in: AOut => AIn, + out: BIn => BOut, + ): Middleware[Any, Nothing, AIn, BIn, AOut, BOut] = + Middleware.transformZIO[AOut, BIn](a => ZIO.succeed(in(a)), b => ZIO.succeed(out(b))) + } + + final class PartialMonoZIO[AOut, BIn](val unit: Unit) extends AnyVal { + def apply[R, E, AIn, BOut]( + in: AOut => ZIO[R, E, AIn], + out: BIn => ZIO[R, E, BOut], + ): Middleware[R, E, AIn, BIn, AOut, BOut] = + new Middleware[R, E, AIn, BIn, AOut, BOut] { + override def apply[R1 <: R, E1 >: E](http: Http[R1, E1, AIn, BIn]): Http[R1, E1, AOut, BOut] = + http.contramapZIO(in).mapZIO(out) + } + } + 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 + Middleware.fromHttp(Http.collect[AOut] { case a if f.isDefinedAt(a) => f(a) }).flatten } 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 + Middleware.fromHttp(Http.collectZIO[AOut] { case a if f.isDefinedAt(a) => f(a) }).flatten } final class PartialIntercept[A, B](val unit: Unit) extends AnyVal { @@ -351,4 +418,9 @@ object Middleware extends Web { self(http).contramapZIO(a => f(a)) } } + + private object Identity extends Middleware[Any, Nothing, Nothing, Any, Any, Nothing] { + override def apply[R1 <: Any, E1 >: Nothing](http: Http[R1, E1, Nothing, Any]): Http[R1, E1, Any, Nothing] = + http.asInstanceOf[Http[R1, E1, Any, Nothing]] + } } diff --git a/zio-http/src/main/scala/zhttp/http/Path.scala b/zio-http/src/main/scala/zhttp/http/Path.scala new file mode 100644 index 000000000..442cff3dc --- /dev/null +++ b/zio-http/src/main/scala/zhttp/http/Path.scala @@ -0,0 +1,56 @@ +package zhttp.http + +final case class Path(segments: Vector[String], trailingSlash: Boolean) { self => + def /(segment: String): Path = copy(segments :+ segment) + + def /:(name: String): Path = copy(name +: segments) + + def drop(n: Int): Path = copy(segments.drop(n)) + + def dropLast(n: Int): Path = copy(segments.reverse.drop(n)) + + def encode: String = { + val ss = segments.filter(_.nonEmpty).mkString("/") + ss match { + case "" if trailingSlash => "/" + case "" if !trailingSlash => "" + case ss => "/" + ss + (if (trailingSlash) "/" else "") + } + } + + def initial: Path = copy(segments.init) + + def isEmpty: Boolean = segments.isEmpty && !trailingSlash + + def isEnd: Boolean = segments.isEmpty + + def last: Option[String] = segments.lastOption + + def nonEmpty: Boolean = !isEmpty + + def reverse: Path = copy(segments.reverse) + + def startsWith(other: Path): Boolean = segments.startsWith(other.segments) + + def take(n: Int): Path = copy(segments.take(n)) + + def toList: List[String] = segments.toList + + override def toString: String = encode +} + +object Path { + val empty: Path = Path(Vector.empty, false) + + /** + * Decodes a path string into a Path. Can fail if the path is invalid. + */ + def decode(path: String): Path = { + val segments = path.split("/").toVector.filter(_.nonEmpty) + segments.isEmpty match { + case true if path.endsWith("/") => Path(Vector.empty, true) + case true => Path(Vector.empty, false) + case _ => Path(segments, path.endsWith("/")) + } + } +} diff --git a/zio-http/src/main/scala/zhttp/http/PathModule.scala b/zio-http/src/main/scala/zhttp/http/PathModule.scala deleted file mode 100644 index 9b68aaac4..000000000 --- a/zio-http/src/main/scala/zhttp/http/PathModule.scala +++ /dev/null @@ -1,101 +0,0 @@ -package zhttp.http - -import scala.annotation.tailrec - -private[zhttp] trait PathModule { module => - val !! : Path = Path.End - - sealed trait Path { self => - final override def toString: String = this.encode - - final def /(name: String): Path = Path(self.toList :+ name) - - final def /:(name: String): Path = append(name) - - final def append(name: String): Path = if (name.isEmpty) self else Path.Cons(name, self) - - final def drop(n: Int): Path = Path(self.toList.drop(n)) - - final def dropLast(n: Int): Path = Path(self.toList.reverse.drop(n).reverse) - - final def encode: String = { - @tailrec - def loop(self: Path, str: String): String = { - self match { - case Path.End => str - case Path.Cons(name, path) => loop(path, s"$str/$name") - } - } - val res = loop(self, "") - if (res.isEmpty) "/" else res - } - - final def initial: Path = self match { - case Path.End => self - case Path.Cons(_, path) => path - } - - final def isEnd: Boolean = self match { - case Path.End => true - case Path.Cons(_, _) => false - } - - final def last: Option[String] = self match { - case Path.End => None - case Path.Cons(name, _) => Option(name) - } - - final def reverse: Path = Path(toList.reverse) - - @tailrec - final def startsWith(other: Path): Boolean = { - if (self == other) true - else - (self, other) match { - case (/(p1, _), p2) => p1.startsWith(p2) - case _ => false - - } - } - - final def take(n: Int): Path = Path(self.toList.take(n)) - - def toList: List[String] - } - - object Path { - def apply(): Path = End - def apply(string: String): Path = if (string.trim.isEmpty) End else Path(string.split("/").toList) - def apply(seqString: String*): Path = Path(seqString.toList) - def apply(list: List[String]): Path = list.foldRight[Path](End)((s, a) => a.append(s)) - - def empty: Path = End - - def unapplySeq(arg: Path): Option[List[String]] = Option(arg.toList) - - case class Cons(name: String, path: Path) extends Path { - override def toList: List[String] = name :: path.toList - } - - case object End extends Path { - override def toList: List[String] = Nil - } - } - - object /: { - def unapply(path: Path): Option[(String, Path)] = - path match { - case Path.End => None - case Path.Cons(name, path) => Option(name -> path) - } - } - - object / { - def unapply(path: Path): Option[(Path, String)] = { - path.toList.reverse match { - case Nil => None - case head :: next => Option((Path(next.reverse), head)) - } - } - } -} diff --git a/zio-http/src/main/scala/zhttp/http/PathSyntax.scala b/zio-http/src/main/scala/zhttp/http/PathSyntax.scala new file mode 100644 index 000000000..4f3cc3812 --- /dev/null +++ b/zio-http/src/main/scala/zhttp/http/PathSyntax.scala @@ -0,0 +1,28 @@ +package zhttp.http + +private[zhttp] trait PathSyntax { module => + val !! : Path = Path.empty + + object /: { + def unapply(path: Path): Option[(String, Path)] = { + for { + head <- path.segments.headOption + tail = path.segments.drop(1) + } yield (head, path.copy(segments = tail)) + } + } + + object / { + def unapply(path: Path): Option[(Path, String)] = { + if (path.segments.length == 1) { + Some(!! -> path.segments.last) + } else if (path.segments.length >= 2) { + val init = path.segments.init + val last = path.segments.last + Some(path.copy(segments = init) -> last) + } else { + None + } + } + } +} diff --git a/zio-http/src/main/scala/zhttp/http/Request.scala b/zio-http/src/main/scala/zhttp/http/Request.scala index 7ec1a4cac..5c793c7ed 100644 --- a/zio-http/src/main/scala/zhttp/http/Request.scala +++ b/zio-http/src/main/scala/zhttp/http/Request.scala @@ -1,16 +1,22 @@ package zhttp.http +import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.{DefaultFullHttpRequest, HttpRequest} import zhttp.http.headers.HeaderExtension -import java.net.InetAddress +import java.io.IOException trait Request extends HeaderExtension[Request] with HttpDataExtension[Request] { self => /** - * Updates the headers using the provided function + * Accesses the channel's context for more low level control */ - final override def updateHeaders(update: Headers => Headers): Request = self.copy(headers = update(self.headers)) + private[zhttp] def unsafeContext: ChannelHandlerContext + + /** + * Gets the HttpRequest + */ + private[zhttp] def unsafeEncode: HttpRequest def copy( version: Version = self.version, @@ -23,13 +29,13 @@ trait Request extends HeaderExtension[Request] with HttpDataExtension[Request] { val h = headers val v = version new Request { - override def method: Method = m - override def url: URL = u - override def headers: Headers = h - override def version: Version = v - override def unsafeEncode: HttpRequest = self.unsafeEncode - override def remoteAddress: Option[InetAddress] = self.remoteAddress - override def data: HttpData = self.data + override def method: Method = m + override def url: URL = u + override def headers: Headers = h + override def version: Version = v + override def unsafeEncode: HttpRequest = self.unsafeEncode + override def data: HttpData = self.data + override def unsafeContext: ChannelHandlerContext = self.unsafeContext } } @@ -59,47 +65,41 @@ trait Request extends HeaderExtension[Request] with HttpDataExtension[Request] { def path: Path = url.path /** - * Gets the remote address if available + * Gets the complete url */ - def remoteAddress: Option[InetAddress] + def url: URL + + /** + * Gets the request's http protocol version + */ + def version: Version /** * Overwrites the method in the request */ - def setMethod(method: Method): Request = self.copy(method = method) + final def setMethod(method: Method): Request = self.copy(method = method) /** * Overwrites the path in the request */ - def setPath(path: Path): Request = self.copy(url = self.url.copy(path = path)) + final def setPath(path: Path): Request = self.copy(url = self.url.copy(path = path)) /** * Overwrites the url in the request */ - def setUrl(url: URL): Request = self.copy(url = url) + final def setUrl(url: URL): Request = self.copy(url = url) /** * Returns a string representation of the request, useful for debugging, * logging or other purposes. It contains the essential properties of HTTP * request: protocol version, method, URL, headers and remote address. */ - override def toString = - s"Request($version, $method, $url, $headers, $remoteAddress)" - - /** - * Gets the HttpRequest - */ - private[zhttp] def unsafeEncode: HttpRequest - - /** - * Gets the complete url - */ - def url: URL + final override def toString = s"Request($version, $method, $url, $headers)" /** - * Gets the request's http protocol version + * Updates the headers using the provided function */ - def version: Version + final override def updateHeaders(update: Headers => Headers): Request = self.copy(headers = update(self.headers)) } @@ -113,48 +113,27 @@ object Request { method: Method = Method.GET, url: URL = URL.root, headers: Headers = Headers.empty, - remoteAddress: Option[InetAddress] = None, data: HttpData = HttpData.Empty, ): Request = { - val m = method - val u = url - val h = headers - val ra = remoteAddress - val d = data - val v = version + val m = method + val u = url + val h = headers + val d = data + val v = version new Request { - override def method: Method = m - override def url: URL = u - override def headers: Headers = h - override def version: Version = v - override def unsafeEncode: HttpRequest = { + override def method: Method = m + override def url: URL = u + override def headers: Headers = h + override def version: Version = v + override def unsafeEncode: HttpRequest = { val jVersion = v.toJava val path = url.relative.encode new DefaultFullHttpRequest(jVersion, method.toJava, path) } - override def remoteAddress: Option[InetAddress] = ra - override def data: HttpData = d + override def data: HttpData = d + override def unsafeContext: ChannelHandlerContext = throw new IOException("Request does not have a context") } } - - /** - * Lift request to TypedRequest with option to extract params - */ - final class ParameterizedRequest[A](req: Request, val params: A) extends Request { - override def headers: Headers = req.headers - override def method: Method = req.method - override def remoteAddress: Option[InetAddress] = req.remoteAddress - override def url: URL = req.url - override def version: Version = req.version - override def unsafeEncode: HttpRequest = req.unsafeEncode - override def data: HttpData = req.data - override def toString: String = - s"ParameterizedRequest($req, $params)" - } - - object ParameterizedRequest { - def apply[A](req: Request, params: A): ParameterizedRequest[A] = new ParameterizedRequest(req, params) - } } diff --git a/zio-http/src/main/scala/zhttp/http/Response.scala b/zio-http/src/main/scala/zhttp/http/Response.scala index f82335e9f..2a8ed4001 100644 --- a/zio-http/src/main/scala/zhttp/http/Response.scala +++ b/zio-http/src/main/scala/zhttp/http/Response.scala @@ -1,14 +1,16 @@ package zhttp.http import io.netty.buffer.Unpooled +import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.HttpVersion.HTTP_1_1 import io.netty.handler.codec.http.{FullHttpResponse, HttpHeaderNames, HttpResponse} import zhttp.html._ import zhttp.http.headers.HeaderExtension +import zhttp.service.ChannelFuture import zhttp.socket.{IsWebSocket, Socket, SocketApp} -import zio.{UIO, ZIO} +import zio.{Task, UIO, ZIO} -import java.io.{PrintWriter, StringWriter} +import java.io.{IOException, PrintWriter, StringWriter} final case class Response private ( status: Status, @@ -18,6 +20,11 @@ final case class Response private ( ) extends HeaderExtension[Response] with HttpDataExtension[Response] { self => + private[zhttp] def close: Task[Unit] = self.attribute.channel match { + case Some(channel) => ChannelFuture.unit(channel.close()) + case None => ZIO.fail(new IOException("Channel context isn't available")) + } + /** * Encodes the Response into a Netty HttpResponse. Sets default headers such * as `content-length`. For performance reasons, it is possible that it uses a @@ -107,11 +114,11 @@ final case class Response private ( } object Response { - private[zhttp] def unsafeFromJResponse(jRes: FullHttpResponse): Response = { + private[zhttp] def unsafeFromJResponse(ctx: ChannelHandlerContext, jRes: FullHttpResponse): Response = { val status = Status.fromHttpResponseStatus(jRes.status()) val headers = Headers.decode(jRes.headers()) val data = HttpData.fromByteBuf(Unpooled.copiedBuffer(jRes.content())) - Response(status, headers, data) + Response(status, headers, data, attribute = Attribute(channel = Some(ctx))) } def apply[R, E]( @@ -235,6 +242,7 @@ object Response { memoize: Boolean = false, serverTime: Boolean = false, encoded: Option[(Response, HttpResponse)] = None, + channel: Option[ChannelHandlerContext] = None, ) { self => def withEncodedResponse(jResponse: HttpResponse, response: Response): Attribute = self.copy(encoded = Some(response -> jResponse)) diff --git a/zio-http/src/main/scala/zhttp/http/Scheme.scala b/zio-http/src/main/scala/zhttp/http/Scheme.scala index ad0a77392..3b22c268e 100644 --- a/zio-http/src/main/scala/zhttp/http/Scheme.scala +++ b/zio-http/src/main/scala/zhttp/http/Scheme.scala @@ -40,12 +40,23 @@ sealed trait Scheme { self => } object Scheme { - def decode(scheme: String): Option[Scheme] = scheme.toUpperCase match { - case "HTTPS" => Option(HTTPS) - case "HTTP" => Option(HTTP) - case "WS" => Option(WS) - case "WSS" => Option(WSS) - case _ => None + /** + * Decodes a string to an Option of Scheme. Returns None in case of + * null/non-valid Scheme + */ + def decode(scheme: String): Option[Scheme] = + Option(unsafeDecode(scheme)) + + private[zhttp] def unsafeDecode(scheme: String): Scheme = { + if (scheme == null) null + else + scheme.length match { + case 5 => HTTPS + case 4 => HTTP + case 3 => WSS + case 2 => WS + case _ => null + } } def fromJScheme(scheme: HttpScheme): Option[Scheme] = scheme match { diff --git a/zio-http/src/main/scala/zhttp/http/URL.scala b/zio-http/src/main/scala/zhttp/http/URL.scala index 93a1c9096..001610bdf 100644 --- a/zio-http/src/main/scala/zhttp/http/URL.scala +++ b/zio-http/src/main/scala/zhttp/http/URL.scala @@ -50,7 +50,7 @@ final case class URL( def setPath(path: Path): URL = copy(path = path) - def setPath(path: String): URL = copy(path = Path(path)) + def setPath(path: String): URL = copy(path = Path.decode(path)) def setPort(port: Int): URL = { val location = kind match { @@ -76,6 +76,13 @@ final case class URL( copy(kind = location) } + def isEqual(other: URL): Boolean = { + self.kind == other.kind && + self.path == other.path && + (self.queryParams.toSet diff other.queryParams.toSet).isEmpty + self.fragment == other.fragment + } + private[zhttp] def relative: URL = self.kind match { case URL.Location.Relative => self case _ => self.copy(kind = URL.Location.Relative) @@ -126,12 +133,12 @@ object URL { path <- Option(uri.getRawPath) port = Option(uri.getPort).filter(_ != -1).getOrElse(portFromScheme(scheme)) connection = URL.Location.Absolute(scheme, host, port) - } yield URL(Path(path), connection, queryParams(uri.getRawQuery), Fragment.fromURI(uri)) + } yield URL(Path.decode(path), connection, queryParams(uri.getRawQuery), Fragment.fromURI(uri)) } private def fromRelativeURI(uri: URI): Option[URL] = for { path <- Option(uri.getRawPath) - } yield URL(Path(path), Location.Relative, queryParams(uri.getRawQuery), Fragment.fromURI(uri)) + } yield URL(Path.decode(path), Location.Relative, queryParams(uri.getRawQuery), Fragment.fromURI(uri)) private def portFromScheme(scheme: Scheme): Int = scheme match { case Scheme.HTTP | Scheme.WS => 80 @@ -164,4 +171,5 @@ object URL { decoded <- Option(uri.getFragment) } yield Fragment(raw, decoded) } + } diff --git a/zio-http/src/main/scala/zhttp/http/headers/HeaderGetters.scala b/zio-http/src/main/scala/zhttp/http/headers/HeaderGetters.scala index 84418b00c..5b8dabf2b 100644 --- a/zio-http/src/main/scala/zhttp/http/headers/HeaderGetters.scala +++ b/zio-http/src/main/scala/zhttp/http/headers/HeaderGetters.scala @@ -163,10 +163,7 @@ trait HeaderGetters[+A] { self => final def cookiesDecoded: List[Cookie] = headerValues(HeaderNames.cookie).flatMap { header => - Cookie.decodeRequestCookie(header) match { - case None => Nil - case Some(list) => list - } + Cookie.decodeRequestCookie(header) } final def date: Option[CharSequence] = diff --git a/zio-http/src/main/scala/zhttp/http/middleware/Csrf.scala b/zio-http/src/main/scala/zhttp/http/middleware/Csrf.scala index b94be961d..6fd4785df 100644 --- a/zio-http/src/main/scala/zhttp/http/middleware/Csrf.scala +++ b/zio-http/src/main/scala/zhttp/http/middleware/Csrf.scala @@ -1,7 +1,7 @@ package zhttp.http.middleware import zhttp.http._ -import zio.ZIO +import zio._ import java.util.UUID diff --git a/zio-http/src/main/scala/zhttp/http/middleware/Web.scala b/zio-http/src/main/scala/zhttp/http/middleware/Web.scala index f54b0f7bf..b8e3effef 100644 --- a/zio-http/src/main/scala/zhttp/http/middleware/Web.scala +++ b/zio-http/src/main/scala/zhttp/http/middleware/Web.scala @@ -31,7 +31,7 @@ private[zhttp] trait Web extends Cors with Csrf with Auth with HeaderModifier[Ht /** * Add log status, method, url and time taken from req to res */ - final def debug: HttpMiddleware[Any, IOException] = + 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 { @@ -134,8 +134,10 @@ private[zhttp] trait Web extends Cors with Csrf with Auth with HeaderModifier[Ht /** * Times out the application with a 408 status code. */ - final def timeout(duration: Duration): HttpMiddleware[Any, Nothing] = - Middleware.identity.race(Middleware.fromHttp(Http.status(Status.RequestTimeout).delayAfter(duration))) + final def timeout(duration: Duration): HttpMiddleware[Clock, Nothing] = + Middleware + .identity[Request, Response] + .race(Middleware.fromHttp(Http.status(Status.RequestTimeout).delayAfter(duration))) /** * Creates a middleware that updates the response produced @@ -147,15 +149,13 @@ private[zhttp] trait Web extends Cors with Csrf with Auth with HeaderModifier[Ht * 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.headers)) + middleware.when(req => cond(req.headers)) /** * 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) + final def whenRequest[R, E](cond: Request => Boolean)(middleware: HttpMiddleware[R, E]): HttpMiddleware[R, E] = + middleware.when(cond) /** * Applies the middleware only if the condition function effectfully evaluates diff --git a/zio-http/src/main/scala/zhttp/http/middleware/package.scala b/zio-http/src/main/scala/zhttp/http/middleware/package.scala index 3baa3d9fc..6307541bd 100644 --- a/zio-http/src/main/scala/zhttp/http/middleware/package.scala +++ b/zio-http/src/main/scala/zhttp/http/middleware/package.scala @@ -1,5 +1,6 @@ package zhttp.http package object middleware { - type HttpMiddleware[-R, +E] = Middleware[R, E, Request, Response, Request, Response] + type HttpMiddleware[-R, +E] = MonoMiddleware[R, E, Request, Response] + type MonoMiddleware[-R, +E, A, B] = Middleware[R, E, A, B, A, B] } diff --git a/zio-http/src/main/scala/zhttp/http/package.scala b/zio-http/src/main/scala/zhttp/http/package.scala index 53d84e27c..70c07da1c 100644 --- a/zio-http/src/main/scala/zhttp/http/package.scala +++ b/zio-http/src/main/scala/zhttp/http/package.scala @@ -1,11 +1,11 @@ package zhttp import io.netty.util.CharsetUtil -import zio.ZIO +import zio.{Chunk, Queue, Trace, UIO, ZIO} import java.nio.charset.Charset -package object http extends PathModule with RequestSyntax with RouteDecoderModule { +package object http extends PathSyntax with RequestSyntax with RouteDecoderModule { type HttpApp[-R, +E] = Http[R, E, Request, Response] type UHttpApp = HttpApp[Any, Nothing] type RHttpApp[-R] = HttpApp[R, Throwable] @@ -21,4 +21,32 @@ package object http extends PathModule with RequestSyntax with RouteDecoderModul object HeaderNames extends headers.HeaderNames object HeaderValues extends headers.HeaderValues + + implicit class QueueWrapper[A](queue: Queue[A]) { + def mapM[B](f: A => UIO[B]): Queue[B] = { + new Queue[B] { self => + override def awaitShutdown(implicit trace: Trace): UIO[Unit] = queue.awaitShutdown + + override def capacity: Int = queue.capacity + + override def isShutdown(implicit trace: Trace): UIO[Boolean] = queue.isShutdown + + override def offer(b: B)(implicit trace: Trace): UIO[Boolean] = ZIO.succeed(b).flatMap(b => self.offer(b)) + + override def offerAll(as: Iterable[B])(implicit trace: Trace): UIO[Boolean] = + ZIO.foreach(as)(b => ZIO.succeed(b)).flatMap(t => self.offerAll(t)) + + override def shutdown(implicit trace: Trace): UIO[Unit] = queue.shutdown + + override def size(implicit trace: Trace): UIO[Int] = queue.size + + override def take(implicit trace: Trace): UIO[B] = queue.take.flatMap(a => f(a)) + + override def takeAll(implicit trace: Trace): UIO[Chunk[B]] = queue.takeAll.flatMap(ZIO.foreach(_)(f)) + + override def takeUpTo(max: Int)(implicit trace: Trace): UIO[Chunk[B]] = + queue.takeUpTo(max).flatMap(ZIO.foreach(_)(f)) + } + } + } } diff --git a/zio-http/src/main/scala/zhttp/service/Client.scala b/zio-http/src/main/scala/zhttp/service/Client.scala index bd6a09e5d..f4c9020a1 100644 --- a/zio-http/src/main/scala/zhttp/service/Client.scala +++ b/zio-http/src/main/scala/zhttp/service/Client.scala @@ -16,7 +16,7 @@ import zhttp.service.Client.Config import zhttp.service.client.ClientSSLHandler.ClientSSLOptions import zhttp.service.client.{ClientInboundHandler, ClientSSLHandler} import zhttp.socket.{Socket, SocketApp} -import zio.{Promise, Task, ZIO} +import zio.{Promise, Scope, Task, ZIO} import java.net.{InetSocketAddress, URI} @@ -38,7 +38,7 @@ final case class Client[R](rtm: HttpRuntime[R], cf: JChannelFactory[Channel], el headers: Headers = Headers.empty, socketApp: SocketApp[R], sslOptions: ClientSSLOptions = ClientSSLOptions.DefaultSSL, - ): ZIO[R, Throwable, Response] = for { + ): ZIO[R with Scope, Throwable, Response] = for { env <- ZIO.environment[R] res <- request( Request( @@ -48,7 +48,7 @@ final case class Client[R](rtm: HttpRuntime[R], cf: JChannelFactory[Channel], el headers, ), clientConfig = Client.Config(socketApp = Some(socketApp.provideEnvironment(env)), ssl = Some(sslOptions)), - ) + ).withFinalizer(_.close.orDie) } yield res /** @@ -159,7 +159,7 @@ object Client { app: SocketApp[R], headers: Headers = Headers.empty, sslOptions: ClientSSLOptions = ClientSSLOptions.DefaultSSL, - ): ZIO[R with EventLoopGroup with ChannelFactory, Throwable, Response] = { + ): ZIO[R with EventLoopGroup with ChannelFactory with Scope, Throwable, Response] = { for { clt <- make[R] uri <- ZIO.fromEither(URL.fromString(url)) diff --git a/zio-http/src/main/scala/zhttp/service/Handler.scala b/zio-http/src/main/scala/zhttp/service/Handler.scala index cebe09a8a..e9de662b1 100644 --- a/zio-http/src/main/scala/zhttp/service/Handler.scala +++ b/zio-http/src/main/scala/zhttp/service/Handler.scala @@ -4,24 +4,22 @@ import io.netty.channel.ChannelHandler.Sharable import io.netty.channel.{ChannelHandlerContext, SimpleChannelInboundHandler} import io.netty.handler.codec.http._ import zhttp.http._ -import zhttp.service.server.content.handlers.ServerResponseHandler -import zhttp.service.server.{ServerTime, WebSocketUpgrade} -import zio.ZIO - -import java.net.{InetAddress, InetSocketAddress} +import zhttp.logging.Logger +import zhttp.service.Handler.log +import zhttp.service.server.WebSocketUpgrade +import zio._ @Sharable private[zhttp] final case class Handler[R]( app: HttpApp[R, Throwable], runtime: HttpRuntime[R], config: Server.Config[R, Throwable], - serverTimeGenerator: ServerTime, + resWriter: ServerResponseWriter[R], ) extends SimpleChannelInboundHandler[HttpObject](false) - with WebSocketUpgrade[R] - with ServerResponseHandler[R] { self => + with WebSocketUpgrade[R] { self => override def channelRead0(ctx: Ctx, msg: HttpObject): Unit = { - + log.debug(s"Message: ${msg.getClass.getSimpleName}") implicit val iCtx: ChannelHandlerContext = ctx msg match { case jReq: FullHttpRequest => @@ -37,75 +35,55 @@ private[zhttp] final case class Handler[R]( override def headers: Headers = Headers.make(jReq.headers()) - override def remoteAddress: Option[InetAddress] = { - ctx.channel().remoteAddress() match { - case m: InetSocketAddress => Some(m.getAddress) - case _ => None - } - } + override def data: HttpData = HttpData.fromByteBuf(jReq.content()) - override def data: HttpData = HttpData.fromByteBuf(jReq.content()) override def version: Version = Version.unsafeFromJava(jReq.protocolVersion()) - /** - * Gets the HttpRequest - */ - override def unsafeEncode = jReq + override def unsafeEncode: HttpRequest = jReq + + override def unsafeContext: Ctx = ctx }, ) catch { - case throwable: Throwable => - writeResponse( - Response - .fromHttpError(HttpError.InternalServerError(cause = Some(throwable))) - .withConnection(HeaderValues.close), - jReq, - ): Unit + case throwable: Throwable => resWriter.write(throwable, jReq) } case jReq: HttpRequest => - if (canHaveBody(jReq)) { - ctx.channel().config().setAutoRead(false): Unit - } + val hasBody = canHaveBody(jReq) + log.debug(s"HasBody: ${hasBody}") + if (hasBody) ctx.channel().config().setAutoRead(false): Unit try unsafeRun( jReq, app, new Request { - override def data: HttpData = HttpData.UnsafeAsync(callback => - ctx - .pipeline() - .addAfter(HTTP_REQUEST_HANDLER, HTTP_CONTENT_HANDLER, new RequestBodyHandler(callback)): Unit, - ) + override def data: HttpData = if (hasBody) asyncData else HttpData.empty + private final def asyncData = + HttpData.UnsafeAsync(callback => + ctx + .pipeline() + .addAfter( + HTTP_REQUEST_HANDLER, + HTTP_CONTENT_HANDLER, + new RequestBodyHandler(callback(ctx)), + ): Unit, + ) override def headers: Headers = Headers.make(jReq.headers()) override def method: Method = Method.fromHttpMethod(jReq.method()) - override def remoteAddress: Option[InetAddress] = { - ctx.channel().remoteAddress() match { - case m: InetSocketAddress => Some(m.getAddress) - case _ => None - } - } + override def url: URL = URL.fromString(jReq.uri()).getOrElse(null) - override def url: URL = URL.fromString(jReq.uri()).getOrElse(null) override def version: Version = Version.unsafeFromJava(jReq.protocolVersion()) - /** - * Gets the HttpRequest - */ - override def unsafeEncode = jReq + override def unsafeEncode: HttpRequest = jReq + + override def unsafeContext: Ctx = ctx }, ) catch { - case throwable: Throwable => - writeResponse( - Response - .fromHttpError(HttpError.InternalServerError(cause = Some(throwable))) - .withConnection(HeaderValues.close), - jReq, - ): Unit + case throwable: Throwable => resWriter.write(throwable, jReq) } case msg: HttpContent => @@ -118,9 +96,10 @@ private[zhttp] final case class Handler[R]( } - private def canHaveBody(req: HttpRequest): Boolean = req.method() match { - case HttpMethod.GET | HttpMethod.HEAD | HttpMethod.OPTIONS | HttpMethod.TRACE => false - case _ => true + private def canHaveBody(req: HttpRequest): Boolean = { + req.method() == HttpMethod.TRACE || + req.headers().contains(HttpHeaderNames.CONTENT_LENGTH) || + req.headers().contains(HttpHeaderNames.TRANSFER_ENCODING) } /** @@ -138,25 +117,13 @@ private[zhttp] final case class Handler[R]( cause => cause.failureOrCause match { case Left(Some(cause)) => - ZIO.succeed { - writeResponse( - Response.fromHttpError(HttpError.InternalServerError(cause = Some(cause))), - jReq, - ) - } + ZIO.succeed { resWriter.write(cause, jReq) } case Left(None) => - ZIO.succeed { - writeResponse(Response.status(Status.NotFound), jReq) - } + ZIO.succeed { resWriter.writeNotFound(jReq) } case Right(other) => other.dieOption match { case Some(defect) => - ZIO.succeed { - writeResponse( - Response.fromHttpError(HttpError.InternalServerError(cause = Some(defect))), - jReq, - ) - } + ZIO.succeed { resWriter.write(defect, jReq) } case None => ZIO.failCause(other) } @@ -166,7 +133,7 @@ private[zhttp] final case class Handler[R]( else { for { _ <- ZIO.attempt { - writeResponse(res, jReq) + resWriter.write(res, jReq) } } yield () }, @@ -177,17 +144,14 @@ private[zhttp] final case class Handler[R]( if (self.isWebSocket(res)) { self.upgradeToWebSocket(jReq, res) } else { - writeResponse(res, jReq): Unit + resWriter.write(res, jReq) } - case HExit.Failure(e) => - writeResponse(Response.fromHttpError(HttpError.InternalServerError(cause = Some(e))), jReq): Unit + case HExit.Failure(e) => resWriter.write(e, jReq) - case HExit.Die(e) => - writeResponse(Response.fromHttpError(HttpError.InternalServerError(cause = Some(e))), jReq): Unit + case HExit.Die(e) => resWriter.write(e, jReq) - case HExit.Empty => - writeResponse(Response.fromHttpError(HttpError.NotFound(Path(jReq.uri()))), jReq): Unit + case HExit.Empty => resWriter.writeNotFound(jReq) } } @@ -196,15 +160,16 @@ private[zhttp] final case class Handler[R]( * Executes program */ private def unsafeRunZIO(program: ZIO[R, Throwable, Any])(implicit ctx: Ctx): Unit = - rt.unsafeRun(ctx) { + runtime.unsafeRun(ctx) { program } - override def serverTime: ServerTime = serverTimeGenerator - - override val rt: HttpRuntime[R] = runtime - override def exceptionCaught(ctx: Ctx, cause: Throwable): Unit = { config.error.fold(super.exceptionCaught(ctx, cause))(f => runtime.unsafeRun(ctx)(f(cause))) } + +} + +object Handler { + val log: Logger = Log.withTags("Server", "Request") } diff --git a/zio-http/src/main/scala/zhttp/service/Logging.scala b/zio-http/src/main/scala/zhttp/service/Logging.scala new file mode 100644 index 000000000..e9269307e --- /dev/null +++ b/zio-http/src/main/scala/zhttp/service/Logging.scala @@ -0,0 +1,27 @@ +package zhttp.service + +import zhttp.logging.Logger + +/** + * Base trait to configure logging. Feel free to edit this file as per your + * requirements to slice and dice internal logging. + */ +trait Logging { + + /** + * Controls if you want to pipe netty logs into the zhttp logger. + */ + val EnableNettyLogging: Boolean = false + + /** + * Name of the property that is used to read the log level from system + * properties. + */ + private val PropName = "ZHttpLogLevel" + + /** + * Global Logging instance used to add log statements everywhere in the + * application. + */ + private[zhttp] val Log: Logger = Logger.console.detectLevelFromProps(PropName) +} diff --git a/zio-http/src/main/scala/zhttp/service/RequestBodyHandler.scala b/zio-http/src/main/scala/zhttp/service/RequestBodyHandler.scala index 27d395a71..06c0c1d29 100644 --- a/zio-http/src/main/scala/zhttp/service/RequestBodyHandler.scala +++ b/zio-http/src/main/scala/zhttp/service/RequestBodyHandler.scala @@ -1,22 +1,18 @@ package zhttp.service import io.netty.channel.{ChannelHandlerContext, SimpleChannelInboundHandler} import io.netty.handler.codec.http.{HttpContent, LastHttpContent} -import zhttp.http.HttpData.{UnsafeChannel, UnsafeContent} -final class RequestBodyHandler(val callback: UnsafeChannel => UnsafeContent => Unit) +final class RequestBodyHandler(val callback: HttpContent => Any) extends SimpleChannelInboundHandler[HttpContent](false) { self => - private var onMessage: UnsafeContent => Unit = _ - override def channelRead0(ctx: ChannelHandlerContext, msg: HttpContent): Unit = { - self.onMessage(new UnsafeContent(msg)) + self.callback(msg) if (msg.isInstanceOf[LastHttpContent]) { ctx.channel().pipeline().remove(self): Unit } } override def handlerAdded(ctx: ChannelHandlerContext): Unit = { - self.onMessage = callback(new UnsafeChannel(ctx)) ctx.read(): Unit } } diff --git a/zio-http/src/main/scala/zhttp/service/Server.scala b/zio-http/src/main/scala/zhttp/service/Server.scala index 68a9633f0..07494c396 100644 --- a/zio-http/src/main/scala/zhttp/service/Server.scala +++ b/zio-http/src/main/scala/zhttp/service/Server.scala @@ -15,9 +15,6 @@ sealed trait Server[-R, +E] { self => import Server._ - def ++[R1 <: R, E1 >: E](other: Server[R1, E1]): Server[R1, E1] = - 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) @@ -32,8 +29,12 @@ sealed trait Server[-R, +E] { self => case UnsafeChannelPipeline(init) => s.copy(channelInitializer = init) case RequestDecompression(enabled, strict) => s.copy(requestDecompression = (enabled, strict)) case ObjectAggregator(maxRequestSize) => s.copy(objectAggregator = maxRequestSize) + case UnsafeServerBootstrap(init) => s.copy(serverBootstrapInitializer = init) } + def ++[R1 <: R, E1 >: E](other: Server[R1, E1]): Server[R1, E1] = + Concat(self, other) + def make(implicit ev: E <:< Throwable, ): ZIO[R with EventLoopGroup with ServerChannelFactory with Scope, Throwable, Start] = @@ -50,9 +51,10 @@ sealed trait Server[-R, +E] { self => start.provideSomeLayer[R1](EventLoopGroup.auto(0) ++ ServerChannelFactory.auto) /** - * Creates a new server listening on the provided port. + * Creates a new server using a HttpServerExpectContinueHandler to send a 100 + * HttpResponse if necessary. */ - def withPort(port: Int): Server[R, E] = Concat(self, Server.Address(new InetSocketAddress(port))) + def withAcceptContinue(enable: Boolean): Server[R, E] = Concat(self, Server.AcceptContinue(enable)) /** * Creates a new server listening on the provided hostname and port. @@ -72,21 +74,17 @@ sealed trait Server[-R, +E] { self => 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. + * Creates a new server with FlushConsolidationHandler to control the flush + * operations in a more efficient way if enabled (@see FlushConsolidationHandler). */ - def withSsl(sslOptions: ServerSSLOptions): Server[R, E] = Concat(self, Server.Ssl(sslOptions)) + def withConsolidateFlush(enable: Boolean): Server[R, E] = Concat(self, ConsolidateFlush(enable)) /** - * Creates a new server using a HttpServerExpectContinueHandler to send a 100 - * HttpResponse if necessary. + * Creates a new server with the errorHandler provided. */ - def withAcceptContinue(enable: Boolean): Server[R, E] = Concat(self, Server.AcceptContinue(enable)) + def withError[R1](errorHandler: Throwable => ZIO[R1, Nothing, Unit]): Server[R with R1, E] = + Concat(self, Server.Error(errorHandler)) /** * Creates a new server using netty FlowControlHandler if enable (@see */ def withFlowControl(enable: Boolean): Server[R, E] = Concat(self, Server.FlowControl(enable)) + /** + * 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 the leak detection level provided (@see ResourceLeakDetector.Level). @@ -101,18 +106,29 @@ sealed trait Server[-R, +E] { self => 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). + * Creates a new server with HttpObjectAggregator with the specified max size + * of the aggregated content. */ - def withKeepAlive(enable: Boolean): Server[R, E] = Concat(self, KeepAlive(enable)) + def withObjectAggregator(maxRequestSize: Int = Int.MaxValue): Server[R, E] = + Concat(self, ObjectAggregator(maxRequestSize)) /** - * Creates a new server with FlushConsolidationHandler to control the flush - * operations in a more efficient way if enabled (@see FlushConsolidationHandler). + * Creates a new server listening on the provided port. */ - def withConsolidateFlush(enable: Boolean): Server[R, E] = Concat(self, ConsolidateFlush(enable)) + def withPort(port: Int): Server[R, E] = Concat(self, Server.Address(new InetSocketAddress(port))) + + /** + * Creates a new server with netty's HttpContentDecompressor to decompress + * Http requests (@see HttpContentDecompressor). + */ + def withRequestDecompression(enabled: Boolean, strict: Boolean): Server[R, E] = + Concat(self, RequestDecompression(enabled, strict)) + + /** + * 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 by passing a function that modifies the channel @@ -126,84 +142,68 @@ sealed trait Server[-R, +E] { self => Concat(self, UnsafeChannelPipeline(unsafePipeline)) /** - * Creates a new server with netty's HttpContentDecompressor to decompress - * Http requests (@see HttpContentDecompressor). + * Provides unsafe access to netty's ServerBootstrap. Modifying server + * bootstrap is generally not advised unless you know what you are doing. */ - def withRequestDecompression(enabled: Boolean, strict: Boolean): Server[R, E] = - Concat(self, RequestDecompression(enabled, strict)) - - /** - * Creates a new server with HttpObjectAggregator with the specified max size - * of the aggregated content. - */ - def withObjectAggregator(maxRequestSize: Int = Int.MaxValue): Server[R, E] = - Concat(self, ObjectAggregator(maxRequestSize)) + def withUnsafeServerBootstrap(unsafeServerbootstrap: ServerBootstrap => Unit): Server[R, E] = + Concat(self, UnsafeServerBootstrap(unsafeServerbootstrap)) } - object Server { - private[zhttp] final case class Config[-R, +E]( - leakDetectionLevel: LeakDetectionLevel = LeakDetectionLevel.SIMPLE, - error: Option[Throwable => ZIO[R, Nothing, Unit]] = None, - sslOption: ServerSSLOptions = null, + 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 disableKeepAlive: UServer = Server.KeepAlive(false) + val consolidateFlush: UServer = ConsolidateFlush(true) - // TODO: move app out of settings - app: HttpApp[R, E] = Http.empty, - address: InetSocketAddress = new InetSocketAddress(8080), - acceptContinue: Boolean = false, - keepAlive: Boolean = true, - consolidateFlush: Boolean = false, - flowControl: Boolean = true, - channelInitializer: ChannelPipeline => Unit = null, - requestDecompression: (Boolean, Boolean) = (false, false), - objectAggregator: Int = -1, - ) { - def useAggregator: Boolean = objectAggregator >= 0 - } + def acceptContinue: UServer = Server.AcceptContinue(true) + + def app[R, E](http: HttpApp[R, E]): Server[R, E] = Server.App(http) /** - * Holds server start information. + * Creates a server from a http app. */ - final case class Start(port: Int = 0) + def apply[R, E](http: HttpApp[R, E]): Server[R, E] = Server.App(http) + + def bind(port: Int): UServer = Server.Address(new InetSocketAddress(port)) + + def bind(hostname: String, port: Int): UServer = Server.Address(new InetSocketAddress(hostname, port)) + + def bind(inetAddress: InetAddress, port: Int): UServer = Server.Address(new InetSocketAddress(inetAddress, port)) - private final case class Concat[R, E](self: Server[R, E], other: Server[R, E]) extends Server[R, E] - private final case class LeakDetection(level: LeakDetectionLevel) extends UServer - private final case class Error[R](errorHandler: Throwable => ZIO[R, Nothing, Unit]) extends Server[R, Nothing] - 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 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 - private final case class UnsafeChannelPipeline(init: ChannelPipeline => Unit) extends UServer - private final case class RequestDecompression(enabled: Boolean, strict: Boolean) extends UServer - private final case class ObjectAggregator(maxRequestSize: Int) extends UServer - - def app[R, E](http: HttpApp[R, E]): Server[R, E] = Server.App(http) - def port(port: Int): UServer = Server.Address(new InetSocketAddress(port)) - def bind(port: Int): UServer = Server.Address(new InetSocketAddress(port)) - def bind(hostname: String, port: Int): UServer = Server.Address(new InetSocketAddress(hostname, port)) - def bind(inetAddress: InetAddress, port: Int): UServer = Server.Address(new InetSocketAddress(inetAddress, port)) def bind(inetSocketAddress: InetSocketAddress): UServer = Server.Address(inetSocketAddress) + + def enableObjectAggregator(maxRequestSize: Int = Int.MaxValue): UServer = ObjectAggregator(maxRequestSize) + 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(true) + + def make[R]( + server: Server[R, Throwable], + ): ZIO[R with EventLoopGroup with ServerChannelFactory with Scope, Throwable, Start] = { + val settings = server.settings() + for { + channelFactory <- ZIO.service[ServerChannelFactory] + eventLoopGroup <- ZIO.service[EventLoopGroup] + zExec <- HttpRuntime.sticky[R](eventLoopGroup) + handler = new ServerResponseWriter(zExec, settings, ServerTime.make) + reqHandler = settings.app.compile(zExec, settings, handler) + init = ServerChannelInitializer(zExec, settings, reqHandler) + serverBootstrap = new ServerBootstrap().channelFactory(channelFactory).group(eventLoopGroup) + chf <- ZIO.attempt(serverBootstrap.childHandler(init).bind(settings.address)) + _ <- ChannelFuture.asZIO(chf) + port <- ZIO.attempt(chf.channel().localAddress().asInstanceOf[InetSocketAddress].getPort) + } yield { + ResourceLeakDetector.setLevel(settings.leakDetectionLevel.jResourceLeakDetectionLevel) + Start(port) + } + } + + def port(port: Int): UServer = Server.Address(new InetSocketAddress(port)) + def requestDecompression(strict: Boolean): UServer = Server.RequestDecompression(enabled = true, strict = strict) - 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 disableKeepAlive: UServer = Server.KeepAlive(false) - val consolidateFlush: UServer = ConsolidateFlush(true) - def unsafePipeline(pipeline: ChannelPipeline => Unit): UServer = UnsafeChannelPipeline(pipeline) - def enableObjectAggregator(maxRequestSize: Int = Int.MaxValue): UServer = ObjectAggregator(maxRequestSize) - /** - * Creates a server from a http app. - */ - def apply[R, E](http: HttpApp[R, E]): Server[R, E] = Server.App(http) + def ssl(sslOptions: ServerSSLOptions): UServer = Server.Ssl(sslOptions) /** * Launches the app on the provided port. @@ -215,7 +215,7 @@ object Server { Server(http) .withPort(port) .make - .flatMap(start => ZIO.succeed(println(s"Server started on port: ${start.port}")) *> ZIO.never) + .flatMap(start => ZIO.succeed(Log.info(s"Server started on port: ${start.port}")) *> ZIO.never) .provideSomeLayer[R](EventLoopGroup.auto(0) ++ ServerChannelFactory.auto ++ Scope.default) def start[R]( @@ -233,23 +233,60 @@ object Server { (Server(http).withBinding(socketAddress).make *> ZIO.never) .provideSomeLayer[R](EventLoopGroup.auto(0) ++ ServerChannelFactory.auto ++ Scope.default) - def make[R]( - server: Server[R, Throwable], - ): ZIO[R with EventLoopGroup with ServerChannelFactory with Scope, Throwable, Start] = { - val settings = server.settings() - for { - channelFactory <- ZIO.service[ServerChannelFactory] - eventLoopGroup <- ZIO.service[EventLoopGroup] - zExec <- HttpRuntime.sticky[R](eventLoopGroup) - reqHandler = settings.app.compile(zExec, settings, ServerTime.make) - init = ServerChannelInitializer(zExec, settings, reqHandler) - serverBootstrap = new ServerBootstrap().channelFactory(channelFactory).group(eventLoopGroup) - chf <- ZIO.attempt(serverBootstrap.childHandler(init).bind(settings.address)) - _ <- ChannelFuture.asZIO(chf) - port <- ZIO.attempt(chf.channel().localAddress().asInstanceOf[InetSocketAddress].getPort) - } yield { - ResourceLeakDetector.setLevel(settings.leakDetectionLevel.jResourceLeakDetectionLevel) - Start(port) - } + def unsafePipeline(pipeline: ChannelPipeline => Unit): UServer = UnsafeChannelPipeline(pipeline) + + def unsafeServerBootstrap(serverBootstrap: ServerBootstrap => Unit): UServer = UnsafeServerBootstrap(serverBootstrap) + + /** + * Holds server start information. + */ + final case class Start(port: Int = 0) + + private[zhttp] final case class Config[-R, +E]( + leakDetectionLevel: LeakDetectionLevel = LeakDetectionLevel.SIMPLE, + error: Option[Throwable => ZIO[R, Nothing, Unit]] = None, + sslOption: ServerSSLOptions = null, + + // TODO: move app out of settings + app: HttpApp[R, E] = Http.empty, + address: InetSocketAddress = new InetSocketAddress(8080), + acceptContinue: Boolean = false, + keepAlive: Boolean = true, + consolidateFlush: Boolean = false, + flowControl: Boolean = true, + channelInitializer: ChannelPipeline => Unit = null, + requestDecompression: (Boolean, Boolean) = (false, false), + objectAggregator: Int = -1, + serverBootstrapInitializer: ServerBootstrap => Unit = null, + ) { + def useAggregator: Boolean = objectAggregator >= 0 } + + private final case class Concat[R, E](self: Server[R, E], other: Server[R, E]) extends Server[R, E] + + private final case class LeakDetection(level: LeakDetectionLevel) extends UServer + + private final case class Error[R](errorHandler: Throwable => ZIO[R, Nothing, Unit]) extends Server[R, Nothing] + + 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 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 + + private final case class UnsafeChannelPipeline(init: ChannelPipeline => Unit) extends UServer + + private final case class RequestDecompression(enabled: Boolean, strict: Boolean) extends UServer + + private final case class ObjectAggregator(maxRequestSize: Int) extends UServer + + private final case class UnsafeServerBootstrap(init: ServerBootstrap => Unit) extends UServer } diff --git a/zio-http/src/main/scala/zhttp/service/ServerResponseWriter.scala b/zio-http/src/main/scala/zhttp/service/ServerResponseWriter.scala new file mode 100644 index 000000000..e2de76108 --- /dev/null +++ b/zio-http/src/main/scala/zhttp/service/ServerResponseWriter.scala @@ -0,0 +1,161 @@ +package zhttp.service + +import io.netty.buffer.ByteBuf +import io.netty.channel.{ChannelHandlerContext, DefaultFileRegion} +import io.netty.handler.codec.http._ +import zhttp.http._ +import zhttp.logging.Logger +import zhttp.service.ServerResponseWriter.log +import zhttp.service.server.ServerTime +import zio._ +import zio.stream.ZStream + +import java.io.File + +private[zhttp] final class ServerResponseWriter[R]( + runtime: HttpRuntime[R], + config: Server.Config[R, Throwable], + serverTime: ServerTime, +) { self => + + /** + * Enables auto-read if possible. Also performs the first read. + */ + private def attemptAutoRead()(implicit ctx: Ctx): Unit = { + if (!config.useAggregator && !ctx.channel().config().isAutoRead) { + ctx.channel().config().setAutoRead(true) + ctx.read(): Unit + } + } + + /** + * Checks if an encoded version of the response exists, uses it if it does. + * Otherwise, it will return a fresh response. It will also set the server + * time if requested by the client. + */ + private def encodeResponse(res: Response): HttpResponse = { + + val jResponse = res.attribute.encoded match { + + // Check if the encoded response exists and/or was modified. + case Some((oRes, jResponse)) if oRes eq res => + jResponse match { + // Duplicate the response without allocating much memory + case response: FullHttpResponse => response.retainedDuplicate() + + case response => response + } + + case _ => res.unsafeEncode() + } + // Identify if the server time should be set and update if required. + if (res.attribute.serverTime) jResponse.headers().set(HttpHeaderNames.DATE, serverTime.refreshAndGet()) + jResponse + } + + private def flushReleaseAndRead(jReq: HttpRequest)(implicit ctx: Ctx): Unit = { + ctx.flush() + releaseAndRead(jReq) + } + + private def releaseAndRead(jReq: HttpRequest)(implicit ctx: Ctx): Unit = { + releaseRequest(jReq) + attemptAutoRead() + } + + /** + * Releases the FullHttpRequest safely. + */ + private def releaseRequest(jReq: HttpRequest)(implicit ctx: Ctx): Unit = { + jReq match { + case jReq: FullHttpRequest if jReq.refCnt() > 0 => jReq.release(jReq.refCnt()): Unit + case _ => () + } + } + + /** + * Writes file content to the Channel. Does not use Chunked transfer encoding + */ + private def unsafeWriteFileContent(file: File)(implicit ctx: ChannelHandlerContext): Unit = { + // Write the content. + ctx.write(new DefaultFileRegion(file, 0, file.length())) + // Write the end marker. + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT): Unit + } + + /** + * Writes data on the channel + */ + private def writeData(data: HttpData, jReq: HttpRequest)(implicit ctx: Ctx): Unit = { + log.debug(s"WriteData: ${data.getClass.getSimpleName}") + data match { + + case _: HttpData.FromAsciiString => flushReleaseAndRead(jReq) + + case _: HttpData.BinaryChunk => flushReleaseAndRead(jReq) + + case _: HttpData.BinaryByteBuf => flushReleaseAndRead(jReq) + + case HttpData.Empty => flushReleaseAndRead(jReq) + + case HttpData.BinaryStream(stream) => + runtime.unsafeRun(ctx) { + writeStreamContent(stream).ensuring(ZIO.succeed(releaseAndRead(jReq))) + } + + case HttpData.JavaFile(unsafeGet) => + unsafeWriteFileContent(unsafeGet()) + releaseAndRead(jReq) + + case HttpData.UnsafeAsync(unsafeRun) => + unsafeRun { _ => msg => + ctx.writeAndFlush(msg) + if (!msg.isInstanceOf[LastHttpContent]) ctx.read(): Unit + } + } + } + + /** + * Writes Binary Stream data to the Channel + */ + private def writeStreamContent[A]( + stream: ZStream[R, Throwable, ByteBuf], + )(implicit ctx: Ctx): ZIO[R, Throwable, Unit] = { + for { + _ <- stream.foreach(c => ZIO.succeed(ctx.writeAndFlush(c))) + _ <- ChannelFuture.unit(ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)) + } yield () + } + + def write(msg: Throwable, jReq: HttpRequest)(implicit ctx: Ctx): Unit = { + val response = Response + .fromHttpError(HttpError.InternalServerError(cause = Some(msg))) + .withConnection(HeaderValues.close) + self.write(response, jReq) + } + + def write(msg: Response, jReq: HttpRequest)(implicit ctx: Ctx): Unit = { + ctx.write(encodeResponse(msg)) + writeData(msg.data, jReq) + () + } + + def write(msg: HttpError, jReq: HttpRequest)(implicit ctx: Ctx): Unit = { + val response = Response.fromHttpError(msg) + self.write(response, jReq) + } + + def write(msg: Status, jReq: HttpRequest)(implicit ctx: Ctx): Unit = { + val response = Response.status(msg) + self.write(response, jReq) + } + + def writeNotFound(jReq: HttpRequest)(implicit ctx: Ctx): Unit = { + val error = HttpError.NotFound(Path.decode(jReq.uri())) + self.write(error, jReq) + } +} + +object ServerResponseWriter { + val log: Logger = Log.withTags("Server", "Response") +} diff --git a/zio-http/src/main/scala/zhttp/service/WebSocketAppHandler.scala b/zio-http/src/main/scala/zhttp/service/WebSocketAppHandler.scala index d49b1d71e..753f5c32a 100644 --- a/zio-http/src/main/scala/zhttp/service/WebSocketAppHandler.scala +++ b/zio-http/src/main/scala/zhttp/service/WebSocketAppHandler.scala @@ -17,7 +17,7 @@ final class WebSocketAppHandler[R]( app: SocketApp[R], ) extends SimpleChannelInboundHandler[JWebSocketFrame] { - override def channelRead0(ctx: ChannelHandlerContext, msg: JWebSocketFrame): Unit = + override def channelRead0(ctx: ChannelHandlerContext, msg: JWebSocketFrame): Unit = { app.message match { case Some(v) => WebSocketFrame.fromJFrame(msg) match { @@ -26,10 +26,11 @@ final class WebSocketAppHandler[R]( } case None => () } + } override def channelUnregistered(ctx: ChannelHandlerContext): Unit = { app.close match { - case Some(v) => zExec.unsafeRun(ctx)(v(ctx.channel().remoteAddress()).uninterruptible) + case Some(v) => zExec.unsafeRunUninterruptible(ctx)(v(ctx.channel().remoteAddress())) case None => ctx.fireChannelUnregistered() } () @@ -37,14 +38,13 @@ final class WebSocketAppHandler[R]( override def exceptionCaught(ctx: ChannelHandlerContext, x: Throwable): Unit = { app.error match { - case Some(v) => zExec.unsafeRun(ctx)(v(x).uninterruptible) + case Some(v) => zExec.unsafeRunUninterruptible(ctx)(v(x)) case None => ctx.fireExceptionCaught(x) } () } override def userEventTriggered(ctx: ChannelHandlerContext, event: AnyRef): Unit = { - event match { case _: WebSocketServerProtocolHandler.HandshakeComplete | ClientHandshakeStateEvent.HANDSHAKE_COMPLETE => app.open match { @@ -68,11 +68,9 @@ final class WebSocketAppHandler[R]( /** * Unsafe channel reader for WSFrame */ - - private def writeAndFlush(ctx: ChannelHandlerContext, stream: ZStream[R, Throwable, WebSocketFrame]): Unit = - zExec.unsafeRun(ctx)( - stream - .mapZIO(frame => ChannelFuture.unit(ctx.writeAndFlush(frame.toWebSocketFrame))) - .runDrain, - ) + private def writeAndFlush(ctx: ChannelHandlerContext, stream: ZStream[R, Throwable, WebSocketFrame]): Unit = { + zExec.unsafeRun(ctx) { + stream.foreach(frame => ChannelFuture.unit(ctx.writeAndFlush(frame.toWebSocketFrame))) + } + } } diff --git a/zio-http/src/main/scala/zhttp/service/client/ClientInboundHandler.scala b/zio-http/src/main/scala/zhttp/service/client/ClientInboundHandler.scala index 77a2347d3..8e41ec6de 100644 --- a/zio-http/src/main/scala/zhttp/service/client/ClientInboundHandler.scala +++ b/zio-http/src/main/scala/zhttp/service/client/ClientInboundHandler.scala @@ -27,8 +27,10 @@ final class ClientInboundHandler[R]( override def channelRead0(ctx: ChannelHandlerContext, msg: FullHttpResponse): Unit = { msg.touch("handlers.ClientInboundHandler-channelRead0") + // NOTE: The promise is made uninterruptible to be able to complete the promise in a error situation. + // It allows to avoid loosing the message from pipeline in case the channel pipeline is closed due to an error. + zExec.unsafeRunUninterruptible(ctx)(promise.succeed(Response.unsafeFromJResponse(ctx, msg))) - zExec.unsafeRunUninterruptible(ctx)(promise.succeed(Response.unsafeFromJResponse(msg))) if (isWebSocket) { ctx.fireChannelRead(msg.retain()) ctx.pipeline().remove(ctx.name()): Unit diff --git a/zio-http/src/main/scala/zhttp/service/logging/NettyLoggerFactory.scala b/zio-http/src/main/scala/zhttp/service/logging/NettyLoggerFactory.scala new file mode 100644 index 000000000..91aaddc71 --- /dev/null +++ b/zio-http/src/main/scala/zhttp/service/logging/NettyLoggerFactory.scala @@ -0,0 +1,49 @@ +package zhttp.service.logging + +import io.netty.util.internal.logging.{AbstractInternalLogger, InternalLogger, InternalLoggerFactory} +import zhttp.logging.{LogLevel, Logger} +import zhttp.service.logging.NettyLoggerFactory.Live + +/** + * Custom implementation that uses the zhttp logger's transport for logging + * netty messages. + */ +final case class NettyLoggerFactory(logger: Logger) extends InternalLoggerFactory { + override def newInstance(name: String): InternalLogger = new Live(name, logger: Logger) +} + +object NettyLoggerFactory { + private final class Live(override val name: String, logger: Logger) extends AbstractInternalLogger(name) { + private val log = logger.withTags("Netty") + override def debug(msg: String): Unit = log.debug(msg) + override def debug(format: String, arg: Any): Unit = log.debug(format.format(arg)) + override def debug(format: String, argA: Any, argB: Any): Unit = log.debug(format.format(argA, argB)) + override def debug(format: String, arguments: Object*): Unit = log.debug(format.format(arguments)) + override def debug(msg: String, t: Throwable): Unit = log.error(msg + "(debug)", t) + override def error(msg: String): Unit = log.error(msg) + override def error(format: String, arg: Any): Unit = log.error(format.format(arg)) + override def error(format: String, argA: Any, argB: Any): Unit = log.error(format.format(argA, argB)) + override def error(format: String, arguments: Object*): Unit = log.error(format.format(arguments)) + override def error(msg: String, t: Throwable): Unit = log.error(msg, t) + override def info(msg: String): Unit = log.info(msg) + override def info(format: String, arg: Any): Unit = log.info(format.format(arg)) + override def info(format: String, argA: Any, argB: Any): Unit = log.info(format.format(argA, argB)) + override def info(format: String, arguments: Object*): Unit = log.info(format.format(arguments)) + override def info(msg: String, t: Throwable): Unit = log.error(msg + "(info)", t) + override def isDebugEnabled: Boolean = log.transports.exists(_.level == LogLevel.Debug) + override def isErrorEnabled: Boolean = log.transports.exists(_.level == LogLevel.Error) + override def isInfoEnabled: Boolean = log.transports.exists(_.level == LogLevel.Info) + override def isTraceEnabled: Boolean = log.transports.exists(_.level == LogLevel.Trace) + override def isWarnEnabled: Boolean = log.transports.exists(_.level == LogLevel.Warn) + override def trace(msg: String): Unit = log.trace(msg) + override def trace(format: String, arg: Any): Unit = log.trace(format.format(arg)) + override def trace(format: String, argA: Any, argB: Any): Unit = log.trace(format.format(argA, argB)) + override def trace(format: String, arguments: Object*): Unit = log.trace(format.format(arguments)) + override def trace(msg: String, t: Throwable): Unit = log.error(msg + "(trace)", t) + override def warn(msg: String): Unit = log.warn(msg) + override def warn(format: String, arg: Any): Unit = log.warn(format.format(arg)) + override def warn(format: String, arguments: Object*): Unit = log.warn(format.format(arguments)) + override def warn(format: String, argA: Any, argB: Any): Unit = log.warn(format.format(argA, argB)) + override def warn(msg: String, t: Throwable): Unit = log.error(msg + "(warn)", t) + } +} diff --git a/zio-http/src/main/scala/zhttp/service/package.scala b/zio-http/src/main/scala/zhttp/service/package.scala index 6ae3949ee..83bce68a5 100644 --- a/zio-http/src/main/scala/zhttp/service/package.scala +++ b/zio-http/src/main/scala/zhttp/service/package.scala @@ -1,8 +1,19 @@ package zhttp -import io.netty.channel.{Channel, ChannelFactory => JChannelFactory, EventLoopGroup => JEventLoopGroup, ServerChannel} +import io.netty.channel.{ + Channel, + ChannelFactory => JChannelFactory, + ChannelHandlerContext, + EventLoopGroup => JEventLoopGroup, + ServerChannel, +} -package object service { +package object service extends Logging { + type ChannelFactory = JChannelFactory[Channel] + type EventLoopGroup = JEventLoopGroup + type ServerChannelFactory = JChannelFactory[ServerChannel] + type UServer = Server[Any, Nothing] + private[zhttp] type Ctx = ChannelHandlerContext private[service] val AUTO_RELEASE_REQUEST = false private[service] val SERVER_CODEC_HANDLER = "SERVER_CODEC" private[service] val HTTP_OBJECT_AGGREGATOR = "HTTP_OBJECT_AGGREGATOR" @@ -20,10 +31,7 @@ package object service { private[service] val CLIENT_INBOUND_HANDLER = "CLIENT_INBOUND_HANDLER" private[service] val WEB_SOCKET_CLIENT_PROTOCOL_HANDLER = "WEB_SOCKET_CLIENT_PROTOCOL_HANDLER" private[service] val HTTP_REQUEST_DECOMPRESSION = "HTTP_REQUEST_DECOMPRESSION" + private[service] val LOW_LEVEL_LOGGING = "LOW_LEVEL_LOGGING" private[zhttp] val HTTP_CONTENT_HANDLER = "HTTP_CONTENT_HANDLER" - type ChannelFactory = JChannelFactory[Channel] - type EventLoopGroup = JEventLoopGroup - type ServerChannelFactory = JChannelFactory[ServerChannel] - type UServer = Server[Any, Nothing] } diff --git a/zio-http/src/main/scala/zhttp/service/server/LogLevelTransform.scala b/zio-http/src/main/scala/zhttp/service/server/LogLevelTransform.scala new file mode 100644 index 000000000..f9ca48821 --- /dev/null +++ b/zio-http/src/main/scala/zhttp/service/server/LogLevelTransform.scala @@ -0,0 +1,15 @@ +package zhttp.service.server +import zhttp.logging.LogLevel + +object LogLevelTransform { + implicit class LogLevelWrapper(level: LogLevel) { + def toNettyLogLevel: io.netty.handler.logging.LogLevel = level match { + case zhttp.logging.LogLevel.Trace => io.netty.handler.logging.LogLevel.TRACE + case zhttp.logging.LogLevel.Debug => io.netty.handler.logging.LogLevel.DEBUG + case zhttp.logging.LogLevel.Info => io.netty.handler.logging.LogLevel.INFO + case zhttp.logging.LogLevel.Warn => io.netty.handler.logging.LogLevel.WARN + case zhttp.logging.LogLevel.Error => io.netty.handler.logging.LogLevel.ERROR + } + } + +} 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 90bf6c370..1c0abfe34 100644 --- a/zio-http/src/main/scala/zhttp/service/server/ServerChannelInitializer.scala +++ b/zio-http/src/main/scala/zhttp/service/server/ServerChannelInitializer.scala @@ -10,8 +10,12 @@ import io.netty.handler.codec.http.HttpObjectDecoder.{ import io.netty.handler.codec.http._ import io.netty.handler.flow.FlowControlHandler import io.netty.handler.flush.FlushConsolidationHandler +import io.netty.handler.logging.LoggingHandler +import zhttp.logging.LogLevel import zhttp.service.Server.Config import zhttp.service._ +import zhttp.service.server.LogLevelTransform._ +import zhttp.service.server.ServerChannelInitializer.log /** * Initializes the netty channel with default handlers @@ -26,10 +30,10 @@ final case class ServerChannelInitializer[R]( // !! IMPORTANT !! // Order of handlers are critical to make this work val pipeline = channel.pipeline() - + log.debug(s"Connection initialized: ${channel.remoteAddress()}") // SSL // Add SSL Handler if CTX is available - val sslctx = if (cfg.sslOption == null) null else cfg.sslOption.sslContext + 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)) @@ -70,6 +74,12 @@ final case class ServerChannelInitializer[R]( // Flushing content is done in batches. Can potentially improve performance. if (cfg.consolidateFlush) pipeline.addLast(HTTP_SERVER_FLUSH_CONSOLIDATION, new FlushConsolidationHandler) + if (EnableNettyLogging) { + import io.netty.util.internal.logging.InternalLoggerFactory + InternalLoggerFactory.setDefaultFactory(zhttp.service.logging.NettyLoggerFactory(log)) + pipeline.addLast(LOW_LEVEL_LOGGING, new LoggingHandler(LogLevel.Debug.toNettyLogLevel)) + } + // RequestHandler // Always add ZIO Http Request Handler pipeline.addLast(HTTP_REQUEST_HANDLER, reqHandler) @@ -78,3 +88,7 @@ final case class ServerChannelInitializer[R]( } } + +object ServerChannelInitializer { + private val log = Log.withTags("Server", "ChannelInitializer") +} diff --git a/zio-http/src/main/scala/zhttp/service/server/content/handlers/ServerResponseHandler.scala b/zio-http/src/main/scala/zhttp/service/server/content/handlers/ServerResponseHandler.scala index 2566a4ee8..8b1378917 100644 --- a/zio-http/src/main/scala/zhttp/service/server/content/handlers/ServerResponseHandler.scala +++ b/zio-http/src/main/scala/zhttp/service/server/content/handlers/ServerResponseHandler.scala @@ -1,128 +1 @@ -package zhttp.service.server.content.handlers -import io.netty.buffer.ByteBuf -import io.netty.channel.{ChannelHandlerContext, DefaultFileRegion} -import io.netty.handler.codec.http._ -import zhttp.http.{HttpData, Response} -import zhttp.service.server.ServerTime -import zhttp.service.{ChannelFuture, HttpRuntime, Server} -import zio.ZIO -import zio.stream.ZStream - -import java.io.File - -private[zhttp] trait ServerResponseHandler[R] { - type Ctx = ChannelHandlerContext - val rt: HttpRuntime[R] - val config: Server.Config[R, Throwable] - - def serverTime: ServerTime - - def writeResponse(msg: Response, jReq: HttpRequest)(implicit ctx: Ctx): Unit = { - ctx.write(encodeResponse(msg)) - writeData(msg.data.asInstanceOf[HttpData.Complete], jReq) - () - } - - /** - * Enables auto-read if possible. Also performs the first read. - */ - private def attemptAutoRead()(implicit ctx: Ctx): Unit = { - if (!config.useAggregator && !ctx.channel().config().isAutoRead) { - ctx.channel().config().setAutoRead(true) - ctx.read(): Unit - } - } - - /** - * Checks if an encoded version of the response exists, uses it if it does. - * Otherwise, it will return a fresh response. It will also set the server - * time if requested by the client. - */ - private def encodeResponse(res: Response): HttpResponse = { - - val jResponse = res.attribute.encoded match { - - // Check if the encoded response exists and/or was modified. - case Some((oRes, jResponse)) if oRes eq res => - jResponse match { - // Duplicate the response without allocating much memory - case response: FullHttpResponse => response.retainedDuplicate() - - case response => response - } - - case _ => res.unsafeEncode() - } - // Identify if the server time should be set and update if required. - if (res.attribute.serverTime) jResponse.headers().set(HttpHeaderNames.DATE, serverTime.refreshAndGet()) - jResponse - } - - private def flushReleaseAndRead(jReq: HttpRequest)(implicit ctx: Ctx): Unit = { - ctx.flush() - releaseAndRead(jReq) - } - - private def releaseAndRead(jReq: HttpRequest)(implicit ctx: Ctx): Unit = { - releaseRequest(jReq) - attemptAutoRead() - } - - /** - * Releases the FullHttpRequest safely. - */ - private def releaseRequest(jReq: HttpRequest)(implicit ctx: Ctx): Unit = { - jReq match { - case jReq: FullHttpRequest if jReq.refCnt() > 0 => jReq.release(jReq.refCnt()): Unit - case _ => () - } - } - - /** - * Writes file content to the Channel. Does not use Chunked transfer encoding - */ - private def unsafeWriteFileContent(file: File)(implicit ctx: ChannelHandlerContext): Unit = { - // Write the content. - ctx.write(new DefaultFileRegion(file, 0, file.length())) - // Write the end marker. - ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT): Unit - } - - /** - * Writes data on the channel - */ - private def writeData(data: HttpData.Complete, jReq: HttpRequest)(implicit ctx: Ctx): Unit = { - data match { - - case _: HttpData.FromAsciiString => flushReleaseAndRead(jReq) - - case _: HttpData.BinaryChunk => flushReleaseAndRead(jReq) - - case _: HttpData.BinaryByteBuf => flushReleaseAndRead(jReq) - - case HttpData.Empty => flushReleaseAndRead(jReq) - - case HttpData.BinaryStream(stream) => - rt.unsafeRun(ctx) { - writeStreamContent(stream).ensuring(ZIO.succeed(releaseAndRead(jReq))) - } - - case HttpData.JavaFile(unsafeGet) => - unsafeWriteFileContent(unsafeGet()) - releaseAndRead(jReq) - } - } - - /** - * Writes Binary Stream data to the Channel - */ - private def writeStreamContent[A]( - stream: ZStream[R, Throwable, ByteBuf], - )(implicit ctx: Ctx): ZIO[R, Throwable, Unit] = { - for { - _ <- stream.foreach(c => ZIO.succeed(ctx.writeAndFlush(c))) - _ <- ChannelFuture.unit(ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)) - } yield () - } -} diff --git a/zio-http/src/main/scala/zhttp/socket/Socket.scala b/zio-http/src/main/scala/zhttp/socket/Socket.scala index f442b6d68..8027d1f64 100644 --- a/zio-http/src/main/scala/zhttp/socket/Socket.scala +++ b/zio-http/src/main/scala/zhttp/socket/Socket.scala @@ -2,11 +2,13 @@ package zhttp.socket import zhttp.http.{Http, Response} import zhttp.service.{ChannelFactory, EventLoopGroup} +import zio._ import zio.stream.ZStream -import zio.{Cause, ZEnvironment, ZIO} sealed trait Socket[-R, +E, -A, +B] { self => import Socket._ + private[zhttp] def execute(a: A): ZStream[R, E, B] = self(a) + def <>[R1 <: R, E1, A1 <: A, B1 >: B](other: Socket[R1, E1, A1, B1]): Socket[R1, E1, A1, B1] = self orElse other @@ -28,13 +30,18 @@ sealed trait Socket[-R, +E, -A, +B] { self => def connect(url: String)(implicit ev: IsWebSocket[R, E, A, B], - ): ZIO[R with EventLoopGroup with ChannelFactory, Throwable, Response] = + ): ZIO[R with EventLoopGroup with ChannelFactory with Scope, Throwable, Response] = self.toSocketApp.connect(url) def contramap[Z](za: Z => A): Socket[R, E, Z, B] = Socket.FCMap(self, za) def contramapZIO[R1 <: R, E1 >: E, Z](za: Z => ZIO[R1, E1, A]): Socket[R1, E1, Z, B] = Socket.FCMapZIO(self, za) + /** + * Delays delivery of messages by the specified duration. + */ + def delay(duration: Duration): Socket[Clock with R, E, A, B] = self.tap(_ => ZIO.sleep(duration)) + def map[C](bc: B => C): Socket[R, E, A, C] = Socket.FMap(self, bc) def mapZIO[R1 <: R, E1 >: E, C](bc: B => ZIO[R1, E1, C]): Socket[R1, E1, A, C] = Socket.FMapZIO(self, bc) @@ -53,6 +60,12 @@ sealed trait Socket[-R, +E, -A, +B] { self => def provideEnvironment(r: ZEnvironment[R]): Socket[Any, E, A, B] = ProvideEnvironment(self, r) + /** + * Executes the effect for each message received from the socket, and ignores + * the output produced. + */ + def tap[R1 <: R, E1 >: E](f: B => ZIO[R1, E1, Any]): Socket[R1, E1, A, B] = self.mapZIO(b => f(b).as(b)) + /** * Converts the Socket into an Http */ @@ -67,8 +80,6 @@ sealed trait Socket[-R, +E, -A, +B] { self => * Creates a socket application from the socket. */ def toSocketApp(implicit ev: IsWebSocket[R, E, A, B]): SocketApp[R] = SocketApp(self) - - private[zhttp] def execute(a: A): ZStream[R, E, B] = self(a) } object Socket { @@ -87,8 +98,12 @@ object Socket { def end: Socket[Any, Nothing, Any, Nothing] = Socket.End + def from[A](iter: A*): Socket[Any, Nothing, Any, A] = fromIterable(iter) + def fromFunction[A]: PartialFromFunction[A] = new PartialFromFunction[A](()) + def fromIterable[A](iter: Iterable[A]): Socket[Any, Nothing, Any, A] = Socket.fromStream(ZStream.fromIterable(iter)) + 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) diff --git a/zio-http/src/main/scala/zhttp/socket/SocketApp.scala b/zio-http/src/main/scala/zhttp/socket/SocketApp.scala index 898f2b01c..69daa71a2 100644 --- a/zio-http/src/main/scala/zhttp/socket/SocketApp.scala +++ b/zio-http/src/main/scala/zhttp/socket/SocketApp.scala @@ -23,7 +23,7 @@ final case class SocketApp[-R]( * Creates a socket connection on the provided URL. Typically used to connect * as a client. */ - def connect(url: String): ZIO[R with EventLoopGroup with ChannelFactory, Throwable, Response] = + def connect(url: String): ZIO[R with EventLoopGroup with ChannelFactory with Scope, Throwable, Response] = Client.socket(url, self) /** diff --git a/zio-http/src/main/scala/zhttp/socket/WebSocketFrame.scala b/zio-http/src/main/scala/zhttp/socket/WebSocketFrame.scala index 505d00224..c983e8cad 100644 --- a/zio-http/src/main/scala/zhttp/socket/WebSocketFrame.scala +++ b/zio-http/src/main/scala/zhttp/socket/WebSocketFrame.scala @@ -70,7 +70,7 @@ object WebSocketFrame { case m: CloseWebSocketFrame => Option(Close(m.statusCode(), Option(m.reasonText()))) case m: ContinuationWebSocketFrame => - Option(Continuation((m.content()), m.isFinalFragment)) + Option(Continuation(m.content(), m.isFinalFragment)) case _ => None } diff --git a/zio-http/src/test/scala/zhttp/endpoint/EndpointSpec.scala b/zio-http/src/test/scala/zhttp/endpoint/EndpointSpec.scala index 8c7519506..8b1378917 100644 --- a/zio-http/src/test/scala/zhttp/endpoint/EndpointSpec.scala +++ b/zio-http/src/test/scala/zhttp/endpoint/EndpointSpec.scala @@ -1,85 +1 @@ -package zhttp.endpoint -import zhttp.http._ -import zio.ZIO -import zio.test.Assertion._ -import zio.test.{ZIOSpecDefault, assert, assertZIO} - -object EndpointSpec extends ZIOSpecDefault { - def spec = suite("Route") { - test("match method") { - val route = Endpoint.fromMethod(Method.GET) - val request = Request(method = Method.GET) - assert(route.extract(request))(isSome(equalTo(()))) - } - test("not match method") { - val route = Endpoint.fromMethod(Method.POST) - val request = Request(method = Method.GET) - assert(route.extract(request))(isNone) - } + - test("match method and string") { - val route = Method.GET / "a" - val request = Request(method = Method.GET, url = URL(Path("a"))) - assert(route.extract(request))(isSome(equalTo(()))) - } + - test("match method and not string") { - val route = Method.GET / "a" - val request = Request(method = Method.GET, url = URL(Path("b"))) - assert(route.extract(request))(isNone) - } - } + suite("Path") { - test("Route[Int]") { - val route = Method.GET / *[Int] - assert(route.extract(!! / "1"))(isSome(equalTo(1))) && assert(route.extract(!! / "a"))(isNone) - } + - test("Route[String]") { - val route = Method.GET / *[String] - assert(route.extract(!! / "a"))(isSome(equalTo("a"))) - } + - test("Route[Boolean]") { - val route = Method.GET / *[Boolean] - assert(route.extract(!! / "True"))(isSome(isTrue)) && - assert(route.extract(!! / "False"))(isSome(isFalse)) && - assert(route.extract(!! / "a"))(isNone) && - assert(route.extract(!! / "1"))(isNone) - } + - test("Route[Int] / Route[Int]") { - val route = Method.GET / *[Int] / *[Int] - assert(route.extract(!! / "1" / "2"))(isSome(equalTo((1, 2)))) && - assert(route.extract(!! / "1" / "b"))(isNone) && - assert(route.extract(!! / "b" / "1"))(isNone) && - assert(route.extract(!! / "1"))(isNone) && - assert(route.extract(!!))(isNone) - } + - test("Route[Int] / c") { - val route = Method.GET / *[Int] / "c" - assert(route.extract(!! / "1" / "c"))(isSome(equalTo(1))) && - assert(route.extract(!! / "1"))(isNone) && - assert(route.extract(!! / "c"))(isNone) - } + - test("Route[Int] / c") { - val route = Method.GET / *[Int] / "c" - assert(route.extract(!! / "1" / "c"))(isSome(equalTo(1))) && - assert(route.extract(!! / "1"))(isNone) && - assert(route.extract(!! / "c"))(isNone) - } - } + - suite("to") { - test("endpoint doesn't match") { - val app = Method.GET / "a" to { _ => Response.ok } - assertZIO(app(Request(url = URL(!! / "b"))).flip)(isNone) - } + - test("endpoint with effect doesn't match") { - val app = Method.GET / "a" to { _ => ZIO.succeed(Response.ok) } - assertZIO(app(Request(url = URL(!! / "b"))).flip)(isNone) - } + - test("endpoint matches") { - val app = Method.GET / "a" to { _ => Response.ok } - assertZIO(app(Request(url = URL(!! / "a"))).map(_.status))(equalTo(Status.Ok)) - } + - test("endpoint with effect matches") { - val app = Method.GET / "a" to { _ => ZIO.succeed(Response.ok) } - assertZIO(app(Request(url = URL(!! / "a"))).map(_.status))(equalTo(Status.Ok)) - } - } -} diff --git a/zio-http/src/test/scala/zhttp/html/DomSpec.scala b/zio-http/src/test/scala/zhttp/html/DomSpec.scala index 85cae08fe..6c11fdf93 100644 --- a/zio-http/src/test/scala/zhttp/html/DomSpec.scala +++ b/zio-http/src/test/scala/zhttp/html/DomSpec.scala @@ -1,6 +1,6 @@ package zhttp.html -import zio.test.{Gen, ZIOSpecDefault, assertTrue, checkAll} +import zio.test.{Gen, ZIOSpecDefault, assertTrue, check, checkAll} object DomSpec extends ZIOSpecDefault { def spec = suite("DomSpec") { @@ -79,7 +79,7 @@ object DomSpec extends ZIOSpecDefault { } } + test("not void") { - checkAll(tagGen) { name => + check(tagGen) { name => val dom = Dom.element(name) assertTrue(dom.encode == s"<${name}>") } diff --git a/zio-http/src/test/scala/zhttp/html/HtmlSpec.scala b/zio-http/src/test/scala/zhttp/html/HtmlSpec.scala index 8635519ad..f7a456ebf 100644 --- a/zio-http/src/test/scala/zhttp/html/HtmlSpec.scala +++ b/zio-http/src/test/scala/zhttp/html/HtmlSpec.scala @@ -44,7 +44,13 @@ case object HtmlSpec extends ZIOSpecDefault { val view = div("Hello!", css := "container" :: Nil) val expected = """
Hello!
""" assert(view.encode)(equalTo(expected.stripMargin)) - }, + } + + suite("implicit conversions")( + test("from unit") { + val view: Html = {} + assert(view.encode)(equalTo("")) + }, + ), ) } } diff --git a/zio-http/src/test/scala/zhttp/http/CookieSpec.scala b/zio-http/src/test/scala/zhttp/http/CookieSpec.scala index 570e968dd..7769ed5c8 100644 --- a/zio-http/src/test/scala/zhttp/http/CookieSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/CookieSpec.scala @@ -8,22 +8,22 @@ object CookieSpec extends ZIOSpecDefault { def spec = suite("Cookies") { suite("response cookies") { test("encode/decode signed/unsigned cookies with secret") { - checkAll(HttpGen.cookies) { cookie => - val cookieString = cookie.encode - assert(Cookie.decodeResponseCookie(cookieString, cookie.secret))(isSome(equalTo(cookie))) && - assert(Cookie.decodeResponseCookie(cookieString, cookie.secret).map(_.encode))(isSome(equalTo(cookieString))) + check(HttpGen.cookies) { cookie => + val expected = cookie.encode + val actual = Cookie.decodeResponseCookie(expected, cookie.secret).map(_.encode) + assert(actual)(isSome(equalTo(expected))) } } } + suite("request cookies") { test("encode/decode multiple cookies with ZIO Test Gen") { - checkAll(for { + check(for { name <- Gen.string content <- Gen.string cookieList <- Gen.listOf(Gen.const(Cookie(name, content))) cookieString <- Gen.const(cookieList.map(x => s"${x.name}=${x.content}").mkString(";")) } yield (cookieList, cookieString)) { case (cookies, message) => - assert(Cookie.decodeRequestCookie(message))(isSome(equalTo(cookies))) + assert(Cookie.decodeRequestCookie(message))(equalTo(cookies)) } } } diff --git a/zio-http/src/test/scala/zhttp/http/HttpSpec.scala b/zio-http/src/test/scala/zhttp/http/HttpSpec.scala index c33949391..f1cedd5f9 100644 --- a/zio-http/src/test/scala/zhttp/http/HttpSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/HttpSpec.scala @@ -6,6 +6,7 @@ import zio.test.TestAspect.timeout import zio.test._ object HttpSpec extends ZIOSpecDefault with HExitAssertion { + def spec = suite("Http")( suite("flatMap")( test("should flatten") { @@ -13,522 +14,512 @@ object HttpSpec extends ZIOSpecDefault with HExitAssertion { val actual = app.execute(0) assert(actual)(isSuccess(equalTo(2))) }, - ) + - suite("orElse")( - test("should succeed") { - val a1 = Http.succeed(1) - val a2 = Http.succeed(2) + ), + suite("orElse")( + test("should succeed") { + val a1 = Http.succeed(1) + val a2 = Http.succeed(2) + val a = a1 <> a2 + val actual = a.execute(()) + assert(actual)(isSuccess(equalTo(1))) + } + + test("should fail with first") { + val a1 = Http.fail("A") + val a2 = Http.succeed("B") val a = a1 <> a2 val actual = a.execute(()) - assert(actual)(isSuccess(equalTo(1))) + assert(actual)(isSuccess(equalTo("B"))) } + - test("should fail with first") { - val a1 = Http.fail("A") - val a2 = Http.succeed("B") - val a = a1 <> a2 - val actual = a.execute(()) - assert(actual)(isSuccess(equalTo("B"))) - } + - test("does not recover from defects") { - val t = new Throwable("boom") - val a1 = Http.die(t) - val a2 = Http.succeed("B") - val a = a1 <> a2 - val actual = a.execute(()) - assert(actual)(isDie(equalTo(t))) - }, - ) + - suite("fail")( - test("should fail") { - val a = Http.fail(100) - val actual = a.execute(()) - assert(actual)(isFailure(equalTo(100))) - }, - ) + - suite("die")( - test("should die") { + test("does not recover from defects") { val t = new Throwable("boom") - val a = Http.die(t) + val a1 = Http.die(t) + val a2 = Http.succeed("B") + val a = a1 <> a2 val actual = a.execute(()) assert(actual)(isDie(equalTo(t))) }, - ) + - suite("foldM")( - test("should catch") { - val a = Http.fail(100).catchAll(e => Http.succeed(e + 1)) + ), + suite("fail")( + test("should fail") { + val a = Http.fail(100) + val actual = a.execute(()) + assert(actual)(isFailure(equalTo(100))) + }, + ), + suite("die")( + test("should die") { + val t = new Throwable("boom") + val a = Http.die(t) + val actual = a.execute(()) + assert(actual)(isDie(equalTo(t))) + }, + ), + suite("foldM")( + test("should catch") { + val a = Http.fail(100).catchAll(e => Http.succeed(e + 1)) + val actual = a.execute(0) + assert(actual)(isSuccess(equalTo(101))) + }, + ), + suite("identity")( + test("should passthru") { + val a = Http.identity[Int] + val actual = a.execute(0) + assert(actual)(isSuccess(equalTo(0))) + }, + ), + suite("collect")( + test("should succeed") { + val a = Http.collect[Int] { case 1 => "OK" } + val actual = a.execute(1) + assert(actual)(isSuccess(equalTo("OK"))) + } + + test("should fail") { + val a = Http.collect[Int] { case 1 => "OK" } val actual = a.execute(0) - assert(actual)(isSuccess(equalTo(101))) + assert(actual)(isEmpty) }, - ) + - suite("identity")( - test("should passthru") { - val a = Http.identity[Int] - val actual = a.execute(0) - assert(actual)(isSuccess(equalTo(0))) + ), + suite("codecMiddleware")( + test("codec success") { + val a = Http.collect[Int] { case v => v.toString } + val b = Http.collect[String] { case v => v.toInt } + val app = Http.identity[String] @@ (a \/ b) + val actual = app.execute(2) + assert(actual)(isSuccess(equalTo(2))) + } + + test("encoder failure") { + val app = Http.identity[Int] @@ (Http.succeed(1) \/ Http.fail("fail")) + val actual = app.execute(()) + assert(actual)(isFailure(equalTo("fail"))) + } + + test("decoder failure") { + val app = Http.identity[Int] @@ (Http.fail("fail") \/ Http.succeed(1)) + val actual = app.execute(()) + assert(actual)(isFailure(equalTo("fail"))) }, - ) + - suite("collect")( - test("should succeed") { - val a = Http.collect[Int] { case 1 => "OK" } + ), + suite("collectHExit")( + test("should succeed") { + val a = Http.collectHExit[Int] { case 1 => HExit.succeed("OK") } + val actual = a.execute(1) + assert(actual)(isSuccess(equalTo("OK"))) + } + + test("should fail") { + val a = Http.collectHExit[Int] { case 1 => HExit.fail("OK") } val actual = a.execute(1) - assert(actual)(isSuccess(equalTo("OK"))) + assert(actual)(isFailure(equalTo("OK"))) } + - test("should fail") { - val a = Http.collect[Int] { case 1 => "OK" } - val actual = a.execute(0) - assert(actual)(isEmpty) - }, - ) + - suite("codecMiddleware")( - test("codec success") { - val a = Http.collect[Int] { case v => v.toString } - val b = Http.collect[String] { case v => v.toInt } - val app = Http.identity[String] @@ (a \/ b) - val actual = app.execute(2) - assert(actual)(isSuccess(equalTo(2))) + test("should die") { + val t = new Throwable("boom") + val a = Http.collectHExit[Int] { case 1 => HExit.die(t) } + val actual = a.execute(1) + assert(actual)(isDie(equalTo(t))) } + - test("encoder failure") { - val app = Http.identity[Int] @@ (Http.succeed(1) \/ Http.fail("fail")) - val actual = app.execute(()) - assert(actual)(isFailure(equalTo("fail"))) - } + - test("decoder failure") { - val app = Http.identity[Int] @@ (Http.fail("fail") \/ Http.succeed(1)) - val actual = app.execute(()) - assert(actual)(isFailure(equalTo("fail"))) - }, - ) + - suite("collectHExit")( - test("should succeed") { + test("should give empty if the inout is not defined") { val a = Http.collectHExit[Int] { case 1 => HExit.succeed("OK") } + val actual = a.execute(0) + assert(actual)(isEmpty) + }, + ), + suite("fromFunctionHExit")( + test("should succeed if the ") { + val a = Http.fromFunctionHExit[Int] { a => HExit.succeed(a + 1) } + val actual = a.execute(1) + assert(actual)(isSuccess(equalTo(2))) + } + + test("should fail if the returned HExit is a failure") { + val a = Http.fromFunctionHExit[Int] { a => HExit.fail(a + 1) } val actual = a.execute(1) - assert(actual)(isSuccess(equalTo("OK"))) + assert(actual)(isFailure(equalTo(2))) } + - test("should fail") { - val a = Http.collectHExit[Int] { case 1 => HExit.fail("OK") } - val actual = a.execute(1) - assert(actual)(isFailure(equalTo("OK"))) - } + - test("should die") { - val t = new Throwable("boom") - val a = Http.collectHExit[Int] { case 1 => HExit.die(t) } - val actual = a.execute(1) - assert(actual)(isDie(equalTo(t))) - } + - test("should give empty if the inout is not defined") { - val a = Http.collectHExit[Int] { case 1 => HExit.succeed("OK") } - val actual = a.execute(0) - assert(actual)(isEmpty) - }, - ) + - suite("fromFunctionHExit")( - test("should succeed if the ") { - val a = Http.fromFunctionHExit[Int] { a => HExit.succeed(a + 1) } + test("should give empty if the returned HExit is empty") { + val a = Http.fromFunctionHExit[Int] { _ => HExit.empty } + val actual = a.execute(0) + assert(actual)(isEmpty) + } + + test("should die if the functions throws an exception") { + val t = new Throwable("boom") + val a = Http.fromFunctionHExit[Int] { _ => throw t } + val actual = a.execute(0) + assert(actual)(isDie(equalTo(t))) + }, + ), + suite("fromHExit")( + test("should succeed if the returned HExit succeeds ") { + val a = Http.fromHExit(HExit.succeed("a")) + val actual = a.execute(1) + assert(actual)(isSuccess(equalTo("a"))) + } + + test("should fail if the returned HExit is a failure") { + val a = Http.fromHExit(HExit.fail("fail")) val actual = a.execute(1) - assert(actual)(isSuccess(equalTo(2))) + assert(actual)(isFailure(equalTo("fail"))) } + - test("should fail if the returned HExit is a failure") { - val a = Http.fromFunctionHExit[Int] { a => HExit.fail(a + 1) } - val actual = a.execute(1) - assert(actual)(isFailure(equalTo(2))) - } + - test("should give empty if the returned HExit is empty") { - val a = Http.fromFunctionHExit[Int] { _ => HExit.empty } - val actual = a.execute(0) - assert(actual)(isEmpty) - } + - test("should die if the functions throws an exception") { - val t = new Throwable("boom") - val a = Http.fromFunctionHExit[Int] { _ => throw t } - val actual = a.execute(0) - assert(actual)(isDie(equalTo(t))) - }, - ) + - suite("fromHExit")( - test("should succeed if the returned HExit succeeds ") { - val a = Http.fromHExit(HExit.succeed("a")) + test("should give empty if the returned HExit is empty") { + val a = Http.fromHExit(HExit.empty) val actual = a.execute(1) - assert(actual)(isSuccess(equalTo("a"))) + assert(actual)(isEmpty) + }, + ), + suite("combine")( + test("should resolve first") { + val a = Http.collect[Int] { case 1 => "A" } + val b = Http.collect[Int] { case 2 => "B" } + val actual = (a ++ b).execute(1) + assert(actual)(isSuccess(equalTo("A"))) + } + + test("should resolve second") { + val a = Http.empty + val b = Http.succeed("A") + val actual = (a ++ b).execute(()) + assert(actual)(isSuccess(equalTo("A"))) } + - test("should fail if the returned HExit is a failure") { - val a = Http.fromHExit(HExit.fail("fail")) - val actual = a.execute(1) - assert(actual)(isFailure(equalTo("fail"))) - } + - test("should give empty if the returned HExit is empty") { - val a = Http.fromHExit(HExit.empty) - val actual = a.execute(1) - assert(actual)(isEmpty) - }, - ) + - - suite("combine")( - test("should resolve first") { + test("should resolve second") { val a = Http.collect[Int] { case 1 => "A" } val b = Http.collect[Int] { case 2 => "B" } - val actual = (a ++ b).execute(1) - assert(actual)(isSuccess(equalTo("A"))) + val actual = (a ++ b).execute(2) + assert(actual)(isSuccess(equalTo("B"))) } + - test("should resolve second") { - val a = Http.empty - val b = Http.succeed("A") - val actual = (a ++ b).execute(()) - assert(actual)(isSuccess(equalTo("A"))) - } + - test("should resolve second") { - val a = Http.collect[Int] { case 1 => "A" } - val b = Http.collect[Int] { case 2 => "B" } - val actual = (a ++ b).execute(2) - assert(actual)(isSuccess(equalTo("B"))) - } + - test("should not resolve") { - val a = Http.collect[Int] { case 1 => "A" } - val b = Http.collect[Int] { case 2 => "B" } - val actual = (a ++ b).execute(3) - assert(actual)(isEmpty) - } + - test("should not resolve") { - val a = Http.empty - val b = Http.empty - val c = Http.empty - val actual = (a ++ b ++ c).execute(()) - assert(actual)(isEmpty) - } + - test("should fail with second") { - val a = Http.empty - val b = Http.fail(100) - val c = Http.succeed("A") - val actual = (a ++ b ++ c).execute(()) - assert(actual)(isFailure(equalTo(100))) - } + - test("should resolve third") { - val a = Http.empty - val b = Http.empty - val c = Http.succeed("C") - val actual = (a ++ b ++ c).execute(()) - assert(actual)(isSuccess(equalTo("C"))) - } + - test("should resolve second") { - val a = Http.fromHExit(HExit.Effect(ZIO.fail(None))) - val b = Http.succeed(2) - val actual = (a ++ b).execute(()).toZIO.either - assertZIO(actual)(isRight) - }, - ) + - suite("asEffect")( - test("should resolve") { + test("should not resolve") { val a = Http.collect[Int] { case 1 => "A" } - val actual = a.execute(1).toZIO - assertZIO(actual)(equalTo("A")) + val b = Http.collect[Int] { case 2 => "B" } + val actual = (a ++ b).execute(3) + assert(actual)(isEmpty) } + - test("should complete") { - val a = Http.collect[Int] { case 1 => "A" } - val actual = a.execute(2).toZIO.either - assertZIO(actual)(isLeft(isNone)) - }, - ) + - suite("collectM")( - test("should be empty") { - val a = Http.collectZIO[Int] { case 1 => ZIO.succeed("A") } - val actual = a.execute(2) + test("should not resolve") { + val a = Http.empty + val b = Http.empty + val c = Http.empty + val actual = (a ++ b ++ c).execute(()) assert(actual)(isEmpty) } + - test("should resolve") { - val a = Http.collectZIO[Int] { case 1 => ZIO.succeed("A") } - val actual = a.execute(1) - assert(actual)(isEffect) - } + - test("should resolve scoped") { - val a = Http.collectScoped[Int] { case 1 => ZIO.succeed("A") } - val actual = a.execute(1) - assert(actual)(isEffect) - } + - test("should resolve second effect") { - val a = Http.empty - val b = Http.succeed("B") - val actual = (a ++ b).execute(2) - assert(actual)(isSuccess(equalTo("B"))) - }, - ) + - suite("route")( - test("should delegate to its HTTP apps") { - val app = Http.route[Int] { - case 1 => Http.succeed(1) - case 2 => Http.succeed(2) - } - val actual = app.execute(2) - assert(actual)(isSuccess(equalTo(2))) + test("should fail with second") { + val a = Http.empty + val b = Http.fail(100) + val c = Http.succeed("A") + val actual = (a ++ b ++ c).execute(()) + assert(actual)(isFailure(equalTo(100))) } + - test("should be empty if no matches") { - val app = Http.route[Int](Map.empty) - val actual = app.execute(1) - assert(actual)(isEmpty) - }, - ) + - suite("tap")( - test("taps the successs") { - for { - r <- Ref.make(0) - app = Http.succeed(1).tap(v => Http.fromZIO(r.set(v))) - _ <- app.execute(()).toZIO - res <- r.get - } yield assert(res)(equalTo(1)) - }, - ) + - suite("tapM")( - test("taps the successs") { - for { - r <- Ref.make(0) - app = Http.succeed(1).tapZIO(r.set) - _ <- app.execute(()).toZIO - res <- r.get - } yield assert(res)(equalTo(1)) - }, - ) + - suite("tapError")( - test("taps the error") { - for { - r <- Ref.make(0) - app = Http.fail(1).tapError(v => Http.fromZIO(r.set(v))) - _ <- app.execute(()).toZIO.ignore - res <- r.get - } yield assert(res)(equalTo(1)) + test("should resolve third") { + val a = Http.empty + val b = Http.empty + val c = Http.succeed("C") + val actual = (a ++ b ++ c).execute(()) + assert(actual)(isSuccess(equalTo("C"))) + } + + test("should resolve second") { + val a = Http.fromHExit(HExit.Effect(ZIO.fail(None))) + val b = Http.succeed(2) + val actual = (a ++ b).execute(()).toZIO.either + assertZIO(actual)(isRight) }, - ) + - suite("tapErrorM")( - test("taps the error") { - for { - r <- Ref.make(0) - app = Http.fail(1).tapErrorZIO(r.set) - _ <- app.execute(()).toZIO.ignore - res <- r.get - } yield assert(res)(equalTo(1)) + ), + suite("asEffect")( + test("should resolve") { + val a = Http.collect[Int] { case 1 => "A" } + val actual = a.execute(1).toZIO + assertZIO(actual)(equalTo("A")) + } + + test("should complete") { + val a = Http.collect[Int] { case 1 => "A" } + val actual = a.execute(2).toZIO.either + assertZIO(actual)(isLeft(isNone)) }, - ) + - suite("tapAll")( - test("taps the success") { - for { - r <- Ref.make(0) - app = (Http.succeed(1): Http[Any, Any, Any, Int]) - .tapAll(_ => Http.empty, _ => Http.empty, v => Http.fromZIO(r.set(v)), Http.empty) - _ <- app.execute(()).toZIO - res <- r.get - } yield assert(res)(equalTo(1)) + ), + suite("collectM")( + test("should be empty") { + val a = Http.collectZIO[Int] { case 1 => ZIO.succeed("A") } + val actual = a.execute(2) + assert(actual)(isEmpty) + } + + test("should resolve") { + val a = Http.collectZIO[Int] { case 1 => ZIO.succeed("A") } + val actual = a.execute(1) + assert(actual)(isEffect) + } + + test("should resolve scoped") { + val a = Http.collectScoped[Int] { case 1 => ZIO.succeed("A") } + val actual = a.execute(1) + assert(actual)(isEffect) + } + + test("should resolve second effect") { + val a = Http.empty + val b = Http.succeed("B") + val actual = (a ++ b).execute(2) + assert(actual)(isSuccess(equalTo("B"))) }, + ), + suite("tap")( + test("taps the successs") { + for { + r <- Ref.make(0) + app = Http.succeed(1).tap(v => Http.fromZIO(r.set(v))) + _ <- app.execute(()).toZIO + res <- r.get + } yield assert(res)(equalTo(1)) + }, + ), + suite("tapM")( + test("taps the successs") { + for { + r <- Ref.make(0) + app = Http.succeed(1).tapZIO(r.set) + _ <- app.execute(()).toZIO + res <- r.get + } yield assert(res)(equalTo(1)) + }, + ), + suite("tapError")( + test("taps the error") { + for { + r <- Ref.make(0) + app = Http.fail(1).tapError(v => Http.fromZIO(r.set(v))) + _ <- app.execute(()).toZIO.ignore + res <- r.get + } yield assert(res)(equalTo(1)) + }, + ), + suite("tapErrorM")( + test("taps the error") { + for { + r <- Ref.make(0) + app = Http.fail(1).tapErrorZIO(r.set) + _ <- app.execute(()).toZIO.ignore + res <- r.get + } yield assert(res)(equalTo(1)) + }, + ), + suite("tapAll")( + test("taps the success") { + for { + r <- Ref.make(0) + app = (Http.succeed(1): Http[Any, Any, Any, Int]) + .tapAll(_ => Http.empty, _ => Http.empty, v => Http.fromZIO(r.set(v)), Http.empty) + _ <- app.execute(()).toZIO + res <- r.get + } yield assert(res)(equalTo(1)) + }, + test("taps the failure") { + for { + r <- Ref.make(0) + app = (Http.fail(1): Http[Any, Int, Any, Any]) + .tapAll(v => Http.fromZIO(r.set(v)), _ => Http.empty, _ => Http.empty, Http.empty) + _ <- app.execute(()).toZIO.ignore + res <- r.get + } yield assert(res)(equalTo(1)) + }, + test("taps the die") { + val t = new Throwable("boom") + for { + r <- Ref.make(0) + app = (Http.die(t): Http[Any, Any, Any, Any]) + .tapAll(_ => Http.empty, _ => Http.fromZIO(r.set(1)), _ => Http.empty, Http.empty) + _ <- app.execute(()).toZIO.exit.ignore + res <- r.get + } yield assert(res)(equalTo(1)) + }, + test("taps the empty") { + for { + r <- Ref.make(0) + app = (Http.empty: Http[Any, Any, Any, Any]) + .tapAll(_ => Http.empty, _ => Http.empty, _ => Http.empty, Http.fromZIO(r.set(1))) + _ <- app.execute(()).toZIO.ignore + res <- r.get + } yield assert(res)(equalTo(1)) + }, + ), + suite("tapAllZIO")( + test("taps the success") { + for { + r <- Ref.make(0) + app = (Http.succeed(1): Http[Any, Any, Any, Int]).tapAllZIO(_ => ZIO.unit, _ => ZIO.unit, r.set, ZIO.unit) + _ <- app.execute(()).toZIO + res <- r.get + } yield assert(res)(equalTo(1)) + } + test("taps the failure") { for { r <- Ref.make(0) - app = (Http.fail(1): Http[Any, Int, Any, Any]) - .tapAll(v => Http.fromZIO(r.set(v)), _ => Http.empty, _ => Http.empty, Http.empty) + app = (Http.fail(1): Http[Any, Int, Any, Any]).tapAllZIO(r.set, _ => ZIO.unit, _ => ZIO.unit, ZIO.unit) _ <- app.execute(()).toZIO.ignore res <- r.get } yield assert(res)(equalTo(1)) - }, + } + test("taps the die") { val t = new Throwable("boom") for { r <- Ref.make(0) app = (Http.die(t): Http[Any, Any, Any, Any]) - .tapAll(_ => Http.empty, _ => Http.fromZIO(r.set(1)), _ => Http.empty, Http.empty) + .tapAllZIO(_ => ZIO.unit, _ => r.set(1), _ => ZIO.unit, ZIO.unit) _ <- app.execute(()).toZIO.exit.ignore res <- r.get } yield assert(res)(equalTo(1)) - }, + } + test("taps the empty") { for { r <- Ref.make(0) app = (Http.empty: Http[Any, Any, Any, Any]) - .tapAll(_ => Http.empty, _ => Http.empty, _ => Http.empty, Http.fromZIO(r.set(1))) + .tapAllZIO(_ => ZIO.unit, _ => ZIO.unit, _ => ZIO.unit, r.set(1)) _ <- app.execute(()).toZIO.ignore res <- r.get } yield assert(res)(equalTo(1)) }, - ) + - suite("tapAllZIO")( - test("taps the success") { - for { - r <- Ref.make(0) - app = (Http.succeed(1): Http[Any, Any, Any, Int]).tapAllZIO(_ => ZIO.unit, _ => ZIO.unit, r.set, ZIO.unit) - _ <- app.execute(()).toZIO - res <- r.get - } yield assert(res)(equalTo(1)) + ), + suite("race") { + test("left wins") { + val http = Http.succeed(1) race Http.succeed(2) + assertZIO(http(()))(equalTo(1)) + } + + test("sync right wins") { + val http = Http.fromZIO(ZIO.succeed(1)) race Http.succeed(2) + assertZIO(http(()))(equalTo(2)) } + - test("taps the failure") { - for { - r <- Ref.make(0) - app = (Http.fail(1): Http[Any, Int, Any, Any]).tapAllZIO(r.set, _ => ZIO.unit, _ => ZIO.unit, ZIO.unit) - _ <- app.execute(()).toZIO.ignore - res <- r.get - } yield assert(res)(equalTo(1)) - } + - test("taps the die") { - val t = new Throwable("boom") - for { - r <- Ref.make(0) - app = (Http.die(t): Http[Any, Any, Any, Any]) - .tapAllZIO(_ => ZIO.unit, _ => r.set(1), _ => ZIO.unit, ZIO.unit) - _ <- app.execute(()).toZIO.exit.ignore - res <- r.get - } yield assert(res)(equalTo(1)) - } + - test("taps the empty") { - for { - r <- Ref.make(0) - app = (Http.empty: Http[Any, Any, Any, Any]) - .tapAllZIO(_ => ZIO.unit, _ => ZIO.unit, _ => ZIO.unit, r.set(1)) - _ <- app.execute(()).toZIO.ignore - res <- r.get - } yield assert(res)(equalTo(1)) - }, - ) + - suite("race") { - test("left wins") { - val http = Http.succeed(1) race Http.succeed(2) + test("sync left wins") { + val http = Http.succeed(1) race Http.fromZIO(ZIO.succeed(2)) assertZIO(http(()))(equalTo(1)) } + - test("sync right wins") { - val http = Http.fromZIO(ZIO.succeed(1)) race Http.succeed(2) - assertZIO(http(()))(equalTo(2)) - } + - test("sync left wins") { - val http = Http.succeed(1) race Http.fromZIO(ZIO.succeed(2)) - assertZIO(http(()))(equalTo(1)) - } + - test("async fast wins") { - val http = Http.succeed(1).delay(1 second) race Http.succeed(2).delay(2 second) - val program = http(()) <& TestClock.adjust(5 second) - assertZIO(program)(equalTo(1)) - } + test("async fast wins") { + val http = Http.succeed(1).delay(1 second) race Http.succeed(2).delay(2 second) + val program = http(()) <& TestClock.adjust(5 second) + assertZIO(program)(equalTo(1)) + } + }, + suite("attempt") { + suite("failure") { + test("fails with a throwable") { + val throwable = new Throwable("boom") + val actual = Http.attempt(throw throwable).execute(()) + assert(actual)(isFailure(equalTo(throwable))) + } } + - suite("attempt") { - suite("failure") { - test("fails with a throwable") { - val throwable = new Throwable("boom") - val actual = Http.attempt(throw throwable).execute(()) - assert(actual)(isFailure(equalTo(throwable))) - } - } + - suite("success") { - test("succeeds with a value") { - val actual = Http.attempt("bar").execute(()) - assert(actual)(isSuccess(equalTo("bar"))) - } + suite("success") { + test("succeeds with a value") { + val actual = Http.attempt("bar").execute(()) + assert(actual)(isSuccess(equalTo("bar"))) } + } + }, + suite("when")( + test("should execute http only when condition applies") { + val app = Http.succeed(1).when((_: Any) => true) + val actual = app.execute(0) + assert(actual)(isSuccess(equalTo(1))) } + - suite("when")( - test("should execute http only when condition applies") { - val app = Http.succeed(1).when((_: Any) => true) + test("should not execute http when condition doesn't apply") { + val app = Http.succeed(1).when((_: Any) => false) val actual = app.execute(0) - assert(actual)(isSuccess(equalTo(1))) + assert(actual)(isEmpty) } + - test("should not execute http when condition doesn't apply") { - val app = Http.succeed(1).when((_: Any) => false) - val actual = app.execute(0) - assert(actual)(isEmpty) - } + - test("should die when condition throws an exception") { - val t = new Throwable("boom") - val app = Http.succeed(1).when((_: Any) => throw t) - val actual = app.execute(0) - assert(actual)(isDie(equalTo(t))) - }, - ) + - suite("catchSome") { - test("catches matching exception") { - val http = + test("should die when condition throws an exception") { + val t = new Throwable("boom") + val app = Http.succeed(1).when((_: Any) => throw t) + val actual = app.execute(0) + assert(actual)(isDie(equalTo(t))) + }, + ), + suite("catchSome") { + test("catches matching exception") { + val http = + Http + .fail(new IllegalArgumentException("boom")) + .catchSome { case _: IllegalArgumentException => + Http.succeed("bar") + } + assert(http.execute {})(isSuccess(equalTo("bar"))) + } + + test("keeps an error if doesn't catch anything") { + val exception = new Throwable("boom") + val http = Http - .fail(new IllegalArgumentException("boom")) - .catchSome { case _: IllegalArgumentException => + .fail(exception) + .catchSome { case _: ArithmeticException => Http.succeed("bar") } - assert(http.execute {})(isSuccess(equalTo("bar"))) + assert(http.execute {})(isFailure(equalTo(exception))) } + - test("keeps an error if doesn't catch anything") { - val exception = new Throwable("boom") - val http = - Http - .fail(exception) - .catchSome { case _: ArithmeticException => - Http.succeed("bar") - } - assert(http.execute {})(isFailure(equalTo(exception))) - } + - test("doesn't affect the success") { - val http = - (Http.succeed("bar"): Http[Any, Throwable, Any, String]).catchSome { case _: Throwable => - Http.succeed("baz") - } - assert(http.execute {})(isSuccess(equalTo("bar"))) - } - } + - suite("refineOrDie") { - test("refines matching exception") { + test("doesn't affect the success") { val http = - Http.fail(new IllegalArgumentException("boom")).refineOrDie { case _: IllegalArgumentException => - "fail" + (Http.succeed("bar"): Http[Any, Throwable, Any, String]).catchSome { case _: Throwable => + Http.succeed("baz") } - assert(http.execute {})(isFailure(equalTo("fail"))) - } + - test("dies if doesn't catch anything") { - val t = new Throwable("boom") - val http = - Http - .fail(t) - .refineOrDie { case _: IllegalArgumentException => - "fail" - } - assert(http.execute {})(isDie(equalTo(t))) - } + - test("doesn't affect the success") { - val http = - (Http.succeed("bar"): Http[Any, Throwable, Any, String]).refineOrDie { case _: Throwable => - Http.succeed("baz") - } - assert(http.execute {})(isSuccess(equalTo("bar"))) + assert(http.execute {})(isSuccess(equalTo("bar"))) + } + }, + suite("refineOrDie") { + test("refines matching exception") { + val http = + Http.fail(new IllegalArgumentException("boom")).refineOrDie { case _: IllegalArgumentException => + "fail" } + assert(http.execute {})(isFailure(equalTo("fail"))) } + - suite("orDie")( - test("dies on failure") { + test("dies if doesn't catch anything") { val t = new Throwable("boom") val http = - Http.fail(t).orDie + Http + .fail(t) + .refineOrDie { case _: IllegalArgumentException => + "fail" + } assert(http.execute {})(isDie(equalTo(t))) - }, + } + test("doesn't affect the success") { val http = - (Http.succeed("bar"): Http[Any, Throwable, Any, String]).orDie + (Http.succeed("bar"): Http[Any, Throwable, Any, String]).refineOrDie { case _: Throwable => + Http.succeed("baz") + } assert(http.execute {})(isSuccess(equalTo("bar"))) - }, - ) + - suite("catchSomeDefect") { - test("catches defect") { - val t = new IllegalArgumentException("boom") - val http = Http.die(t).catchSomeDefect { case _: IllegalArgumentException => Http.succeed("OK") } - assert(http.execute {})(isSuccess(equalTo("OK"))) - } + - test("catches thrown defects") { - val http = Http - .collect[Any] { case _ => throw new IllegalArgumentException("boom") } - .catchSomeDefect { case _: IllegalArgumentException => Http.succeed("OK") } - assert(http.execute {})(isSuccess(equalTo("OK"))) - } + - test("propagates non-caught defect") { - val t = new IllegalArgumentException("boom") - val http = Http.die(t).catchSomeDefect { case _: SecurityException => Http.succeed("OK") } - assert(http.execute {})(isDie(equalTo(t))) - } + } + }, + suite("orDie")( + test("dies on failure") { + val t = new Throwable("boom") + val http = + Http.fail(t).orDie + assert(http.execute {})(isDie(equalTo(t))) + }, + test("doesn't affect the success") { + val http = + (Http.succeed("bar"): Http[Any, Throwable, Any, String]).orDie + assert(http.execute {})(isSuccess(equalTo("bar"))) + }, + ), + suite("catchSomeDefect") { + test("catches defect") { + val t = new IllegalArgumentException("boom") + val http = Http.die(t).catchSomeDefect { case _: IllegalArgumentException => Http.succeed("OK") } + assert(http.execute {})(isSuccess(equalTo("OK"))) } + - suite("catchNonFatalOrDie") { - test("catches non-fatal exception") { - val t = new IllegalArgumentException("boom") - val http = Http.fail(t).catchNonFatalOrDie { _ => Http.succeed("OK") } + test("catches thrown defects") { + val http = Http + .collect[Any] { case _ => throw new IllegalArgumentException("boom") } + .catchSomeDefect { case _: IllegalArgumentException => Http.succeed("OK") } assert(http.execute {})(isSuccess(equalTo("OK"))) } + - test("dies with fatal exception") { - val t = new OutOfMemoryError() - val http = Http.fail(t).catchNonFatalOrDie { case _ => Http.succeed("OK") } - assert(http.execute {})(isDie(equalTo(t))) - } + test("propagates non-caught defect") { + val t = new IllegalArgumentException("boom") + val http = Http.die(t).catchSomeDefect { case _: SecurityException => Http.succeed("OK") } + assert(http.execute {})(isDie(equalTo(t))) + } + }, + suite("catchNonFatalOrDie") { + test("catches non-fatal exception") { + val t = new IllegalArgumentException("boom") + val http = Http.fail(t).catchNonFatalOrDie { _ => Http.succeed("OK") } + assert(http.execute {})(isSuccess(equalTo("OK"))) + } + + test("dies with fatal exception") { + val t = new OutOfMemoryError() + val http = Http.fail(t).catchNonFatalOrDie { case _ => Http.succeed("OK") } + assert(http.execute {})(isDie(equalTo(t))) + } + }, + suite("merge")( + test("merges error into success") { + val http = Http.fail(1).merge + assert(http.execute {})(isSuccess(equalTo(1))) }, + ), ) @@ timeout(10 seconds) } diff --git a/zio-http/src/test/scala/zhttp/http/MiddlewareSpec.scala b/zio-http/src/test/scala/zhttp/http/MiddlewareSpec.scala index 07ef7daf8..ca2e89681 100644 --- a/zio-http/src/test/scala/zhttp/http/MiddlewareSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/MiddlewareSpec.scala @@ -7,11 +7,21 @@ import zio.test.{TestClock, TestConsole, ZIOSpecDefault, assert, assertZIO} object MiddlewareSpec extends ZIOSpecDefault with HExitAssertion { def spec = suite("Middleware") { val increment = Middleware.codec[Int, Int](decoder = a => Right(a + 1), encoder = b => Right(b + 1)) - test("empty") { + test("identity") { val http = Http.empty val app = Middleware.identity(http) assertZIO(app(()).either)(isLeft(isNone)) } + + test("identity - 2") { + val http = Http.succeed(1) + val app = Middleware.identity[Unit, Int](http) + assertZIO(app(()))(equalTo(1)) + } + + test("empty") { + val mid = Middleware.empty + val app = Http.succeed(1) @@ mid + assertZIO(app(()).either)(isLeft(isNone)) + } + test("constant") { val mid = Middleware.fromHttp(Http.succeed("OK")) val app = Http.succeed(1) @@ mid @@ -54,17 +64,17 @@ object MiddlewareSpec extends ZIOSpecDefault with HExitAssertion { assertZIO(app(0))(equalTo(3)) } + test("runBefore") { - val mid = Middleware.identity.runBefore(Console.printLine("A")) + val mid = Middleware.identity[Any, Unit].runBefore(Console.printLine("A")) val app = Http.fromZIO(Console.printLine("B")) @@ mid assertZIO(app(()) *> TestConsole.output)(equalTo(Vector("A\n", "B\n"))) } + test("runAfter") { - val mid = Middleware.identity.runAfter(Console.printLine("B")) + val mid = Middleware.identity[Any, Unit].runAfter(Console.printLine("B")) val app = Http.fromZIO(Console.printLine("A")) @@ mid assertZIO(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 mid = Middleware.identity[Any, Unit].runBefore(Console.printLine("A")).runAfter(Console.printLine("C")) val app = Http.fromZIO(Console.printLine("B")) @@ mid assertZIO(app(()) *> TestConsole.output)(equalTo(Vector("A\n", "B\n", "C\n"))) } + @@ -113,24 +123,30 @@ object MiddlewareSpec extends ZIOSpecDefault with HExitAssertion { } } + suite("when") { - val mid = Middleware.succeed(0) + val mid = Middleware.transform[Int, Int]( + in = _ + 1, + out = _ + 1, + ) test("condition is true") { - val app = Http.identity[Int] @@ mid.when[Int](_ => true) - assertZIO(app(10))(equalTo(0)) + val app = Http.identity[Int] @@ mid.when((_: Any) => true) + assertZIO(app(10))(equalTo(12)) } + test("condition is false") { - val app = Http.identity[Int] @@ mid.when[Int](_ => false) + val app = Http.identity[Int] @@ mid.when((_: Any) => false) assertZIO(app(1))(equalTo(1)) } } + suite("whenZIO") { - val mid = Middleware.succeed(0) + val mid = Middleware.transform[Int, Int]( + in = _ + 1, + out = _ + 1, + ) test("condition is true") { - val app = Http.identity[Int] @@ mid.whenZIO[Any, Nothing, Int](_ => ZIO.succeed(true)) - assertZIO(app(10))(equalTo(0)) + val app = Http.identity[Int] @@ mid.whenZIO((_: Any) => ZIO.succeed(true)) + assertZIO(app(10))(equalTo(12)) } + test("condition is false") { - val app = Http.identity[Int] @@ mid.whenZIO[Any, Nothing, Int](_ => ZIO.succeed(false)) + val app = Http.identity[Int] @@ mid.whenZIO((_: Any) => ZIO.succeed(false)) assertZIO(app(1))(equalTo(1)) } } + @@ -151,6 +167,22 @@ object MiddlewareSpec extends ZIOSpecDefault with HExitAssertion { assertZIO(app("1").exit)(fails(anything)) } } + + test("allow") { + val mid = Middleware.allow[Int, Int](_ > 4) + val app = Http.succeed(1) @@ mid + for { + test1 <- assertZIO(app(1).either)(isLeft(isNone)) + test2 <- assertZIO(app(6))(equalTo(1)) + } yield test1 && test2 + } + + test("allowZIO") { + val mid = Middleware.allowZIO[Int, Int](x => ZIO.succeed(x > 4)) + val app = Http.succeed(1) @@ mid + for { + test1 <- assertZIO(app(1).either)(isLeft(isNone)) + test2 <- assertZIO(app(6))(equalTo(1)) + } yield test1 && test2 + } + suite("codecHttp") { test("codec success") { val a = Http.collect[Int] { case v => v.toString } @@ -170,5 +202,5 @@ object MiddlewareSpec extends ZIOSpecDefault with HExitAssertion { assertZIO(app("2").exit)(fails(anything)) } } - } + }.provide(Clock.live) } diff --git a/zio-http/src/test/scala/zhttp/http/PathSpec.scala b/zio-http/src/test/scala/zhttp/http/PathSpec.scala index 96f30c58a..7160d5946 100644 --- a/zio-http/src/test/scala/zhttp/http/PathSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/PathSpec.scala @@ -1,5 +1,6 @@ package zhttp.http +import zhttp.internal.HttpGen import zio.test.Assertion._ import zio.test._ @@ -7,133 +8,299 @@ object PathSpec extends ZIOSpecDefault with HExitAssertion { def collect[A](pf: PartialFunction[Path, A]): Path => Option[A] = path => pf.lift(path) def spec = suite("Path")( - suite("toList")( - test("empty")(assert(Path().toList)(equalTo(Nil))) + - test("empty string")(assert(Path("").toList)(equalTo(Nil))) + - test("just /")(assert(Path("/").toList)(equalTo(Nil))) + - test("un-prefixed")(assert(Path("A").toList)(equalTo(List("A")))) + - test("prefixed")(assert(Path("/A").toList)(equalTo(List("A")))) + - test("nested paths")(assert(Path("A", "B", "C").toList)(equalTo(List("A", "B", "C")))) + - test("encoding string")(assert(Path("A", "B%2FC").toList)(equalTo(List("A", "B%2FC")))), - ) + - suite("apply()")( - test("empty")(assert(Path())(equalTo(!!))) + - test("empty string")(assert(Path(""))(equalTo(!!))) + - test("just /")(assert(Path("/"))(equalTo(!!))) + - test("prefixed path")(assert(Path("/A"))(equalTo(Path("A")))) + - test("encoded paths")(assert(Path("/A/B%2FC"))(equalTo(Path("A", "B%2FC")))) + - test("nested paths")(assert(Path("/A/B/C"))(equalTo(Path("A", "B", "C")))), - ) + - suite("unapplySeq")( - test("a, b, c") { - val path = collect { case Path(a, b, c) => (a, b, c) } - assert(path(Path("a", "b", "c")))(isSome(equalTo(("a", "b", "c")))) + suite("Syntax")( + suite("/")( + test("isDefined") { + + val gen = Gen.elements( + // Exact + collect { case !! => true } -> !!, + collect { case !! / "a" => true } -> !! / "a", + collect { case !! / "a" => true } -> !! / "a", + collect { case !! / "a" / "b" => true } -> !! / "a" / "b", + collect { case !! / "a" / "b" / "c" => true } -> !! / "a" / "b" / "c", + + // Wildcards + collect { case !! / _ => true } -> !! / "a", + collect { case !! / _ / _ => true } -> !! / "a" / "b", + collect { case !! / _ / _ / _ => true } -> !! / "a" / "b" / "c", + + // Wildcard mix + collect { case _ / "c" => true } -> !! / "a" / "b" / "c", + collect { case _ / _ / "c" => true } -> !! / "a" / "b" / "c", + ) + + checkAll(gen) { case (pf, path) => + assertTrue(pf(path).isDefined) + } }, - ) + - suite("asString")( - test("a, b, c") { - val path = Path("a", "b", "c").encode - assert(path)(equalTo("/a/b/c")) - } + - test("Path()") { - val path = Path().encode - assert(path)(equalTo("/")) - } + - test("!!") { - val path = !!.encode - assert(path)(equalTo("/")) - }, - ) + - suite("PathSyntax /")( - test("construction") { - val path = !! / "a" / "b" / "c" - assert(path)(equalTo(Path("a", "b", "c"))) - } + - test("extract path / a / b / c") { - val path = collect { case !! / "a" / b / c => (b, c) } - assert(path(Path("a", "b", "c")))(isSome(equalTo(("b", "c")))) - } + - test("extract path / a / b / c") { - val path = collect { case !! / "a" / b => b } - assert(path(Path("a", "b", "c")))(isNone) - } + - test("extract path / a / b / c") { - val path = collect { case !! / "a" / b => b } - assert(path(Path("a", "b")))(isSome(equalTo("b"))) - }, - ) + - suite("PathSyntax /:")( - test("construction") { - val path = "a" /: "b" /: "c" /: !! - assert(path)(equalTo(Path("a", "b", "c"))) - } + - suite("default")( - test("extract path 'name' /: name") { - val path = collect { case "name" /: name => name.encode } - assert(path(Path("name", "a", "b", "c")))(isSome(equalTo("/a/b/c"))) - } + - test("extract paths 'name' /: a /: b /: 'c' /: !!") { - val path = collect { case "name" /: a /: b /: "c" /: !! => (a, b) } - assert(path(Path("name", "a", "b", "c")))(isSome(equalTo(("a", "b")))) - } + - test("extract paths 'name' /: a /: b /: _") { - val path = collect { case "name" /: a /: b /: _ => (a, b) } - assert(path(Path("name", "a", "b", "c")))(isSome(equalTo(("a", "b")))) - } + - test("extract paths 'name' /: name /: 'a' /: 'b' /: 'c' /: !!") { - val path = collect { case "name" /: name /: "a" /: "b" /: "c" /: !! => name.toString } - assert(path(Path("name", "Xyz", "a", "b", "c")))(isSome(equalTo("Xyz"))) - }, - ) + - suite("int()")( - test("extract path 'user' /: int(1)") { - val path = collect { case "user" /: int(age) /: !! => age } - assert(path(Path("user", "1")))(isSome(equalTo(1))) - } + - test("extract path 'user' /: int(Xyz)") { - val path = collect { case "user" /: int(age) /: !! => age } - assert(path(Path("user", "Xyz")))(isNone) - }, - ) + - suite("boolean()")( - test("extract path 'user' /: boolean(true)") { - val path = collect { case "user" /: boolean(ok) /: !! => ok } - assert(path(Path("user", "True")))(isSome(isTrue)) - } + - test("extract path 'user' /: boolean(false)") { - val path = collect { case "user" /: boolean(ok) /: !! => ok } - assert(path(Path("user", "false")))(isSome(isFalse)) - }, - ), - ) + - suite("startsWith")( - test("isTrue") { - assert(!! / "a" / "b" / "c" / "d" startsWith !! / "a")(isTrue) && - assert(!! / "a" / "b" / "c" / "d" startsWith !! / "a" / "b")(isTrue) && - assert(!! / "a" / "b" / "c" / "d" startsWith !! / "a" / "b" / "c")(isTrue) && - assert(!! / "a" / "b" / "c" / "d" startsWith !! / "a" / "b" / "c" / "d")(isTrue) - } + - test("isFalse") { - assert(!! / "a" / "b" / "c" / "d" startsWith !! / "a" / "b" / "c" / "d" / "e")(isFalse) && - assert(!! / "a" / "b" / "c" startsWith !! / "a" / "b" / "c" / "d")(isFalse) && - assert(!! / "a" / "b" startsWith !! / "a" / "b" / "c")(isFalse) && - assert(!! / "a" startsWith !! / "a" / "b")(isFalse) - } + - test("isFalse") { - assert(!! / "abcd" startsWith !! / "a")(isFalse) - }, - ) + - test("drop") { - assert(!! / "a" / "b" / "c" drop 1)(equalTo(!! / "b" / "c")) && - assert(!! drop 1)(equalTo(!!)) - } + - test("dropLast") { - assert(!! / "a" / "b" / "c" dropLast 1)(equalTo(!! / "a" / "b")) && - assert(!! dropLast 1)(equalTo(!!)) - } + - test("take") { - assert(!! / "a" / "b" / "c" take 1)(equalTo(!! / "a")) && - assert(!! take 1)(equalTo(!!)) + test("isEmpty") { + val gen = Gen.elements( + collect { case !! => true } -> !! / "a", + collect { case !! / "a" => true } -> !! / "b", + collect { case !! / "a" / "b" => true } -> !! / "a", + collect { case !! / "a" / "b" / "c" => true } -> !! / "a" / "b", + ) + + checkAll(gen) { case (pf, path) => + assertTrue(pf(path).isEmpty) + } + }, + ), + suite("/:")( + test("isDefined") { + val gen = Gen.elements( + // Exact + collect { case "a" /: !! => true } -> "a" /: !!, + collect { case "a" /: "b" /: !! => true } -> "a" /: "b" /: !!, + collect { case "a" /: "b" /: "c" /: !! => true } -> "a" /: "b" /: "c" /: !!, + + // Wildcard + collect { case "a" /: _ => true } -> "a" /: !!, + collect { case "a" /: "b" /: _ => true } -> "a" /: "b" /: !!, + collect { case "a" /: _ /: _ => true } -> "a" /: "b" /: !!, + collect { case "a" /: _ => true } -> "a" /: "b" /: !!, + + // + collect { case "a" /: "b" /: "c" /: _ => true } -> "a" /: "b" /: "c" /: !!, + collect { case "a" /: "b" /: _ /: _ => true } -> "a" /: "b" /: "c" /: !!, + collect { case "a" /: _ /: _ /: _ => true } -> "a" /: "b" /: "c" /: !!, + collect { case _ /: _ /: _ /: _ => true } -> "a" /: "b" /: "c" /: !!, + collect { case _ /: _ /: _ => true } -> "a" /: "b" /: "c" /: !!, + collect { case _ /: _ => true } -> "a" /: "b" /: "c" /: !!, + ) + + checkAll(gen) { case (pf, path) => + assertTrue(pf(path).isDefined) + } + }, + test("isEmpty") { + val gen = Gen.elements( + collect { case "a" /: !! => true } -> "b" /: !!, + collect { case "a" /: "b" /: !! => true } -> "a" /: !!, + collect { case "a" /: "b" /: "c" /: !! => true } -> "a" /: "b" /: !!, + ) + + checkAll(gen) { case (pf, path) => + assertTrue(pf(path).isEmpty) + } + }, + ), + ), + suite("int()")( + test("extract path 'user' /: int(1)") { + val path = collect { case "user" /: int(age) /: !! => age } + assert(path(Path.decode("/user/1")))(isSome(equalTo(1))) + }, + test("extract path 'user' /: int(Xyz)") { + val path = collect { case "user" /: int(age) /: !! => age } + assert(path(Path.decode("/user/Xyz")))(isNone) + }, + ), + suite("boolean()")( + test("extract path 'user' /: boolean(true)") { + val path = collect { case "user" /: boolean(ok) /: !! => ok } + assert(path(Path.decode("/user/True")))(isSome(isTrue)) + }, + test("extract path 'user' /: boolean(false)") { + val path = collect { case "user" /: boolean(ok) /: !! => ok } + assert(path(Path.decode("/user/false")))(isSome(isFalse)) + }, + ), + suite("startsWith")( + test("isTrue") { + val gen = Gen.elements( + !! -> !!, + !! / "a" -> !! / "a", + !! / "a" / "b" -> !! / "a" / "b", + !! / "a" / "b" / "c" -> !! / "a", + !! / "a" / "b" / "c" -> !! / "a" / "b" / "c", + !! / "a" / "b" / "c" -> !! / "a" / "b" / "c", + !! / "a" / "b" / "c" -> !! / "a" / "b" / "c", + ) + + checkAll(gen) { case (path, expected) => + val actual = path.startsWith(expected) + assertTrue(actual) + } + }, + test("isFalse") { + val gen = Gen.elements( + !! -> !! / "a", + !! / "a" -> !! / "a" / "b", + !! / "a" -> !! / "b", + !! / "a" / "b" -> !! / "a" / "b" / "c", + ) + + checkAll(gen) { case (path, expected) => + val actual = !path.startsWith(expected) + assertTrue(actual) + } }, + ), + test("take") { + val gen = Gen.elements( + (1, !!) -> !!, + (1, !! / "a") -> !! / "a", + (1, !! / "a" / "b") -> !! / "a", + (1, !! / "a" / "b" / "c") -> !! / "a", + (2, !! / "a" / "b" / "c") -> !! / "a" / "b", + (3, !! / "a" / "b" / "c") -> !! / "a" / "b" / "c", + (4, !! / "a" / "b" / "c") -> !! / "a" / "b" / "c", + ) + + checkAll(gen) { case ((n, path), expected) => + val actual = path.take(n) + assertTrue(actual == expected) + } + }, + test("drop") { + val gen = Gen.elements( + (1, !!) -> !!, + (1, !! / "a") -> !!, + (1, !! / "a" / "b") -> !! / "b", + (1, !! / "a" / "b" / "c") -> !! / "b" / "c", + (2, !! / "a" / "b" / "c") -> !! / "c", + (3, !! / "a" / "b" / "c") -> !!, + (4, !! / "a" / "b" / "c") -> !!, + ) + + checkAll(gen) { case ((n, path), expected) => + val actual = path.drop(n) + assertTrue(actual == expected) + } + }, + test("dropLast") { + val gen = Gen.elements( + (1, !!) -> !!, + (1, !! / "a") -> !!, + (1, !! / "a" / "b") -> !! / "a", + (1, !! / "a" / "b" / "c") -> !! / "a" / "b", + (2, !! / "a" / "b" / "c") -> !! / "a", + (3, !! / "a" / "b" / "c") -> !!, + (4, !! / "a" / "b" / "c") -> !!, + ) + + checkAll(gen) { case ((n, path), expected) => + val actual = path.dropLast(n) + assertTrue(actual == expected) + } + }, + suite("encode/decode/encode")( + test("anyPath") { + check(HttpGen.path) { path => + val expected = path.encode + val decoded = Path.decode(expected) + + assertTrue(decoded.encode == expected) && + assertTrue(decoded.toString() == expected) + } + }, + test("is symmetric") { + check(HttpGen.path) { path => + val expected = path.encode + val actual = Path.decode(expected) + assertTrue(actual == path) + } + }, + test("string elements") { + val gen = Gen.elements( + // basic + "", + "/", + + // with leading slash + "/a", + "/a/b", + "/a/b/c", + + // without leading slash + "a", + "a/b", + "a/b/c", + + // with trailing slash + "a/", + "a/b/", + "a/b/c/", + + // with leading & trailing slash + "/a/", + "/a/b/", + "/a/b/c/", + + // with encoded chars + "a/b%2Fc", + "/a/b%2Fc", + "a/b%2Fc/", + "/a/b%2Fc/", + ) + + checkAll(gen) { path => + val actual = path + val expected = Path.decode(actual).encode + assertTrue(actual == expected) + } + }, + test("path elements") { + val gen = Gen.elements( + !!, + !! / "a", + !! / "a" / "b", + !! / "a" / "b" / "c", + !! / "a" / "b" / "c" / "", + !! / "a" / "b" / "c" / "" / "", + !! / "a" / "b" / "c" / "" / "" / "", + "a" /: !!, + "a" /: "b" /: !!, + "a" /: "b" /: "c" /: !!, + "a" /: "b" /: "c" /: "" /: !!, + "a" /: "b" /: "c" /: "" /: "" /: !!, + "a" /: "b" /: "c" /: "" /: "" /: "" /: !!, + ) + + checkAll(gen) { path => + val actual = path.encode + val expected = Path.decode(actual).encode + assertTrue(actual == expected) + } + }, + ), + suite("decode")( + suite("trailingSlash")( + test("isTrue") { + val gen = Gen.elements( + "a/", + "a/b/", + "a/b/c/", + "/a/", + "/a/b/", + "/a/b/c/", + ) + + checkAll(gen) { path => + val actual = Path.decode(path) + assertTrue(actual.trailingSlash) + } + }, + test("isFalse") { + val gen = Gen.elements( + "", + "//", + "a", + "a/b", + "a/b/c", + "/a", + "/a/b", + "/a/b/c", + ) + + checkAll(gen) { path => + val actual = Path.decode(path) + assertTrue(!actual.trailingSlash) + } + }, + ), + ), ) } diff --git a/zio-http/src/test/scala/zhttp/http/RequestSpec.scala b/zio-http/src/test/scala/zhttp/http/RequestSpec.scala index 63cd82cf4..0c3864f64 100644 --- a/zio-http/src/test/scala/zhttp/http/RequestSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/RequestSpec.scala @@ -10,19 +10,10 @@ object RequestSpec extends ZIOSpecDefault { test("should produce string representation of a request") { check(HttpGen.request) { req => assert(req.toString)( - equalTo(s"Request(${req.version}, ${req.method}, ${req.url}, ${req.headers}, ${req.remoteAddress})"), + equalTo(s"Request(${req.version}, ${req.method}, ${req.url}, ${req.headers})"), ) } - } + - test("should produce string representation of a parameterized request") { - check(HttpGen.parameterizedRequest(Gen.alphaNumericString)) { req => - assert(req.toString)( - equalTo( - s"ParameterizedRequest(Request(${req.version}, ${req.method}, ${req.url}, ${req.headers}, ${req.remoteAddress}), ${req.params})", - ), - ) - } - } + } }, ) } diff --git a/zio-http/src/test/scala/zhttp/http/URLSpec.scala b/zio-http/src/test/scala/zhttp/http/URLSpec.scala index b38afac30..35510cb22 100644 --- a/zio-http/src/test/scala/zhttp/http/URLSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/URLSpec.scala @@ -51,10 +51,10 @@ object URLSpec extends ZIOSpecDefault { suite("asString")( test("using gen") { - checkAll(HttpGen.url) { case url => + check(HttpGen.url) { case url => val source = url.encode - val decoded = URL.fromString(source).map(_.encode) - assert(decoded)(isRight(equalTo(source))) + val decoded = URL.fromString(source) + assert(decoded.map(_.isEqual(url)))(isRight(equalTo(true))) } } + test("empty") { @@ -96,7 +96,7 @@ object URLSpec extends ZIOSpecDefault { val expected = URL( - Path("/list/users"), + Path.decode("/list/users"), URL.Location.Relative, Map("user_id" -> List("1", "2"), "order" -> List("ASC"), "text" -> List("zio-http is awesome!")), ) @@ -118,7 +118,7 @@ object URLSpec extends ZIOSpecDefault { }, test("returns relative URL if port, host, and scheme are not set") { val builderUrl = URL.empty - .setPath(Path("/list")) + .setPath(Path.decode("/list")) .setQueryParams( Map("type" -> List("builder"), "query" -> List("provided")), ) diff --git a/zio-http/src/test/scala/zhttp/http/middleware/WebSpec.scala b/zio-http/src/test/scala/zhttp/http/middleware/WebSpec.scala index a7fe9e97a..52f21c5cd 100644 --- a/zio-http/src/test/scala/zhttp/http/middleware/WebSpec.scala +++ b/zio-http/src/test/scala/zhttp/http/middleware/WebSpec.scala @@ -163,7 +163,7 @@ object WebSpec extends ZIOSpecDefault with HttpAppTestExtensions { self => assertZIO(app(Request()))(contains("ValueA")) } } - } + }.provide(Console.live ++ Clock.live) private def cond(flg: Boolean) = (_: Any) => flg diff --git a/zio-http/src/test/scala/zhttp/internal/HttpGen.scala b/zio-http/src/test/scala/zhttp/internal/HttpGen.scala index 6a05a78a8..e34332167 100644 --- a/zio-http/src/test/scala/zhttp/internal/HttpGen.scala +++ b/zio-http/src/test/scala/zhttp/internal/HttpGen.scala @@ -1,7 +1,6 @@ package zhttp.internal import io.netty.buffer.Unpooled -import zhttp.http.Request.ParameterizedRequest import zhttp.http.Scheme.{HTTP, HTTPS, WS, WSS} import zhttp.http.URL.Location import zhttp.http._ @@ -118,19 +117,10 @@ object HttpGen { ) } yield cnt - def path: Gen[Sized, Path] = { - for { - l <- Gen.listOf(Gen.alphaNumericString) - p <- Gen.const(Path(l)) - } yield p - } - - def parameterizedRequest[R, A](paramsGen: Gen[R, A]): Gen[R with Sized, ParameterizedRequest[A]] = { - for { - req <- request - params <- paramsGen - } yield ParameterizedRequest(req, params) - } + def path: Gen[Sized, Path] = for { + segments <- Gen.listOf(Gen.alphaNumericStringBounded(1, 5)) + trailingSlash <- Gen.boolean + } yield Path(segments.toVector, trailingSlash) def request: Gen[Sized, Request] = for { version <- httpVersion @@ -138,7 +128,7 @@ object HttpGen { url <- HttpGen.url headers <- Gen.listOf(HttpGen.header).map(Headers(_)) data <- HttpGen.httpData(Gen.listOf(Gen.alphaNumericString)) - } yield Request(version, method, url, headers, None, data) + } yield Request(version, method, url, headers, data) def response[R](gContent: Gen[R, List[String]]): Gen[Sized with R, Response] = { for { diff --git a/zio-http/src/test/scala/zhttp/internal/HttpRunnableSpec.scala b/zio-http/src/test/scala/zhttp/internal/HttpRunnableSpec.scala index ca5beb995..08c29c732 100644 --- a/zio-http/src/test/scala/zhttp/internal/HttpRunnableSpec.scala +++ b/zio-http/src/test/scala/zhttp/internal/HttpRunnableSpec.scala @@ -76,10 +76,13 @@ abstract class HttpRunnableSpec extends ZIOSpecDefault { self => id <- Http.fromZIO(DynamicServer.deploy(app)) url <- Http.fromZIO(DynamicServer.wsURL) response <- Http.fromFunctionZIO[SocketApp[HttpEnv]] { app => - Client.socket( - url = url, - headers = Headers(DynamicServer.APP_ID, id), - app = app, + ZIO.scoped[HttpEnv]( + Client + .socket( + url = url, + headers = Headers(DynamicServer.APP_ID, id), + app = app, + ), ) } } yield response diff --git a/zio-http/src/test/scala/zhttp/service/ClientSpec.scala b/zio-http/src/test/scala/zhttp/service/ClientSpec.scala index b6d515ec7..00493cbcc 100644 --- a/zio-http/src/test/scala/zhttp/service/ClientSpec.scala +++ b/zio-http/src/test/scala/zhttp/service/ClientSpec.scala @@ -30,10 +30,10 @@ object ClientSpec extends HttpRunnableSpec { val res = app.deploy.bodyAsString.run(method = Method.POST, content = HttpData.fromString("ZIO user")) assertZIO(res)(equalTo("ZIO user")) } + - test("empty content") { + test("non empty content") { val app = Http.empty - val responseContent = app.deploy.body.run() - assertZIO(responseContent)(isEmpty) + val responseContent = app.deploy.body.run().map(_.length) + assertZIO(responseContent)(isGreaterThan(0)) } + test("text content") { val app = Http.text("zio user does not exist") diff --git a/zio-http/src/test/scala/zhttp/service/RequestStreamingServerSpec.scala b/zio-http/src/test/scala/zhttp/service/RequestStreamingServerSpec.scala new file mode 100644 index 000000000..fba23143e --- /dev/null +++ b/zio-http/src/test/scala/zhttp/service/RequestStreamingServerSpec.scala @@ -0,0 +1,49 @@ +package zhttp.service +import zhttp.http._ +import zhttp.internal.{DynamicServer, HttpRunnableSpec} +import zhttp.service.ServerSpec.requestBodySpec +import zio.test.Assertion.equalTo +import zio.test.TestAspect.{sequential, timeout} +import zio.test._ +import zio.{ZIO, durationInt} + +object RequestStreamingServerSpec extends HttpRunnableSpec { + private val env = + EventLoopGroup.nio() ++ ChannelFactory.nio ++ zhttp.service.server.ServerChannelFactory.nio ++ DynamicServer.live + + private val appWithReqStreaming = serve(DynamicServer.app, Some(Server.enableObjectAggregator(-1))) + + /** + * Generates a string of the provided length and char. + */ + private def genString(size: Int, char: Char): String = { + val buffer = new Array[Char](size) + for (i <- 0 until size) buffer(i) = char + new String(buffer) + } + + def largeContentSpec = suite("ServerStreamingSpec") { + test("test unsafe large content") { + val size = 1024 * 1024 + val content = genString(size, '?') + + val app = Http.fromFunctionZIO[Request] { + _.bodyAsStream.runCount.map(bytesCount => { + Response.text(bytesCount.toString) + }) + } + + val res = app.deploy.bodyAsString.run(content = HttpData.fromString(content)) + + assertZIO(res)(equalTo(size.toString)) + + } + } + + override def spec = + suite("RequestStreamingServerSpec") { + val spec = requestBodySpec + largeContentSpec + suite("app with request streaming") { ZIO.scoped(appWithReqStreaming.as(List(spec))) } + }.provideCustomLayerShared(env) @@ timeout(10 seconds) @@ sequential + +} diff --git a/zio-http/src/test/scala/zhttp/service/SSLSpec.scala b/zio-http/src/test/scala/zhttp/service/SSLSpec.scala index a1d79e72d..6cae988cf 100644 --- a/zio-http/src/test/scala/zhttp/service/SSLSpec.scala +++ b/zio-http/src/test/scala/zhttp/service/SSLSpec.scala @@ -9,7 +9,7 @@ import zhttp.service.server._ import zio._ import zio.test.Assertion.equalTo import zio.test.TestAspect.{ignore, timeout} -import zio.test.{Gen, TestEnvironment, ZIOSpecDefault, assertZIO, checkAll} +import zio.test.{Gen, TestEnvironment, ZIOSpecDefault, assertZIO, check} object SSLSpec extends ZIOSpecDefault { val env = EventLoopGroup.auto() ++ ChannelFactory.auto ++ ServerChannelFactory.auto ++ Scope.default @@ -60,6 +60,12 @@ object SSLSpec extends ZIOSpecDefault { .map(_.status) assertZIO(actual)(equalTo(Status.Ok)) } + + test("Https Redirect when client makes http request") { + val actual = Client + .request("http://localhost:8073/success", ssl = ClientSSLOptions.CustomSSL(clientSSL1)) + .map(_.status) + assertZIO(actual)(equalTo(Status.Ok)) + } + test("Https Redirect when client makes http request") { val actual = Client .request("http://localhost:8073/success", ssl = ClientSSLOptions.CustomSSL(clientSSL1)) @@ -67,7 +73,7 @@ object SSLSpec extends ZIOSpecDefault { assertZIO(actual)(equalTo(Status.PermanentRedirect)) } + test("Https request with a large payload should respond with 413") { - checkAll(payload) { payload => + check(payload) { payload => val actual = Client .request( "https://localhost:8073/text", diff --git a/zio-http/src/test/scala/zhttp/service/ServerSpec.scala b/zio-http/src/test/scala/zhttp/service/ServerSpec.scala index 461a28515..62edaf92b 100644 --- a/zio-http/src/test/scala/zhttp/service/ServerSpec.scala +++ b/zio-http/src/test/scala/zhttp/service/ServerSpec.scala @@ -5,11 +5,11 @@ import zhttp.html._ import zhttp.http._ import zhttp.internal.{DynamicServer, HttpGen, HttpRunnableSpec} import zhttp.service.server._ +import zio._ import zio.stream.{ZPipeline, ZStream} import zio.test.Assertion._ import zio.test.TestAspect._ import zio.test._ -import zio.{Chunk, ZIO, durationInt} import java.nio.file.Paths @@ -23,9 +23,10 @@ object ServerSpec extends HttpRunnableSpec { private val env = EventLoopGroup.nio() ++ ChannelFactory.nio ++ ServerChannelFactory.nio ++ DynamicServer.live + private val MaxSize = 1024 * 10 private val app = - serve(DynamicServer.app, Some(Server.requestDecompression(true) ++ Server.enableObjectAggregator(4096))) - private val appWithReqStreaming = serve(DynamicServer.app, None) + serve(DynamicServer.app, Some(Server.requestDecompression(true) ++ Server.enableObjectAggregator(MaxSize))) + private val appWithReqStreaming = serve(DynamicServer.app, Some(Server.requestDecompression(true))) def dynamicAppSpec = suite("DynamicAppSpec") { suite("success") { @@ -50,7 +51,7 @@ object ServerSpec extends HttpRunnableSpec { } + test("header is set") { val res = app.deploy.headerValue(HeaderNames.contentLength).run() - assertZIO(res)(isSome(equalTo("0"))) + assertZIO(res)(isSome(equalTo("439"))) } } + suite("error") { @@ -83,21 +84,6 @@ object ServerSpec extends HttpRunnableSpec { assertZIO(res)(isSome(anything)) } } + - suite("die") { - val app = Http.die(new Error("SERVER_ERROR")) - test("status is 500") { - val res = app.deploy.status.run() - assertZIO(res)(equalTo(Status.InternalServerError)) - } + - test("content is set") { - val res = app.deploy.bodyAsString.run() - assertZIO(res)(containsString("SERVER_ERROR")) - } + - test("header is set") { - val res = app.deploy.headerValue(HeaderNames.contentLength).run() - assertZIO(res)(isSome(anything)) - } - } + suite("echo content") { val app = Http.collectZIO[Request] { case req => req.bodyAsString.map(text => Response.text(text)) @@ -118,6 +104,12 @@ object ServerSpec extends HttpRunnableSpec { test("one char") { val res = app.deploy.bodyAsString.run(content = HttpData.fromString("1")) assertZIO(res)(equalTo("1")) + } + + test("data") { + val dataStream = ZStream.repeat("A").take(MaxSize.toLong) + val app = Http.collect[Request] { case req => Response(data = req.data) } + val res = app.deploy.bodyAsByteBuf.map(_.readableBytes()).run(content = HttpData.fromStream(dataStream)) + assertZIO(res)(equalTo(MaxSize)) } } + suite("headers") { @@ -166,7 +158,7 @@ object ServerSpec extends HttpRunnableSpec { Response.text(req.contentLength.getOrElse(-1).toString) } test("has content-length") { - checkAll(Gen.alphaNumericString) { string => + check(Gen.alphaNumericString) { string => val res = app.deploy.bodyAsString.run(content = HttpData.fromString(string)) assertZIO(res)(equalTo(string.length.toString)) } @@ -178,9 +170,9 @@ object ServerSpec extends HttpRunnableSpec { } } - def responseSpec = suite("ResponseSpec") { + def responseSpec = suite("ResponseSpec") { test("data") { - checkAll(nonEmptyContent) { case (string, data) => + check(nonEmptyContent) { case (string, data) => val res = Http.fromData(data).deploy.bodyAsString.run() assertZIO(res)(equalTo(string)) } @@ -207,7 +199,7 @@ object ServerSpec extends HttpRunnableSpec { } + test("header") { - checkAll(HttpGen.header) { case header @ (name, value) => + check(HttpGen.header) { case header @ (name, value) => val res = Http.ok.addHeader(header).deploy.headerValue(name).run() assertZIO(res)(isSome(equalTo(value))) } @@ -272,19 +264,20 @@ object ServerSpec extends HttpRunnableSpec { } } } + def requestBodySpec = suite("RequestBodySpec") { test("POST Request stream") { val app: Http[Any, Throwable, Request, Response] = Http.collect[Request] { case req => Response(data = HttpData.fromStream(req.bodyAsStream)) } - checkAll(Gen.alphaNumericString) { c => + check(Gen.alphaNumericString) { c => assertZIO(app.deploy.bodyAsString.run(path = !!, method = Method.POST, content = HttpData.fromString(c)))( equalTo(c), ) } } + test("FromASCIIString: toHttp") { - checkAll(Gen.asciiString) { payload => + check(Gen.asciiString) { payload => val res = HttpData.fromAsciiString(AsciiString.cached(payload)).toHttp.map(_.toString(HTTP_CHARSET)) assertZIO(res.run())(equalTo(payload)) } diff --git a/zio-http/src/test/scala/zhttp/service/WebSocketServerSpec.scala b/zio-http/src/test/scala/zhttp/service/WebSocketServerSpec.scala index 858fe0007..41ec610f8 100644 --- a/zio-http/src/test/scala/zhttp/service/WebSocketServerSpec.scala +++ b/zio-http/src/test/scala/zhttp/service/WebSocketServerSpec.scala @@ -1,12 +1,12 @@ package zhttp.service -import zhttp.http.Status +import zhttp.http.{Http, Status} import zhttp.internal.{DynamicServer, HttpRunnableSpec} import zhttp.service.server._ import zhttp.socket.{Socket, WebSocketFrame} import zio.stream.ZStream import zio.test.Assertion.equalTo -import zio.test.TestAspect.timeout +import zio.test.TestAspect.{nonFlaky, timeout} import zio.test._ import zio.{Chunk, ZIO, _} @@ -45,4 +45,37 @@ object WebSocketServerSpec extends HttpRunnableSpec { assertZIO(app(socket.toSocketApp.onOpen(open)).map(_.status))(equalTo(Status.SwitchingProtocols)) } } + + def websocketOnCloseSpec = suite("WebSocketOnCloseSpec") { + test("success") { + for { + clockEnv <- ZIO.environment[Clock] + + // Maintain a flag to check if the close handler was completed + isSet <- Promise.make[Nothing, Unit] + isStarted <- Promise.make[Nothing, Unit] + + // Setup websocket server + onClose = isStarted.succeed(()) <&> isSet.succeed(()).delay(5 seconds) + serverSocket = Socket.empty.toSocketApp.onClose(_ => onClose) + serverHttp = Http.fromZIO(serverSocket.toResponse).deployWS + + // Setup Client + closeSocket = Socket.end.delay(1 second) + clientSocket = Socket.empty.toSocketApp.onOpen(closeSocket) + + // Deploy the server and send it a socket request + _ <- serverHttp(clientSocket.provideEnvironment(clockEnv)) + + // Wait for the close handler to complete + + _ <- TestClock.adjust(2 seconds) + _ <- isStarted.await + _ <- TestClock.adjust(5 seconds) + _ <- isSet.await + + // Check if the close handler was completed + } yield assertCompletes + } @@ nonFlaky + } } diff --git a/zio-http/src/test/scala/zhttp/socket/SocketSpec.scala b/zio-http/src/test/scala/zhttp/socket/SocketSpec.scala index 6d5b563e8..fc48fa47c 100644 --- a/zio-http/src/test/scala/zhttp/socket/SocketSpec.scala +++ b/zio-http/src/test/scala/zhttp/socket/SocketSpec.scala @@ -11,7 +11,7 @@ object SocketSpec extends ZIOSpecDefault { def spec = suite("SocketSpec") { operationsSpec - } @@ timeout(5 seconds) + }.provide(Clock.live) @@ timeout(5 seconds) def operationsSpec = suite("OperationsSpec") { test("fromStream provide") { @@ -67,6 +67,24 @@ object SocketSpec extends ZIOSpecDefault { test("toHttp") { val http = Socket.succeed(WebSocketFrame.ping).toHttp assertZIO(http(()).map(_.status))(equalTo(Status.SwitchingProtocols)) + } + + test("delay") { + val socket = + Socket.from(1, 2, 3).delay(1.second).mapZIO(i => Clock.instant.map(time => (time.getEpochSecond, i))) + val program = for { + f <- socket(()).runCollect.fork + _ <- TestClock.adjust(10 second) + l <- f.join + } yield l.toList + assertZIO(program)(equalTo(List((1L, 1), (2L, 2), (3L, 3)))) + } + + test("tap") { + val socket = Socket.from(1, 2, 3).tap(i => Console.printLine(i.toString)) + val program = for { + _ <- socket(()).runDrain + l <- TestConsole.output + } yield l + assertZIO(program)(equalTo(Vector("1\n", "2\n", "3\n"))) } } }