diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 000000000..6230a73da --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,31 @@ +name: Scala CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + test: + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + java: ['adopt@1.8', 'adopt@1.11'] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + - name: set up sbt and java + uses: olafurpg/setup-scala@v13 + with: + java-version: ${{ matrix.java }} + - name: Run tests + shell: bash + run: | + JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF8 sbt test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 99aa09c20..ea1f44ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ target/ .classpath local.* +.bsp .bloop .metals .vscode diff --git a/.scalafmt.conf b/.scalafmt.conf index ba81b0ccf..5dc92b992 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,6 @@ -version = 2.7.2 +runner.dialect = scala3 +version = 3.0.3 +docstrings.style = Asterisk maxColumn = 160 align.preset = some rewrite.rules = [ diff --git a/build.sbt b/build.sbt index 6bbdc2ca3..e5af52472 100644 --- a/build.sbt +++ b/build.sbt @@ -3,61 +3,53 @@ import microsites.ExtraMdFileConfig lazy val buildSettings = Seq( organization := "com.github.finagle", - scalaVersion := "2.12.12", - crossScalaVersions := Seq("2.12.12", "2.13.3") + scalaVersion := "3.0.2", + crossScalaVersions := Seq("3.0.2", "2.13.3") ) -lazy val twitterVersion = "20.9.0" -lazy val circeVersion = "0.13.0" -lazy val circeIterateeVersion = "0.13.0-M2" -lazy val circeFs2Version = "0.13.0" -lazy val shapelessVersion = "2.3.3" -lazy val catsVersion = "2.2.0" -lazy val argonautVersion = "6.3.1" -lazy val iterateeVersion = "0.19.0" -lazy val refinedVersion = "0.9.17" -lazy val catsEffectVersion = "2.2.0" -lazy val fs2Version = "2.4.4" +lazy val twitterVersion = "21.6.0" +lazy val circeVersion = "0.14.1" +lazy val circeFs2Version = "0.14.0" +lazy val shapelessVersion = "2.3.7" +lazy val catsVersion = "2.6.1" +lazy val argonautVersion = "6.3.6" +lazy val refinedVersion = "0.9.27" +lazy val catsEffectVersion = "2.5.3" +lazy val fs2Version = "2.5.9" -def compilerOptions(scalaVersion: String): Seq[String] = Seq( - "-deprecation", - "-encoding", "UTF-8", - "-feature", - "-language:existentials", - "-language:higherKinds", - "-language:implicitConversions", - "-unchecked", - "-Ywarn-dead-code", - "-Ywarn-numeric-widen", - "-Xlint" -) ++ (CrossVersion.partialVersion(scalaVersion) match { - case Some((2, scalaMajor)) if scalaMajor == 12 => scala212CompilerOptions - case Some((2, scalaMajor)) if scalaMajor == 13 => scala213CompilerOptions -}) - -lazy val scala212CompilerOptions = Seq( - "-Yno-adapted-args", - "-Ywarn-unused-import", - "-Xfuture" -) - -lazy val scala213CompilerOptions = Seq( - "-Wunused:imports" -) +def compilerOptions(scalaVersion: String): Seq[String] = CrossVersion.partialVersion(scalaVersion) match { + case Some((2, _)) => + Seq( + "-deprecation", + "-encoding", "UTF-8", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-unchecked", + "-Ywarn-dead-code", + "-Ywarn-numeric-widen", + "-Xlint", + "-Wunused:imports" + ) + case Some((3, _)) => + Seq("-Ykind-projector:underscores") + case _ => Seq() +} val testDependencies = Seq( - "org.scalacheck" %% "scalacheck" % "1.14.3", - "org.scalatest" %% "scalatest" % "3.2.2", + "org.scalacheck" %% "scalacheck" % "1.15.4", + "org.scalatest" %% "scalatest" % "3.2.9", + "org.scalatestplus" %% "scalacheck-1-15" % "3.2.9.0", "org.typelevel" %% "cats-laws" % catsVersion, - "org.typelevel" %% "discipline-scalatest" % "2.0.1" + //"org.typelevel" %% "discipline-scalatest" % "2.0.1" ) val baseSettings = Seq( libraryDependencies ++= Seq( - "com.chuusai" %% "shapeless" % shapelessVersion, + "com.chuusai" %% "shapeless" % shapelessVersion cross CrossVersion.for3Use2_13, "org.typelevel" %% "cats-core" % catsVersion, - "com.twitter" %% "finagle-http" % twitterVersion, - scalaOrganization.value % "scala-reflect" % scalaVersion.value, + "com.twitter" %% "finagle-http" % twitterVersion cross CrossVersion.for3Use2_13, "org.typelevel" %% "cats-effect" % catsEffectVersion ) ++ testDependencies.map(_ % "test"), resolvers ++= Seq( @@ -65,14 +57,14 @@ val baseSettings = Seq( Resolver.sonatypeRepo("snapshots") ), scalacOptions ++= compilerOptions(scalaVersion.value), - scalacOptions in(Compile, console) ~= { + /*scalacOptions in(Compile, console) ~= { _.filterNot(Set("-Ywarn-unused-import")) }, - scalacOptions in(Compile, console) += "-Yrepl-class-based", - fork in Test := true, - javaOptions in ThisBuild ++= Seq("-Xss2048K"), - addCompilerPlugin("org.typelevel" % "kind-projector" % "0.10.3" cross CrossVersion.binary), - ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.4.2", + scalacOptions in(Compile, console) += "-Yrepl-class-based",*/ + Test / fork := true, + ThisBuild / javaOptions ++= Seq("-Xss2048K"), + //addCompilerPlugin("org.typelevel" % "kind-projector" % "0.10.3" cross CrossVersion.binary), + ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.5.0", semanticdbEnabled := true, semanticdbVersion := scalafixSemanticdb.revision ) @@ -115,7 +107,7 @@ lazy val publishSettings = Seq( else Some("releases" at nexus + "service/local/staging/deploy/maven2") }, - publishArtifact in Test := false, + Test / publishArtifact := false, pgpSecretRing := file("local.secring.gpg"), pgpPublicRing := file("local.pubring.gpg"), releasePublishArtifactsAction := PgpKeys.publishSigned.value, @@ -181,53 +173,12 @@ lazy val noPublish = Seq( lazy val allSettings = baseSettings ++ buildSettings ++ publishSettings -lazy val docSettings = allSettings ++ Seq( - micrositeName := "Finch", - micrositeDescription := "Scala combinator library for building Finagle HTTP services", - micrositeAuthor := "Vladimir Kostyukov", - micrositeCompilingDocsTool := WithMdoc, - mdocIn := baseDirectory.value / "mdoc", - micrositeHighlightTheme := "atom-one-light", - micrositeHomepage := "https://finagle.github.io/finch/", - micrositeDocumentationUrl := "api", - micrositeGithubOwner := "finagle", - micrositeGithubRepo := "finch", - micrositeBaseUrl := "finch", - micrositeExtraMdFiles := Map(file("CONTRIBUTING.md") -> ExtraMdFileConfig("contributing.md", "docs")), - micrositePalette := Map( - "brand-primary" -> "#3b3c3b", - "brand-secondary" -> "#4c4d4c", - "brand-tertiary" -> "#5d5e5d", - "gray-dark" -> "#48494B", - "gray" -> "#7D7E7D", - "gray-light" -> "#E5E6E5", - "gray-lighter" -> "#F4F3F4", - "white-color" -> "#FFFFFF"), - addMappingsToSiteDir(mappings in(ScalaUnidoc, packageDoc), micrositeDocumentationUrl), - ghpagesNoJekyll := false, - scalacOptions in(ScalaUnidoc, unidoc) ++= Seq( - "-groups", - "-implicits", - "-skip-packages", "scalaz", - "-doc-source-url", scmInfo.value.get.browseUrl + "/tree/master€{FILE_PATH}.scala", - "-sourcepath", baseDirectory.in(LocalRootProject).value.getAbsolutePath, - "-doc-root-content", (resourceDirectory.in(Compile).value / "rootdoc.txt").getAbsolutePath - ), - scalacOptions ~= { - _.filterNot(Set("-Yno-predef", "-Xlint", "-Ywarn-unused-import")) - }, - git.remoteRepo := "git@github.com:finagle/finch.git", - unidocProjectFilter in(ScalaUnidoc, unidoc) := inAnyProject -- inProjects(benchmarks, jsonTest), - includeFilter in makeSite := "*.html" | "*.css" | "*.png" | "*.jpg" | "*.gif" | "*.svg" | "*.js" | "*.swf" | "*.yml" | "*.md", - siteSubdirName in ScalaUnidoc := "docs" -) - lazy val finch = project.in(file(".")) .settings(moduleName := "finch") .settings(allSettings) .settings(noPublish) .settings( - initialCommands in console := + initialCommands / console := """ |import io.finch._ |import io.finch.circe._ @@ -248,24 +199,14 @@ lazy val finch = project.in(file(".")) "io.circe" %% "circe-generic" % circeVersion )) .aggregate( - core, fs2, iteratee, generic, argonaut, circe, benchmarks, test, jsonTest, examples, refined + core, fs2, generic, argonaut, circe, benchmarks, test, jsonTest, examples, refined ) - .dependsOn(core, iteratee, generic, circe) + .dependsOn(core, generic, circe) lazy val core = project .settings(moduleName := "finchx-core") .settings(allSettings) -lazy val iteratee = project - .settings(moduleName := "finchx-iteratee") - .settings(allSettings) - .settings( - libraryDependencies ++= Seq( - "io.iteratee" %% "iteratee-core" % iterateeVersion - ) - ) - .dependsOn(core % "compile->compile;test->test") - lazy val fs2 = project .settings(moduleName := "finchx-fs2") .settings(allSettings) @@ -295,11 +236,10 @@ lazy val jsonTest = project.in(file("json-test")) .settings( libraryDependencies ++= Seq( "io.circe" %% "circe-core" % circeVersion, - "io.circe" %% "circe-jawn" % circeVersion, - "io.circe" %% "circe-iteratee" % circeIterateeVersion + "io.circe" %% "circe-jawn" % circeVersion ) ++ testDependencies ) - .dependsOn(core, iteratee) + .dependsOn(core) lazy val argonaut = project .settings(moduleName := "finchx-argonaut") @@ -315,8 +255,7 @@ lazy val circe = project .settings( libraryDependencies ++= Seq( "io.circe" %% "circe-core" % circeVersion, - "io.circe" %% "circe-iteratee" % circeIterateeVersion, - "io.circe" %% "circe-fs2" % circeFs2Version, + //"io.circe" %% "circe-fs2" % circeFs2Version, "io.circe" %% "circe-jawn" % circeVersion, "io.circe" %% "circe-generic" % circeVersion % "test" ) @@ -328,28 +267,11 @@ lazy val refined = project .settings(allSettings) .settings( libraryDependencies ++= Seq( - "eu.timepit" %% "refined" % refinedVersion, - "eu.timepit" %% "refined-cats" % refinedVersion % "test", - "eu.timepit" %% "refined-scalacheck" % refinedVersion % "test" + "eu.timepit" %% "refined" % refinedVersion ) ) .dependsOn(core % "test->test;compile->compile") -lazy val docs = project - .settings(moduleName := "finchx-docs") - .settings(docSettings) - .settings(noPublish) - .settings( - libraryDependencies ++= Seq( - "io.circe" %% "circe-generic" % circeVersion, - "com.twitter" %% "twitter-server" % twitterVersion, - "joda-time" % "joda-time" % "2.9.9", - "org.mockito" % "mockito-all" % "1.10.19" - ) - ) - .enablePlugins(MicrositesPlugin, ScalaUnidocPlugin) - .dependsOn(core, circe, argonaut, iteratee, refined, fs2) - lazy val examples = project .settings(moduleName := "finchx-examples") .settings(allSettings) @@ -365,11 +287,11 @@ lazy val examples = project .settings( libraryDependencies ++= Seq( "io.circe" %% "circe-generic" % circeVersion, - "com.twitter" %% "finagle-stats" % twitterVersion, - "com.twitter" %% "twitter-server" % twitterVersion + "com.twitter" %% "finagle-stats" % twitterVersion cross CrossVersion.for3Use2_13, + "com.twitter" %% "twitter-server" % twitterVersion cross CrossVersion.for3Use2_13 ) ) - .dependsOn(core, circe, iteratee) + .dependsOn(core, circe) lazy val benchmarks = project .settings(moduleName := "finchx-benchmarks") @@ -379,7 +301,7 @@ lazy val benchmarks = project .settings(libraryDependencies += "io.circe" %% "circe-generic" % circeVersion) .settings(coverageExcludedPackages := "io\\.finch\\..*;") .settings( - javaOptions in run ++= Seq( + run / javaOptions ++= Seq( "-Djava.net.preferIPv4Stack=true", "-XX:+AggressiveOpts", "-XX:+UseParNewGC", diff --git a/core/src/main/scala/io/finch/Accept.scala b/core/src/main/scala/io/finch/Accept.scala index ad4e94158..9bf0e3c5e 100644 --- a/core/src/main/scala/io/finch/Accept.scala +++ b/core/src/main/scala/io/finch/Accept.scala @@ -2,15 +2,15 @@ package io.finch import java.util.Locale -import shapeless.Witness - /** - * Models an HTTP Accept header (see RFC2616, 14.1). - * - * @note This API doesn't validate the input primary/sub types. - * - * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - */ + * Models an HTTP Accept header (see RFC2616, 14.1). + * + * @note + * This API doesn't validate the input primary/sub types. + * + * @see + * https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + */ abstract class Accept { def primary: String def sub: String @@ -37,14 +37,14 @@ object Accept { def apply(a: Accept): Boolean = false } - implicit val json: Matcher[Application.Json] = fromWitness[Application.Json] - implicit val xml: Matcher[Application.Xml] = fromWitness[Application.Xml] - implicit val text: Matcher[Text.Plain] = fromWitness[Text.Plain] - implicit val html: Matcher[Text.Html] = fromWitness[Text.Html] + implicit val json: Matcher[Application.Json] = fromLiteral[Application.Json] + implicit val xml: Matcher[Application.Xml] = fromLiteral[Application.Xml] + implicit val text: Matcher[Text.Plain] = fromLiteral[Text.Plain] + implicit val html: Matcher[Text.Html] = fromLiteral[Text.Html] - implicit def fromWitness[CT <: String](implicit w: Witness.Aux[CT]): Matcher[CT] = { + implicit def fromLiteral[CT <: String](implicit w: ValueOf[CT]): Matcher[CT] = { val slashIndex = w.value.indexOf(47) - if (slashIndex == 0 || slashIndex == w.value.length) Empty.asInstanceOf[Matcher[CT]] + if slashIndex == 0 || slashIndex == w.value.length then Empty.asInstanceOf[Matcher[CT]] else new Matcher[CT] { private val primary: String = w.value.substring(0, slashIndex).trim.toLowerCase(Locale.ENGLISH) @@ -56,15 +56,15 @@ object Accept { } /** - * Parses an [[Accept]] instance from a given string. Returns `null` when not able to parse. - */ + * Parses an [[Accept]] instance from a given string. Returns `null` when not able to parse. + */ def fromString(s: String): Accept = { // Adopted from Java's MimeType's API. val slashIndex = s.indexOf(47) val semIndex = s.indexOf(59) - val length = if (semIndex < 0) s.length else semIndex + val length = if semIndex < 0 then s.length else semIndex - if (slashIndex < 0 || slashIndex >= length) Empty + if slashIndex < 0 || slashIndex >= length then Empty else new Accept { val primary: String = s.substring(0, slashIndex).trim.toLowerCase(Locale.ENGLISH) diff --git a/core/src/main/scala/io/finch/Bootstrap.scala b/core/src/main/scala/io/finch/Bootstrap.scala index c0971106a..0b1752de2 100644 --- a/core/src/main/scala/io/finch/Bootstrap.scala +++ b/core/src/main/scala/io/finch/Bootstrap.scala @@ -6,36 +6,36 @@ import com.twitter.finagle.http.{Request, Response} import shapeless._ /** - * Bootstraps a Finagle HTTP service out of the collection of Finch endpoints. - * - * {{{ - * val api: Service[Request, Response] = Bootstrap - * .configure(negotiateContentType = true, enableMethodNotAllowed = true) - * .serve[Application.Json](getUser :+: postUser) - * .serve[Text.Plain](healthcheck) - * .toService - * }}} - * - * == Supported Configuration Options == - * - * - `includeDateHeader` (default: `true`): whether or not to include the Date header into - * each response (see RFC2616, section 14.18) - * - * - `includeServerHeader` (default: `true`): whether or not to include the Server header into - * each response (see RFC2616, section 14.38) - * - * - `enableMethodNotAllowed` (default: `false`): whether or not to enable 405 MethodNotAllowed HTTP - * response (see RFC2616, section 10.4.6) - * - * - `enableUnsupportedMediaType` (default: `false`) whether or not to enable 415 - * UnsupportedMediaType HTTP response (see RFC7231, section 6.5.13) - * - * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html - * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html - * @see https://tools.ietf.org/html/rfc7231#section-6.5.13 - */ -class Bootstrap[F[_], ES <: HList, CTS <: HList]( + * Bootstraps a Finagle HTTP service out of the collection of Finch endpoints. + * + * {{{ + * val api: Service[Request, Response] = Bootstrap + * .configure(negotiateContentType = true, enableMethodNotAllowed = true) + * .serve[Application.Json](getUser :+: postUser) + * .serve[Text.Plain](healthcheck) + * .toService + * }}} + * + * ==Supported Configuration Options== + * + * - `includeDateHeader` (default: `true`): whether or not to include the Date header into each response (see RFC2616, section 14.18) + * + * - `includeServerHeader` (default: `true`): whether or not to include the Server header into each response (see RFC2616, section 14.38) + * + * - `enableMethodNotAllowed` (default: `false`): whether or not to enable 405 MethodNotAllowed HTTP response (see RFC2616, section 10.4.6) + * + * - `enableUnsupportedMediaType` (default: `false`) whether or not to enable 415 UnsupportedMediaType HTTP response (see RFC7231, section 6.5.13) + * + * @see + * https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + * @see + * https://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html + * @see + * https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + * @see + * https://tools.ietf.org/html/rfc7231#section-6.5.13 + */ +class Bootstrap[F[_], ES <: Tuple, CTS <: Tuple]( val endpoints: ES, val includeDateHeader: Boolean = true, val includeServerHeader: Boolean = true, @@ -44,9 +44,9 @@ class Bootstrap[F[_], ES <: HList, CTS <: HList]( ) { self => class Serve[CT] { - def apply[FF[_], E](e: Endpoint[FF, E]): Bootstrap[FF, Endpoint[FF, E] :: ES, CT :: CTS] = - new Bootstrap[FF, Endpoint[FF, E] :: ES, CT :: CTS]( - e :: self.endpoints, + def apply[FF[_], E](e: Endpoint[FF, E]): Bootstrap[FF, Endpoint[FF, E] *: ES, CT *: CTS] = + new Bootstrap[FF, Endpoint[FF, E] *: ES, CT *: CTS]( + e *: self.endpoints, includeDateHeader, includeServerHeader, enableMethodNotAllowed, @@ -89,8 +89,8 @@ class Bootstrap[F[_], ES <: HList, CTS <: HList]( } object Bootstrap - extends Bootstrap[Id, HNil, HNil]( - endpoints = HNil, + extends Bootstrap[Id, EmptyTuple, EmptyTuple]( + endpoints = EmptyTuple, includeDateHeader = true, includeServerHeader = true, enableMethodNotAllowed = false, diff --git a/core/src/main/scala/io/finch/Compile.scala b/core/src/main/scala/io/finch/Compile.scala index 294e95ba7..0b73ee8da 100644 --- a/core/src/main/scala/io/finch/Compile.scala +++ b/core/src/main/scala/io/finch/Compile.scala @@ -9,17 +9,17 @@ import io.finch.internal.currentTime import shapeless._ /** - * Compiles a given list of [[Endpoint]]s and their content-types into single [[Endpoint.Compiled]]. - * - * Guarantees to: - * - * - handle Finch's own errors (i.e., [[Error]] and [[Error]]) as 400s - * - copy requests's HTTP version onto a response - * - respond with 404 when an endpoint is not matched - * - respond with 405 when an endpoint is not matched because method wasn't allowed (serve back an `Allow` header) - * - include the date header on each response (unless disabled) - * - include the server header on each response (unless disabled) - */ + * Compiles a given list of [[Endpoint]] s and their content-types into single [[Endpoint.Compiled]]. + * + * Guarantees to: + * + * - handle Finch's own errors (i.e., [[Error]] and [[Error]]) as 400s + * - copy requests's HTTP version onto a response + * - respond with 404 when an endpoint is not matched + * - respond with 405 when an endpoint is not matched because method wasn't allowed (serve back an `Allow` header) + * - include the date header on each response (unless disabled) + * - include the server header on each response (unless disabled) + */ @implicitNotFound( """An Endpoint you're trying to compile is missing one or more encoders. @@ -32,15 +32,15 @@ import shapeless._ See https://github.com/finagle/finch/blob/master/docs/src/main/tut/cookbook.md#fixing-the-toservice-compile-error """ ) -trait Compile[F[_], ES <: HList, CTS <: HList] { +trait Compile[F[_], ES <: Tuple, CTS <: Tuple] { def apply(endpoints: ES, options: Compile.Options, context: Compile.Context): Endpoint.Compiled[F] } object Compile { /** - * HTTP options propagated from [[Bootstrap]]. - */ + * HTTP options propagated from [[Bootstrap]]. + */ final case class Options( includeDateHeader: Boolean, includeServerHeader: Boolean, @@ -49,10 +49,10 @@ object Compile { ) /** - * HTTP context propagated between endpoints. - * - * - `wouldAllow`: when non-empty, indicates that the incoming method wasn't allowed/matched - */ + * HTTP context propagated between endpoints. + * + * - `wouldAllow`: when non-empty, indicates that the incoming method wasn't allowed/matched + */ final case class Context(wouldAllow: List[Method] = Nil) private val respond400: PartialFunction[Throwable, Output[Nothing]] = { @@ -68,23 +68,23 @@ object Compile { private def conformHttp(rep: Response, version: Version, options: Options): Response = { rep.version = version - if (options.includeDateHeader) { + if options.includeDateHeader then { rep.headerMap.setUnsafe("Date", currentTime()) } - if (options.includeServerHeader) { + if options.includeServerHeader then { rep.headerMap.setUnsafe("Server", "Finch") } rep } - implicit def hnilTS[F[_]](implicit F: Applicative[F]): Compile[F, HNil, HNil] = new Compile[F, HNil, HNil] { - def apply(es: HNil, opts: Options, ctx: Context): Endpoint.Compiled[F] = - Endpoint.Compiled[F] { req: Request => + given emptyTupleTs[F[_]](using F: Applicative[F]): Compile[F, EmptyTuple, EmptyTuple] = new Compile[F, EmptyTuple, EmptyTuple] { + def apply(es: EmptyTuple, opts: Options, ctx: Context): Endpoint.Compiled[F] = + Endpoint.Compiled[F] { (req: Request) => val rep = Response() - if (ctx.wouldAllow.nonEmpty && opts.enableMethodNotAllowed) { + if ctx.wouldAllow.nonEmpty && opts.enableMethodNotAllowed then { rep.status = Status.MethodNotAllowed rep.allow = ctx.wouldAllow } else { @@ -95,26 +95,34 @@ object Compile { } } - type IsNegotiable[C] = OrElse[C <:< Coproduct, DummyImplicit] + type IsNegotiable[C] = io.finch.internal.OrElse[C <:< Coproduct, DummyImplicit] - implicit def hlistTS[F[_], A, EH <: Endpoint[F, A], ET <: HList, CTH, CTT <: HList](implicit + given nonEmptyTupleTs[F[_], A, EH <: Endpoint[F, A], ET <: Tuple, CTH, CTT <: Tuple](using ntrA: ToResponse.Negotiable[F, A, CTH], ntrE: ToResponse.Negotiable[F, Exception, CTH], F: MonadError[F, Throwable], tsT: Compile[F, ET, CTT], isNegotiable: IsNegotiable[CTH] - ): Compile[F, Endpoint[F, A] :: ET, CTH :: CTT] = new Compile[F, Endpoint[F, A] :: ET, CTH :: CTT] { - def apply(es: Endpoint[F, A] :: ET, opts: Options, ctx: Context): Endpoint.Compiled[F] = { - val handler = if (opts.enableUnsupportedMediaType) respond415.orElse(respond400) else respond400 + ): Compile[F, Endpoint[F, A] *: ET, CTH *: CTT] = new Compile[F, Endpoint[F, A] *: ET, CTH *: CTT] { + def apply(es: Endpoint[F, A] *: ET, opts: Options, ctx: Context): Endpoint.Compiled[F] = { + val handler = if opts.enableUnsupportedMediaType then respond415.orElse(respond400) else respond400 val negotiateContent = isNegotiable.fold(_ => true, _ => false) val underlying = es.head.handle(handler) - Endpoint.Compiled[F] { req: Request => + Endpoint.Compiled[F] { (req: Request) => underlying(Input.fromRequest(req)) match { case EndpointResult.Matched(rem, trc, out) if rem.route.isEmpty => - val accept = if (negotiateContent) req.accept.map(a => Accept.fromString(a)).toList else Nil - - F.flatMap(out)(oa => oa.toResponse(F, ntrA(accept), ntrE(accept)).map(r => conformHttp(r, req.version, opts))).attempt.map(e => trc -> e) + val accept = if negotiateContent then req.accept.map(a => Accept.fromString(a)).toList else Nil + out + .flatMap { (oa: Output[A]) => + given tra: ToResponse.Aux[F, A, CTH] = ntrA(accept) + given tre: ToResponse.Aux[F, Exception, CTH] = ntrE(accept) + oa.toResponse[F, CTH].map(r => conformHttp(r, req.version, opts)) + } + .attempt + .map { e => + trc -> e + } case EndpointResult.NotMatched.MethodNotAllowed(allowed) => tsT(es.tail, opts, ctx.copy(wouldAllow = ctx.wouldAllow ++ allowed))(req) diff --git a/core/src/main/scala/io/finch/Decode.scala b/core/src/main/scala/io/finch/Decode.scala index 20fad8d79..8f53f7b86 100644 --- a/core/src/main/scala/io/finch/Decode.scala +++ b/core/src/main/scala/io/finch/Decode.scala @@ -5,12 +5,11 @@ import java.nio.charset.Charset import scala.util.control.NoStackTrace import com.twitter.io.Buf -import shapeless.{:+:, CNil, Coproduct, Witness} +import shapeless.{:+:, CNil, Coproduct} /** - * Decodes an HTTP payload represented as [[Buf]] (encoded with [[Charset]]) into - * an arbitrary type `A`. - */ + * Decodes an HTTP payload represented as [[Buf]] (encoded with [[Charset]]) into an arbitrary type `A`. + */ trait Decode[A] { type ContentType <: String @@ -20,9 +19,8 @@ trait Decode[A] { object Decode { /** - * Indicates that a payload can not be decoded with a given [[Decode]] instance (or a coproduct - * of instances). - */ + * Indicates that a payload can not be decoded with a given [[Decode]] instance (or a coproduct of instances). + */ object UnsupportedMediaTypeException extends Exception with NoStackTrace type Aux[A, CT <: String] = Decode[A] { type ContentType = CT } @@ -31,8 +29,8 @@ object Decode { type Text[A] = Aux[A, Text.Plain] /** - * Creates an instance for a given type. - */ + * Creates an instance for a given type. + */ def instance[A, CT <: String](fn: (Buf, Charset) => Either[Throwable, A]): Aux[A, CT] = new Decode[A] { type ContentType = CT def apply(b: Buf, cs: Charset): Either[Throwable, A] = fn(b, cs) @@ -45,14 +43,13 @@ object Decode { instance[A, Text.Plain](fn) /** - * Returns a [[Decode]] instance for a given type (with required content type). - */ + * Returns a [[Decode]] instance for a given type (with required content type). + */ @inline def apply[A, CT <: String](implicit d: Aux[A, CT]): Aux[A, CT] = d /** - * Abstracting over [[Decode]] to select a correct decoder according to the `Content-Type` header - * value. - */ + * Abstracting over [[Decode]] to select a correct decoder according to the `Content-Type` header value. + */ trait Dispatchable[A, CT] { def apply(ct: String, b: Buf, cs: Charset): Either[Throwable, A] } @@ -66,17 +63,17 @@ object Decode { implicit def coproductToDispatchable[A, CTH <: String, CTT <: Coproduct](implicit decode: Decode.Aux[A, CTH], - witness: Witness.Aux[CTH], + witness: ValueOf[CTH], tail: Dispatchable[A, CTT] ): Dispatchable[A, CTH :+: CTT] = new Dispatchable[A, CTH :+: CTT] { def apply(ct: String, b: Buf, cs: Charset): Either[Throwable, A] = - if (ct.equalsIgnoreCase(witness.value)) decode(b, cs) + if ct.equalsIgnoreCase(witness.value) then decode(b, cs) else tail(ct, b, cs) } implicit def singleToDispatchable[A, CT <: String](implicit decode: Decode.Aux[A, CT], - witness: Witness.Aux[CT] + witness: ValueOf[CT] ): Dispatchable[A, CT] = coproductToDispatchable[A, CT, CNil].asInstanceOf[Dispatchable[A, CT]] } } diff --git a/core/src/main/scala/io/finch/DecodeEntity.scala b/core/src/main/scala/io/finch/DecodeEntity.scala index cd01c6da4..5ec78ed4a 100644 --- a/core/src/main/scala/io/finch/DecodeEntity.scala +++ b/core/src/main/scala/io/finch/DecodeEntity.scala @@ -2,12 +2,11 @@ package io.finch import java.util.UUID -import shapeless._ +import scala.reflect.api.Mirror /** - * Decodes an HTTP entity (eg: header, query-string param) represented as UTF-8 `String` into - * an arbitrary type `A`. - */ + * Decodes an HTTP entity (eg: header, query-string param) represented as UTF-8 `String` into an arbitrary type `A`. + */ trait DecodeEntity[A] { def apply(s: String): Either[Throwable, A] } @@ -15,8 +14,8 @@ trait DecodeEntity[A] { object DecodeEntity extends HighPriorityDecode { /** - * Returns a [[DecodeEntity]] instance for a given type. - */ + * Returns a [[DecodeEntity]] instance for a given type. + */ @inline def apply[A](implicit d: DecodeEntity[A]): DecodeEntity[A] = d implicit val decodeString: DecodeEntity[String] = instance(s => Right(s)) @@ -51,7 +50,7 @@ trait HighPriorityDecode extends LowPriorityDecode { ) implicit val decodeUUID: DecodeEntity[UUID] = instance(s => - if (s.length != 36) Left(new IllegalArgumentException(s"Too long for UUID: ${s.length}")) + if s.length != 36 then Left(new IllegalArgumentException(s"Too long for UUID: ${s.length}")) else try Right(UUID.fromString(s)) catch { case e: Throwable => Left(e) } @@ -62,18 +61,18 @@ trait HighPriorityDecode extends LowPriorityDecode { trait LowPriorityDecode { /** - * Creates an [[DecodeEntity]] instance from a given function `String => Either[Throwable, A]`. - */ + * Creates an [[DecodeEntity]] instance from a given function `String => Either[Throwable, A]`. + */ def instance[A](fn: String => Either[Throwable, A]): DecodeEntity[A] = new DecodeEntity[A] { def apply(s: String): Either[Throwable, A] = fn(s) } /** - * Creates a [[Decode]] from [[shapeless.Generic]] for single value case classes. - */ - implicit def decodeFromGeneric[A, H <: HList, E](implicit - gen: Generic.Aux[A, H], - ev: (E :: HNil) =:= H, + * Creates a [[Decode]] from [[shapeless.Generic]] for single value case classes. + */ + implicit def decodeFromGeneric[A, H <: Tuple, E](implicit + mirror: scala.deriving.Mirror.ProductOf[A], + ev: (E *: EmptyTuple) =:= H, de: DecodeEntity[E] - ): DecodeEntity[A] = instance(s => de(s).right.map(b => gen.from(b :: HNil))) + ): DecodeEntity[A] = instance(s => de(s).map(b => mirror.fromProduct(b *: EmptyTuple))) } diff --git a/core/src/main/scala/io/finch/DecodePath.scala b/core/src/main/scala/io/finch/DecodePath.scala index ca3949272..77de2eae9 100644 --- a/core/src/main/scala/io/finch/DecodePath.scala +++ b/core/src/main/scala/io/finch/DecodePath.scala @@ -5,9 +5,8 @@ import java.util.UUID import io.finch.internal.TooFastString /** - * Decodes an HTTP path (eg: /foo/bar/baz) represented as UTF-8 `String` into - * an arbitrary type `A`. - */ + * Decodes an HTTP path (eg: /foo/bar/baz) represented as UTF-8 `String` into an arbitrary type `A`. + */ trait DecodePath[A] { def apply(s: String): Option[A] } @@ -25,7 +24,7 @@ object DecodePath { implicit val decodeLong: DecodePath[Long] = instance(_.tooLong) implicit val decodeBoolean: DecodePath[Boolean] = instance(_.tooBoolean) implicit val decodeUUID: DecodePath[UUID] = instance { s => - if (s.length != 36) None + if s.length != 36 then None else try Some(UUID.fromString(s)) catch { case _: Exception => None } diff --git a/core/src/main/scala/io/finch/DecodeStream.scala b/core/src/main/scala/io/finch/DecodeStream.scala index cc8990c25..ea7273528 100644 --- a/core/src/main/scala/io/finch/DecodeStream.scala +++ b/core/src/main/scala/io/finch/DecodeStream.scala @@ -7,9 +7,8 @@ import scala.annotation.implicitNotFound import com.twitter.io.Buf /** - * Stream HTTP streamed payload represented as S[F, Buf] into - * a S[F, A] of arbitrary type `A`. - */ + * Stream HTTP streamed payload represented as S[F, Buf] into a S[F, A] of arbitrary type `A`. + */ trait DecodeStream[S[_[_], _], F[_], A] { type ContentType <: String diff --git a/core/src/main/scala/io/finch/Encode.scala b/core/src/main/scala/io/finch/Encode.scala index e07eea797..197b94a0b 100644 --- a/core/src/main/scala/io/finch/Encode.scala +++ b/core/src/main/scala/io/finch/Encode.scala @@ -6,8 +6,8 @@ import cats.Show import com.twitter.io.Buf /** - * Encodes an HTTP payload (represented as an arbitrary type `A`) with a given [[Charset]]. - */ + * Encodes an HTTP payload (represented as an arbitrary type `A`) with a given [[Charset]]. + */ trait Encode[A] { type ContentType <: String @@ -58,8 +58,8 @@ trait HighPriorityEncodeInstances extends LowPriorityEncodeInstances { object Encode extends HighPriorityEncodeInstances { /** - * Returns a [[Encode]] instance for a given type (with required content type). - */ + * Returns a [[Encode]] instance for a given type (with required content type). + */ @inline final def apply[A, CT <: String](implicit e: Aux[A, CT]): Aux[A, CT] = e implicit val encodeExceptionAsTextPlain: Text[Exception] = diff --git a/core/src/main/scala/io/finch/EncodeStream.scala b/core/src/main/scala/io/finch/EncodeStream.scala index 2b8fb4412..6461f8e18 100644 --- a/core/src/main/scala/io/finch/EncodeStream.scala +++ b/core/src/main/scala/io/finch/EncodeStream.scala @@ -5,8 +5,8 @@ import java.nio.charset.Charset import com.twitter.io.{Buf, Reader} /** - * A type-class that defines encoding of a stream in a shape of `S[F[_], A]` to Finagle's [[Reader]]. - */ + * A type-class that defines encoding of a stream in a shape of `S[F[_], A]` to Finagle's [[Reader]]. + */ trait EncodeStream[F[_], S[_[_], _], A] { type ContentType <: String diff --git a/core/src/main/scala/io/finch/Endpoint.scala b/core/src/main/scala/io/finch/Endpoint.scala index 002556a98..de0062794 100644 --- a/core/src/main/scala/io/finch/Endpoint.scala +++ b/core/src/main/scala/io/finch/Endpoint.scala @@ -14,76 +14,74 @@ import com.twitter.finagle.http.{Cookie => FinagleCookie, Method => FinagleMetho import com.twitter.io.Buf import io.finch.endpoint._ import io.finch.internal._ -import shapeless._ +import shapeless.{HList => _, :: => _, _} import shapeless.ops.adjoin.Adjoin import shapeless.ops.hlist.Tupler +import scala.deriving.Mirror /** - * An `Endpoint` represents the HTTP endpoint. - * - * It is well known and widely adopted in Finagle that "Your Server is a Function" - * (i.e., `Request => Future[Response]`). In a REST/HTTP API setting this function may be viewed as - * `Request =1=> (A =2=> Future[B]) =3=> Future[Response]`, where transformation `1` is a request - * decoding (deserialization), transformation `2` - is a business logic and transformation `3` is - - * a response encoding (serialization). The only interesting part here is transformation `2` (i.e., - * `A => Future[B]`), which represents an application business. - * - * An `Endpoint` transformation (`map`, `mapAsync`, etc.) encodes the business logic, while the - * rest of Finch ecosystem takes care about both serialization and deserialization. - * - * A typical way to transform (or map) the `Endpoint` is to use [[internal.Mapper]]: - * - * {{{ - * import io.finch._ - * - * case class Foo(i: Int) - * case class Bar(s: String) - * - * val foo: Endpoint[Foo] = get("foo") { Ok(Foo(42)) } - * val bar: Endpoint[Bar] = get("bar" :: path[String]) { s: String => Ok(Bar(s)) } - * }}} - * - * `Endpoint`s are also composable in terms of or-else combinator (known as a "space invader" - * operator `:+:`) that takes two `Endpoint`s and returns a coproduct `Endpoint`. - * - * {{{ - * import io.finch._ - * - * val foobar: Endpoint[Foo :+: Bar :+: CNil] = foo :+: bar - * }}} - * - * An `Endpoint` might be converted into a Finagle [[Service]] with `Endpoint.toService` method so - * it can be served within Finagle HTTP. - * - * {{{ - * import com.twitter.finagle.Http - * - * Http.server.serve(foobar.toService) - * }}} - */ + * An `Endpoint` represents the HTTP endpoint. + * + * It is well known and widely adopted in Finagle that "Your Server is a Function" (i.e., `Request => Future[Response]`). In a REST/HTTP API setting this + * function may be viewed as `Request =1=> (A =2=> Future[B]) =3=> Future[Response]`, where transformation `1` is a request decoding (deserialization), + * transformation `2` - is a business logic and transformation `3` is - a response encoding (serialization). The only interesting part here is transformation + * `2` (i.e., `A => Future[B]`), which represents an application business. + * + * An `Endpoint` transformation (`map`, `mapAsync`, etc.) encodes the business logic, while the rest of Finch ecosystem takes care about both serialization and + * deserialization. + * + * A typical way to transform (or map) the `Endpoint` is to use [[internal.Mapper]]: + * + * {{{ + * import io.finch._ + * + * case class Foo(i: Int) + * case class Bar(s: String) + * + * val foo: Endpoint[Foo] = get("foo") { Ok(Foo(42)) } + * val bar: Endpoint[Bar] = get("bar" :: path[String]) { s: String => Ok(Bar(s)) } + * }}} + * + * `Endpoint`s are also composable in terms of or-else combinator (known as a "space invader" operator `:+:`) that takes two `Endpoint`s and returns a coproduct + * `Endpoint`. + * + * {{{ + * import io.finch._ + * + * val foobar: Endpoint[Foo :+: Bar :+: CNil] = foo :+: bar + * }}} + * + * An `Endpoint` might be converted into a Finagle [[Service]] with `Endpoint.toService` method so it can be served within Finagle HTTP. + * + * {{{ + * import com.twitter.finagle.Http + * + * Http.server.serve(foobar.toService) + * }}} + */ trait Endpoint[F[_], A] { self => /** - * Request item (part) that's this endpoint work with. - */ + * Request item (part) that's this endpoint work with. + */ def item: items.RequestItem = items.MultipleItems /** - * Runs this endpoint. - */ + * Runs this endpoint. + */ def apply(input: Input): Endpoint.Result[F, A] /** - * Maps this endpoint to the given function `A => B`. - */ - final def map[B](fn: A => B)(implicit F: Monad[F]): Endpoint[F, B] = + * Maps this endpoint to the given function `A => B`. + */ + final def map[B](fn: A => B)(using F: Monad[F]): Endpoint[F, B] = mapAsync(fn.andThen(F.pure)) /** - * Maps this endpoint to the given function `A => Future[B]`. - */ - final def mapAsync[B](fn: A => F[B])(implicit F: Monad[F]): Endpoint[F, B] = + * Maps this endpoint to the given function `A => Future[B]`. + */ + final def mapAsync[B](fn: A => F[B])(using Monad[F]): Endpoint[F, B] = new Endpoint[F, B] with (Output[A] => F[Output[B]]) { final def apply(oa: Output[A]): F[Output[B]] = oa.traverse(fn) @@ -100,15 +98,15 @@ trait Endpoint[F[_], A] { } /** - * Maps this endpoint to the given function `A => Output[B]`. - */ - final def mapOutput[B](fn: A => Output[B])(implicit F: MonadError[F, Throwable]): Endpoint[F, B] = + * Maps this endpoint to the given function `A => Output[B]`. + */ + final def mapOutput[B](fn: A => Output[B])(using F: MonadError[F, Throwable]): Endpoint[F, B] = mapOutputAsync(a => F.catchNonFatal(fn(a))) /** - * Maps this endpoint to the given function `A => Future[Output[B]]`. - */ - final def mapOutputAsync[B](fn: A => F[Output[B]])(implicit F: Monad[F]): Endpoint[F, B] = + * Maps this endpoint to the given function `A => Future[Output[B]]`. + */ + final def mapOutputAsync[B](fn: A => F[Output[B]])(using Monad[F]): Endpoint[F, B] = new Endpoint[F, B] with (Output[A] => F[Output[B]]) { final def apply(oa: Output[A]): F[Output[B]] = oa.traverseFlatten(fn) @@ -124,23 +122,22 @@ trait Endpoint[F[_], A] { } /** - * Transforms this endpoint to the given function `F[Output[A]] => F[Output[B]]`. - * - * Might be useful to perform some extra action on the underlying `Future`. For example, time - * the latency of the given endpoint. - * - * {{{ - * import io.finch._ - * import com.twitter.finagle.stats._ - * - * def time[A](stat: Stat, e: Endpoint[A]): Endpoint[A] = - * e.transform(f => Stat.timeFuture(s)(f)) - * }}} - */ + * Transforms this endpoint to the given function `F[Output[A]] => F[Output[B]]`. + * + * Might be useful to perform some extra action on the underlying `Future`. For example, time the latency of the given endpoint. + * + * {{{ + * import io.finch._ + * import com.twitter.finagle.stats._ + * + * def time[A](stat: Stat, e: Endpoint[A]): Endpoint[A] = + * e.transform(f => Stat.timeFuture(s)(f)) + * }}} + */ final def transformOutput[B](fn: F[Output[A]] => F[Output[B]]): Endpoint[F, B] = new Endpoint[F, B] { final def apply(input: Input): Endpoint.Result[F, B] = self(input) match { - case EndpointResult.Matched(rem, trc, out) => + case EndpointResult.Matched(rem, trc, out: F[Output[A]]) => EndpointResult.Matched(rem, trc, fn(out)) case skipped: EndpointResult.NotMatched[F] => skipped } @@ -151,16 +148,16 @@ trait Endpoint[F[_], A] { } /** - * Transform this endpoint to the given function `F[A] => F[B]` - */ + * Transform this endpoint to the given function `F[A] => F[B]` + */ @deprecated("Use .transform instead", "0.29") - final def transformF[B](fn: F[A] => F[B])(implicit F: Monad[F]): Endpoint[F, B] = + final def transformF[B](fn: F[A] => F[B])(using Monad[F]): Endpoint[F, B] = transform(fn) /** - * Transform this endpoint to the given function `F[A] => F[B]` - */ - final def transform[B](fn: F[A] => F[B])(implicit F: Monad[F]): Endpoint[F, B] = + * Transform this endpoint to the given function `F[A] => F[B]` + */ + final def transform[B](fn: F[A] => F[B])(using F: Monad[F]): Endpoint[F, B] = new Endpoint[F, B] { def apply(input: Input): Endpoint.Result[F, B] = self(input) match { @@ -181,8 +178,8 @@ trait Endpoint[F[_], A] { } /** - * Transform effect of endpoint given natural transformation `F ~> G` - */ + * Transform effect of endpoint given natural transformation `F ~> G` + */ final def mapK[G[_]](nat: F ~> G): Endpoint[G, A] = new Endpoint[G, A] { def apply(input: Input): Endpoint.Result[G, A] = @@ -197,21 +194,18 @@ trait Endpoint[F[_], A] { } /** - * Returns a product of this and `other` endpoint. The resulting endpoint returns a tuple - * of both values. - * - * This combinator is an important piece for Finch's error accumulation. In its current form, - * `product` will accumulate Finch's own errors (i.e., [[io.finch.Error]]s) into [[io.finch.Errors]]) and - * will fail-fast with the first non-Finch error (just ordinary `Exception`) observed. - */ - final def product[B](other: Endpoint[F, B])(implicit F: MonadError[F, Throwable]): Endpoint[F, (A, B)] = + * Returns a product of this and `other` endpoint. The resulting endpoint returns a tuple of both values. + * + * This combinator is an important piece for Finch's error accumulation. In its current form, `product` will accumulate Finch's own errors (i.e., + * [[io.finch.Error]] s) into [[io.finch.Errors]]) and will fail-fast with the first non-Finch error (just ordinary `Exception`) observed. + */ + final def product[B](other: Endpoint[F, B])(using MonadError[F, Throwable]): Endpoint[F, (A, B)] = productWith(other)(Tuple2.apply) /** - * Returns a product of this and `other` endpoint. The resulting endpoint returns a value of - * resulting type for product function. - */ - final def productWith[B, O](other: Endpoint[F, B])(p: (A, B) => O)(implicit + * Returns a product of this and `other` endpoint. The resulting endpoint returns a value of resulting type for product function. + */ + final def productWith[B, O](other: Endpoint[F, B])(p: (A, B) => O)(using F: MonadError[F, Throwable] ): Endpoint[F, O] = new Endpoint[F, O] with (((Either[Throwable, Output[A]], Either[Throwable, Output[B]])) => F[Output[O]]) { @@ -252,9 +246,9 @@ trait Endpoint[F[_], A] { } /** - * Composes this endpoint with the given [[Endpoint]]. - */ - final def ::[B](other: Endpoint[F, B])(implicit + * Composes this endpoint with the given [[Endpoint]]. + */ + final def ::[B](other: Endpoint[F, B])(using pa: PairAdjoin[B, A], F: MonadError[F, Throwable] ): Endpoint[F, pa.Out] = new Endpoint[F, pa.Out] with ((B, A) => pa.Out) { @@ -270,21 +264,19 @@ trait Endpoint[F[_], A] { } /** - * Sequentially composes this endpoint with the given `other` endpoint. The resulting endpoint - * will succeed if either this or `that` endpoints are succeed. - * - * == Matching Rules == - * - * - if both endpoints match, the result with a shorter remainder (in terms of consumed route) is picked - * - if both endpoints don't match, the more specific result (explaining the reason for not matching) - * is picked - */ + * Sequentially composes this endpoint with the given `other` endpoint. The resulting endpoint will succeed if either this or `that` endpoints are succeed. + * + * ==Matching Rules== + * + * - if both endpoints match, the result with a shorter remainder (in terms of consumed route) is picked + * - if both endpoints don't match, the more specific result (explaining the reason for not matching) is picked + */ final def coproduct[B >: A](other: Endpoint[F, B]): Endpoint[F, B] = new Endpoint[F, B] { final def apply(input: Input): Endpoint.Result[F, B] = self(input) match { case a @ EndpointResult.Matched(_, _, _) => other(input) match { case b @ EndpointResult.Matched(_, _, _) => - if (a.rem.route.length <= b.rem.route.length) a else b + if a.rem.route.length <= b.rem.route.length then a else b case _ => a } case a => @@ -306,9 +298,9 @@ trait Endpoint[F[_], A] { } /** - * Composes this endpoint with another in such a way that coproducts are flattened. - */ - final def :+:[B](that: Endpoint[F, B])(implicit + * Composes this endpoint with another in such a way that coproducts are flattened. + */ + final def :+:[B](that: Endpoint[F, B])(using a: Adjoin[B :+: A :+: CNil], F: MonadError[F, Throwable] ): Endpoint[F, a.Out] = { @@ -319,123 +311,119 @@ trait Endpoint[F[_], A] { } /** - * Converts this endpoint to a Finagle service `Request => Future[Response]` that serves JSON. - * - * Consider using [[io.finch.Bootstrap]] instead. - */ - final def toService(implicit + * Converts this endpoint to a Finagle service `Request => Future[Response]` that serves JSON. + * + * Consider using [[io.finch.Bootstrap]] instead. + */ + final def toService(using F: Effect[F], tr: ToResponse.Aux[F, A, Application.Json], tre: ToResponse.Aux[F, Exception, Application.Json] ): Service[Request, Response] = toServiceAs[Application.Json] /** - * Converts this endpoint to a Finagle service `Request => Future[Response]` that serves custom - * content-type `CT`. - * - * Consider using [[io.finch.Bootstrap]] instead. - */ - final def toServiceAs[CT <: String](implicit + * Converts this endpoint to a Finagle service `Request => Future[Response]` that serves custom content-type `CT`. + * + * Consider using [[io.finch.Bootstrap]] instead. + */ + final def toServiceAs[CT <: String](using F: Effect[F], tr: ToResponse.Aux[F, A, CT], tre: ToResponse.Aux[F, Exception, CT] ): Service[Request, Response] = Bootstrap.serve[CT](this).toService /** - * Converts this endpoint to Endpoint.Compiled[F] what is efficiently is Kleisli[F, Request, Response] - * where responses are encoded with JSON encoder. - * - * Consider using [[io.finch.Bootstrap]] instead - */ - final def compile(implicit + * Converts this endpoint to Endpoint.Compiled[F] what is efficiently is Kleisli[F, Request, Response] where responses are encoded with JSON encoder. + * + * Consider using [[io.finch.Bootstrap]] instead + */ + final def compile(using F: MonadError[F, Throwable], tr: ToResponse.Aux[F, A, Application.Json], tre: ToResponse.Aux[F, Exception, Application.Json] ): Endpoint.Compiled[F] = compileAs[Application.Json] /** - * Converts this endpoint to Endpoint.Compiled[F] what is efficiently is Kleisli[F, Request, Response] - * where responses are encoded with encoder corresponding to `CT` Content-Type. - * - * Consider using [[io.finch.Bootstrap]] instead - */ - final def compileAs[CT <: String](implicit + * Converts this endpoint to Endpoint.Compiled[F] what is efficiently is Kleisli[F, Request, Response] where responses are encoded with encoder corresponding + * to `CT` Content-Type. + * + * Consider using [[io.finch.Bootstrap]] instead + */ + final def compileAs[CT <: String](using F: MonadError[F, Throwable], tr: ToResponse.Aux[F, A, CT], tre: ToResponse.Aux[F, Exception, CT] ): Endpoint.Compiled[F] = Bootstrap.serve[CT](this).compile /** - * Recovers from any exception occurred in this endpoint by creating a new endpoint that will - * handle any matching throwable from the underlying future. - */ - final def rescue(pf: PartialFunction[Throwable, F[Output[A]]])(implicit + * Recovers from any exception occurred in this endpoint by creating a new endpoint that will handle any matching throwable from the underlying future. + */ + final def rescue(pf: PartialFunction[Throwable, F[Output[A]]])(using F: ApplicativeError[F, Throwable] ): Endpoint[F, A] = transformOutput(foa => foa.recoverWith(pf)) /** - * Recovers from any exception occurred in this endpoint by creating a new endpoint that will - * handle any matching throwable from the underlying future. - */ - final def handle(pf: PartialFunction[Throwable, Output[A]])(implicit + * Recovers from any exception occurred in this endpoint by creating a new endpoint that will handle any matching throwable from the underlying future. + */ + final def handle(pf: PartialFunction[Throwable, Output[A]])(using F: ApplicativeError[F, Throwable] ): Endpoint[F, A] = rescue(pf.andThen(F.pure(_))) /** - * Validates the result of this endpoint using a `predicate`. The rule is used for error - * reporting. - * - * @param rule text describing the rule being validated - * @param predicate returns true if the data is valid - * @return an endpoint that will return the value of this reader if it is valid. - * Otherwise the future fails with an [[Error.NotValid]] error. - */ - final def should(rule: String)(predicate: A => Boolean)(implicit F: MonadError[F, Throwable]): Endpoint[F, A] = + * Validates the result of this endpoint using a `predicate`. The rule is used for error reporting. + * + * @param rule + * text describing the rule being validated + * @param predicate + * returns true if the data is valid + * @return + * an endpoint that will return the value of this reader if it is valid. Otherwise the future fails with an [[Error.NotValid]] error. + */ + final def should(rule: String)(predicate: A => Boolean)(using F: MonadError[F, Throwable]): Endpoint[F, A] = mapAsync(a => - if (predicate(a)) F.pure(a) + if predicate(a) then F.pure(a) else F.raiseError(Error.NotValid(self.item, "should " + rule)) ) /** - * Validates the result of this endpoint using a `predicate`. The rule is used for error reporting. - * - * @param rule text describing the rule being validated - * @param predicate returns false if the data is valid - * @return an endpoint that will return the value of this reader if it is valid. - * Otherwise the future fails with a [[Error.NotValid]] error. - */ - final def shouldNot(rule: String)(predicate: A => Boolean)(implicit F: MonadError[F, Throwable]): Endpoint[F, A] = + * Validates the result of this endpoint using a `predicate`. The rule is used for error reporting. + * + * @param rule + * text describing the rule being validated + * @param predicate + * returns false if the data is valid + * @return + * an endpoint that will return the value of this reader if it is valid. Otherwise the future fails with a [[Error.NotValid]] error. + */ + final def shouldNot(rule: String)(predicate: A => Boolean)(using MonadError[F, Throwable]): Endpoint[F, A] = should(s"not $rule.")(x => !predicate(x)) /** - * Validates the result of this endpoint using a predefined `rule`. This method allows for rules - * to be reused across multiple endpoints. - * - * @param rule the predefined [[ValidationRule]] that will return true if the data is - * valid - * @return an endpoint that will return the value of this reader if it is valid. - * Otherwise the future fails with an [[Error.NotValid]] error. - */ - final def should(rule: ValidationRule[A])(implicit F: MonadError[F, Throwable]): Endpoint[F, A] = + * Validates the result of this endpoint using a predefined `rule`. This method allows for rules to be reused across multiple endpoints. + * + * @param rule + * the predefined [[ValidationRule]] that will return true if the data is valid + * @return + * an endpoint that will return the value of this reader if it is valid. Otherwise the future fails with an [[Error.NotValid]] error. + */ + final def should(rule: ValidationRule[A])(using MonadError[F, Throwable]): Endpoint[F, A] = should(rule.description)(rule.apply) /** - * Validates the result of this endpoint using a predefined `rule`. This method allows for rules - * to be reused across multiple endpoints. - * - * @param rule the predefined [[ValidationRule]] that will return false if the data is - * valid - * @return an endpoint that will return the value of this reader if it is valid. - * Otherwise the future fails with a [[Error.NotValid]] error. - */ - final def shouldNot(rule: ValidationRule[A])(implicit F: MonadError[F, Throwable]): Endpoint[F, A] = + * Validates the result of this endpoint using a predefined `rule`. This method allows for rules to be reused across multiple endpoints. + * + * @param rule + * the predefined [[ValidationRule]] that will return false if the data is valid + * @return + * an endpoint that will return the value of this reader if it is valid. Otherwise the future fails with a [[Error.NotValid]] error. + */ + final def shouldNot(rule: ValidationRule[A])(using MonadError[F, Throwable]): Endpoint[F, A] = shouldNot(rule.description)(rule.apply) /** - * Lifts this endpoint into one that always succeeds, with [[Either[Throwable, A]] representing both success and - * failure cases. - */ - final def attempt(implicit F: MonadError[F, Throwable]): Endpoint[F, Either[Throwable, A]] = + * Lifts this endpoint into one that always succeeds, with [[Either[Throwable, A]] representing both success and failure cases. + */ + final def attempt(using MonadError[F, Throwable]): Endpoint[F, Either[Throwable, A]] = new Endpoint[F, Either[Throwable, A]] with (Either[Throwable, Output[A]] => Output[Either[Throwable, A]]) { final def apply(toa: Either[Throwable, Output[A]]): Output[Either[Throwable, A]] = toa match { case Right(oo) => oo.map(Right.apply) @@ -454,8 +442,8 @@ trait Endpoint[F[_], A] { } /** - * Overrides the `toString` method on this endpoint. - */ + * Overrides the `toString` method on this endpoint. + */ final def withToString(ts: => String): Endpoint[F, A] = new Endpoint[F, A] { final def apply(input: Input): Endpoint.Result[F, A] = self(input) @@ -464,45 +452,25 @@ trait Endpoint[F[_], A] { } /** - * Provides extension methods for [[Endpoint]] to support coproduct and path syntax. - */ + * Provides extension methods for [[Endpoint]] to support coproduct and path syntax. + */ object Endpoint { - /** - * Enables a very simple syntax allowing to "map" endpoints to arbitrary functions. The types are - * resolved at compile time and no reflection is used. - * - * For example: - * - * {{{ - * import io.finch._ - * import io.cats.effect.IO - * - * object Mapping extends Endpoint.Module[IO] { - * def hello = get("hello" :: path[String]) { s: String => - * Ok(s) - * } - * } - * }}} - */ - trait Mappable[F[_], A] extends Endpoint[F, A] { - self => - final def apply(mapper: Mapper[F, A]): Endpoint[F, mapper.Out] = mapper(self) - } + export io.finch.Mappable /** - * An alias for [[EndpointResult]]. - */ + * An alias for [[EndpointResult]]. + */ type Result[F[_], A] = EndpointResult[F, A] /** - * An alias for [[EndpointModule]]. - */ + * An alias for [[EndpointModule]]. + */ type Module[F[_]] = EndpointModule[F] /** - * Representation of function `Request => F[Response]` - */ + * Representation of function `Request => F[Response]` + */ type Compiled[F[_]] = Kleisli[F, Request, (Trace, Either[Throwable, Response])] object Compiled { @@ -512,199 +480,189 @@ object Endpoint { } - implicit final class HListEndpointOps[F[_], L <: HList](val self: Endpoint[F, L]) extends AnyVal { + extension [F[_], L <: Tuple](self: Endpoint[F, L]) { /** - * Converts this endpoint to one that returns any type with this [[shapeless.HList]] as its - * representation. - */ - def as[A](implicit gen: Generic.Aux[A, L], F: Monad[F]): Endpoint[F, A] = self.map(gen.from) + * Converts this endpoint to one that returns any type with this [[shapeless.HList]] as its representation. + */ + def as[A](using mirror: Mirror.ProductOf[A], F: Monad[F]): Endpoint[F, A] = self.map(tuple => mirror.fromProduct(tuple)) - /** - * Converts this endpoint to one that returns a tuple with the same types as this - * [[shapeless.HList]]. - * - * Note that this will fail at compile time if this this [[shapeless.HList]] contains more than - * 22 elements. - */ - def asTuple(implicit t: Tupler[L], F: Monad[F]): Endpoint[F, t.Out] = self.map(t(_)) } /** - * Implicit conversion that adds convenience methods to endpoint for optional values. - */ - implicit final class OptionEndpointOps[F[_], A](val self: Endpoint[F, Option[A]]) extends AnyVal { + * Extension that adds convenience methods to endpoint for optional values. + */ + extension [F[_], A](self: Endpoint[F, Option[A]]) { /** - * If endpoint is empty it will return provided default value. - */ - def withDefault[B >: A](default: => B)(implicit F: Monad[F]): Endpoint[F, B] = + * If endpoint is empty it will return provided default value. + */ + def withDefault[B >: A](default: => B)(using Monad[F]): Endpoint[F, B] = self.map(_.getOrElse(default)) /** - * If endpoint is empty it will return provided alternative. - */ - def orElse[B >: A](alternative: => Option[B])(implicit F: Monad[F]): Endpoint[F, Option[B]] = + * If endpoint is empty it will return provided alternative. + */ + def orElse[B >: A](alternative: => Option[B])(using Monad[F]): Endpoint[F, Option[B]] = self.map(_.orElse(alternative)) } - implicit final class ValueEndpointOps[F[_], B](val self: Endpoint[F, B]) extends AnyVal { + /* implicit final class ValueEndpointOps[F[_], B](val self: Endpoint[F, B]) extends AnyVal { - /** - * Converts this endpoint to one that returns any type with `B :: HNil` as its representation. - */ - def as[A](implicit gen: Generic.Aux[A, B :: HNil], F: Monad[F]): Endpoint[F, A] = - self.map(value => gen.from(value :: HNil)) - } + /** + * Converts this endpoint to one that returns any type with `B *: EmptyTuple` as its representation. + */ + def as[A](using gen: Generic.Aux[A, B *: EmptyTuple], F: Monad[F]): Endpoint[F, A] = + self.map(value => gen.from(value *: EmptyTuple)) + }*/ - private trait EndpointSemigroupK[F[_]] extends SemigroupK[Endpoint[F, *]] { + private trait EndpointSemigroupK[F[_]] extends SemigroupK[Endpoint[F, _]] { def combineK[A](x: Endpoint[F, A], y: Endpoint[F, A]): Endpoint[F, A] = x.coproduct(y) } - private trait EndpointMonoidK[F[_]] extends EndpointSemigroupK[F] with MonoidK[Endpoint[F, *]] { + private trait EndpointMonoidK[F[_]] extends EndpointSemigroupK[F] with MonoidK[Endpoint[F, _]] { def empty[A]: Endpoint[F, A] = Endpoint.empty[F, A] } - private class EndpointFunctor[F[_]: Monad] extends Functor[Endpoint[F, *]] { + private class EndpointFunctor[F[_]: Monad] extends Functor[Endpoint[F, _]] { def map[A, B](fa: Endpoint[F, A])(f: A => B): Endpoint[F, B] = fa.map(f) } - private class EndpointApply[F[_]](implicit F: MonadError[F, Throwable]) extends EndpointFunctor[F] with Apply[Endpoint[F, *]] { + private class EndpointApply[F[_]](using MonadError[F, Throwable]) extends EndpointFunctor[F] with Apply[Endpoint[F, _]] { def ap[A, B](ff: Endpoint[F, A => B])(fa: Endpoint[F, A]): Endpoint[F, B] = ff.productWith(fa)((f, a) => f(a)) } - private class EndpointApplicative[F[_]](implicit F: MonadError[F, Throwable]) extends EndpointApply[F] with Applicative[Endpoint[F, *]] { + private class EndpointApplicative[F[_]](using MonadError[F, Throwable]) extends EndpointApply[F] with Applicative[Endpoint[F, _]] { def pure[A](x: A): Endpoint[F, A] = Endpoint.const[F, A](x) } - private class EndpointAlternative[F[_]](implicit F: MonadError[F, Throwable]) + private class EndpointAlternative[F[_]](using MonadError[F, Throwable]) extends EndpointApplicative[F] with EndpointMonoidK[F] - with Alternative[Endpoint[F, *]] + with Alternative[Endpoint[F, _]] - implicit def endpointAlternative[F[_]](implicit F: MonadError[F, Throwable]): Alternative[Endpoint[F, *]] = + given endpointAlternative[F[_]](using MonadError[F, Throwable]): Alternative[Endpoint[F, _]] = new EndpointAlternative[F] - implicit def endpointApplicative[F[_]](implicit F: MonadError[F, Throwable]): Applicative[Endpoint[F, *]] = + given endpointApplicative[F[_]](using MonadError[F, Throwable]): Applicative[Endpoint[F, _]] = new EndpointApplicative[F] - implicit def endpointFunctor[F[_]: Monad]: Functor[Endpoint[F, *]] = + given endpointFunctor[F[_]: Monad]: Functor[Endpoint[F, _]] = new EndpointFunctor[F] - implicit def endpointMonoidK[F[_]]: MonoidK[Endpoint[F, *]] = + given endpointMonoidK[F[_]]: MonoidK[Endpoint[F, _]] = new EndpointMonoidK[F] {} /** - * Instantiates an [[EndpointModule]] for a given effect type `F`. This is enables better type - * inference when constucting endpoint instances. - * - * For example, `lift` infer the resulting endpoint based on the argument type (string): - * - * {{{ - * import io.finch._, cats.effect.IO - * val e = Endpoint[IO].lift("foo") // Endpoint[IO, String] - * }}} - */ + * Instantiates an [[EndpointModule]] for a given effect type `F`. This is enables better type inference when constucting endpoint instances. + * + * For example, `lift` infer the resulting endpoint based on the argument type (string): + * + * {{{ + * import io.finch._, cats.effect.IO + * val e = Endpoint[IO].lift("foo") // Endpoint[IO, String] + * }}} + */ def apply[F[_]]: EndpointModule[F] = EndpointModule[F] /** - * Convert [[Endpoint.Compiled]] into Finagle Service - */ + * Convert [[Endpoint.Compiled]] into Finagle Service + */ def toService[F[_]: Effect](compiled: Endpoint.Compiled[F]): Service[Request, Response] = ToService(compiled) /** - * Creates an empty [[Endpoint]] (an endpoint that never matches) for a given type. - */ + * Creates an empty [[Endpoint]] (an endpoint that never matches) for a given type. + */ def empty[F[_], A]: Endpoint[F, A] = new Endpoint[F, A] { final def apply(input: Input): Result[F, A] = EndpointResult.NotMatched[F] } /** - * An [[Endpoint]] that, when composed with other endpoints, doesn't change anything. - */ - def zero[F[_]](implicit F: Applicative[F]): Endpoint[F, HNil] = - new Endpoint[F, HNil] { - final def apply(input: Input): Result[F, HNil] = - EndpointResult.Matched(input, Trace.empty, F.pure(Output.HNil)) + * An [[Endpoint]] that, when composed with other endpoints, doesn't change anything. + */ + def zero[F[_]](using F: Applicative[F]): Endpoint[F, EmptyTuple] = + new Endpoint[F, EmptyTuple] { + final def apply(input: Input): Result[F, EmptyTuple] = + EndpointResult.Matched(input, Trace.empty, F.pure(Output.EmptyTuple)) } /** - * Creates an [[Endpoint]] that always matches and returns a given value (evaluated eagerly). - */ - def const[F[_], A](a: A)(implicit F: Applicative[F]): Endpoint[F, A] = + * Creates an [[Endpoint]] that always matches and returns a given value (evaluated eagerly). + */ + def const[F[_], A](a: A)(using F: Applicative[F]): Endpoint[F, A] = new Endpoint[F, A] { final def apply(input: Input): Result[F, A] = EndpointResult.Matched(input, Trace.empty, F.pure(Output.payload(a))) } /** - * Creates an [[Endpoint]] that always matches and returns a given value (evaluated lazily). - * - * This might be useful for wrapping functions returning arbitrary value within [[Endpoint]] - * context. - * - * Example: the following endpoint will recompute a random integer on each request. - * - * {{{ - * val nextInt: Endpoint[Int] = Endpoint.lift(scala.util.random.nextInt) - * }}} - */ - def lift[F[_], A](a: => A)(implicit F: Sync[F]): Endpoint[F, A] = + * Creates an [[Endpoint]] that always matches and returns a given value (evaluated lazily). + * + * This might be useful for wrapping functions returning arbitrary value within [[Endpoint]] context. + * + * Example: the following endpoint will recompute a random integer on each request. + * + * {{{ + * val nextInt: Endpoint[Int] = Endpoint.lift(scala.util.random.nextInt) + * }}} + */ + def lift[F[_], A](a: => A)(using F: Sync[F]): Endpoint[F, A] = new Endpoint[F, A] { final def apply(input: Input): Result[F, A] = EndpointResult.Matched(input, Trace.empty, F.delay(Output.payload(a))) } /** - * Creates an [[Endpoint]] that always matches and returns a given `F` (evaluated lazily). - */ - def liftAsync[F[_], A](fa: => F[A])(implicit F: Sync[F]): Endpoint[F, A] = + * Creates an [[Endpoint]] that always matches and returns a given `F` (evaluated lazily). + */ + def liftAsync[F[_], A](fa: => F[A])(using F: Sync[F]): Endpoint[F, A] = new Endpoint[F, A] { final def apply(input: Input): Result[F, A] = - EndpointResult.Matched(input, Trace.empty, F.suspend(fa).map(a => Output.payload(a))) + EndpointResult.Matched(input, Trace.empty, F.defer(fa).map(a => Output.payload(a))) } /** - * Creates an [[Endpoint]] that always matches and returns a given `Output` (evaluated lazily). - */ - def liftOutput[F[_], A](oa: => Output[A])(implicit F: Sync[F]): Endpoint[F, A] = + * Creates an [[Endpoint]] that always matches and returns a given `Output` (evaluated lazily). + */ + def liftOutput[F[_], A](oa: => Output[A])(using F: Sync[F]): Endpoint[F, A] = new Endpoint[F, A] { final def apply(input: Input): Result[F, A] = EndpointResult.Matched(input, Trace.empty, F.delay(oa)) } /** - * Creates an [[Endpoint]] that always matches and returns a given `F[Output]` - * (evaluated lazily). - */ - def liftOutputAsync[F[_], A](foa: => F[Output[A]])(implicit F: Sync[F]): Endpoint[F, A] = + * Creates an [[Endpoint]] that always matches and returns a given `F[Output]` (evaluated lazily). + */ + def liftOutputAsync[F[_], A](foa: => F[Output[A]])(using F: Sync[F]): Endpoint[F, A] = new Endpoint[F, A] { final def apply(input: Input): Result[F, A] = - EndpointResult.Matched(input, Trace.empty, F.suspend(foa)) + EndpointResult.Matched(input, Trace.empty, F.defer(foa)) } /** - * Creates an [[Endpoint]] from a given [[InputStream]]. Uses [[Resource]] for safer resource - * management and [[ContextShift]] for offloading blocking work from a worker pool. - * - * @see [[fromFile]] - */ - def fromInputStream[F[_]](stream: Resource[F, InputStream])(implicit + * Creates an [[Endpoint]] from a given [[InputStream]]. Uses [[Resource]] for safer resource management and [[ContextShift]] for offloading blocking work + * from a worker pool. + * + * @see + * [[fromFile]] + */ + def fromInputStream[F[_]](stream: Resource[F, InputStream])(using F: Sync[F], S: ContextShift[F] ): Endpoint[F, Buf] = new FromInputStream[F](stream) /** - * Creates an [[Endpoint]] from a given [[File]]. Uses [[Resource]] for safer resource - * management and [[ContextShift]] for offloading blocking work from a worker pool. - * - * @see [[fromInputStream]] - */ - def fromFile[F[_]](file: File)(implicit + * Creates an [[Endpoint]] from a given [[File]]. Uses [[Resource]] for safer resource management and [[ContextShift]] for offloading blocking work from a + * worker pool. + * + * @see + * [[fromInputStream]] + */ + def fromFile[F[_]](file: File)(using F: Sync[F], S: ContextShift[F] ): Endpoint[F, Buf] = @@ -713,37 +671,36 @@ object Endpoint { ) /** - * Creates an [[Endpoint]] that serves an asset (static content) from a Java classpath resource, - * located at `path`, as a static content. The returned endpoint will only match `GET` requests - * with path identical to asset's. - * - * This could be especially useful in local development, when throughput and latency matter less - * than quick iterations. These means, however, are not recommended for production usage. Web - * servers (Nginx, Apache) will do much better job serving static files. - * - * Example project structure: - * - * {{{ - * ├── scala - * │ └── Main.scala - * └── resources - * ├── index.html - * └── script.js - * }}} - * - * Example bootstrap: - * - * {{{ - * Bootstrap - * ... - * .serve[Text.Html](Endpoint[IO].classpathAsset("/index.html")) - * .serve[Application.Javascript](Endpoint[IO].classpathAsset("/script.js")) - * ... - * }}} - * - * @see https://docs.oracle.com/javase/8/docs/technotes/guides/lang/resources.html - */ - def classpathAsset[F[_]](path: String)(implicit + * Creates an [[Endpoint]] that serves an asset (static content) from a Java classpath resource, located at `path`, as a static content. The returned endpoint + * will only match `GET` requests with path identical to asset's. + * + * This could be especially useful in local development, when throughput and latency matter less than quick iterations. These means, however, are not + * recommended for production usage. Web servers (Nginx, Apache) will do much better job serving static files. + * + * Example project structure: + * + * {{{ + * ├── scala + * │ └── Main.scala + * └── resources + * ├── index.html + * └── script.js + * }}} + * + * Example bootstrap: + * + * {{{ + * Bootstrap + * ... + * .serve[Text.Html](Endpoint[IO].classpathAsset("/index.html")) + * .serve[Application.Javascript](Endpoint[IO].classpathAsset("/script.js")) + * ... + * }}} + * + * @see + * https://docs.oracle.com/javase/8/docs/technotes/guides/lang/resources.html + */ + def classpathAsset[F[_]](path: String)(using F: Sync[F], S: ContextShift[F] ): Endpoint[F, Buf] = { @@ -755,21 +712,20 @@ object Endpoint { } /** - * Creates an [[Endpoint]] that serves an asset (static content) from a filesystem, located at - * `path`, as a static content. The returned endpoint will only match `GET` requests with path - * identical to asset's. - * - * Example bootstrap: - * - * {{{ - * Bootstrap - * ... - * .serve[Text.Html](Endpoint[IO].filesystemAsset("index.html")) - * .serve[Application.Javascript](Endpoint[IO].filesystemAsset("script.js")) - * ... - * }}} - */ - def filesystemAsset[F[_]](path: String)(implicit + * Creates an [[Endpoint]] that serves an asset (static content) from a filesystem, located at `path`, as a static content. The returned endpoint will only + * match `GET` requests with path identical to asset's. + * + * Example bootstrap: + * + * {{{ + * Bootstrap + * ... + * .serve[Text.Html](Endpoint[IO].filesystemAsset("index.html")) + * .serve[Application.Javascript](Endpoint[IO].filesystemAsset("script.js")) + * ... + * }}} + */ + def filesystemAsset[F[_]](path: String)(using F: Sync[F], S: ContextShift[F] ): Endpoint[F, Buf] = { @@ -780,9 +736,9 @@ object Endpoint { } /** - * A root [[Endpoint]] that always matches and extracts the current request. - */ - def root[F[_]](implicit F: Sync[F]): Endpoint[F, Request] = + * A root [[Endpoint]] that always matches and extracts the current request. + */ + def root[F[_]](using F: Sync[F]): Endpoint[F, Request] = new Endpoint[F, Request] { final def apply(input: Input): Result[F, Request] = EndpointResult.Matched(input, Trace.empty, F.delay(Output.payload(input.request))) @@ -791,381 +747,351 @@ object Endpoint { } /** - * An [[Endpoint]] that always matches any path. - */ - def pathAny[F[_]](implicit F: Applicative[F]): Endpoint[F, HNil] = - new Endpoint[F, HNil] { - final def apply(input: Input): Result[F, HNil] = + * An [[Endpoint]] that always matches any path. + */ + def pathAny[F[_]](using F: Applicative[F]): Endpoint[F, EmptyTuple] = + new Endpoint[F, EmptyTuple] { + final def apply(input: Input): Result[F, EmptyTuple] = EndpointResult.Matched( input.withRoute(Nil), Trace.fromRoute(input.route), - F.pure(Output.HNil) + F.pure(Output.EmptyTuple) ) final override def toString: String = "*" } /** - * An [[Endpoint]] that matches an empty path. - */ - def pathEmpty[F[_]](implicit F: Applicative[F]): Endpoint[F, HNil] = - new Endpoint[F, HNil] { - final def apply(input: Input): Result[F, HNil] = - if (input.route.isEmpty) - EndpointResult.Matched(input, Trace.empty, F.pure(Output.HNil)) + * An [[Endpoint]] that matches an empty path. + */ + def pathEmpty[F[_]](using F: Applicative[F]): Endpoint[F, EmptyTuple] = + new Endpoint[F, EmptyTuple] { + final def apply(input: Input): Result[F, EmptyTuple] = + if input.route.isEmpty then EndpointResult.Matched(input, Trace.empty, F.pure(Output.EmptyTuple)) else EndpointResult.NotMatched[F] final override def toString: String = "" } /** - * A matching [[Endpoint]] that reads a value of type `A` (using the implicit - * [[DecodePath]] instances defined for `A`) from the current path segment. - */ + * A matching [[Endpoint]] that reads a value of type `A` (using the implicit [[DecodePath]] instances defined for `A`) from the current path segment. + */ def path[F[_]: Sync, A: DecodePath: ClassTag]: Endpoint[F, A] = new ExtractPath[F, A] /** - * A matching [[Endpoint]] that reads a tail value `A` (using the implicit - * [[DecodePath]] instances defined for `A`) from the entire path. - */ + * A matching [[Endpoint]] that reads a tail value `A` (using the implicit [[DecodePath]] instances defined for `A`) from the entire path. + */ def paths[F[_]: Sync, A: DecodePath: ClassTag]: Endpoint[F, List[A]] = new ExtractPaths[F, A] /** - * An [[Endpoint]] that matches a given string. - */ - def path[F[_]: Sync](s: String): Endpoint[F, HNil] = + * An [[Endpoint]] that matches a given string. + */ + def path[F[_]: Sync](s: String): Endpoint[F, EmptyTuple] = new MatchPath[F](s) /** - * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The - * resulting [[Endpoint]] succeeds on the request only if its method is `GET` and the underlying - * endpoint succeeds on it. - */ + * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its + * method is `GET` and the underlying endpoint succeeds on it. + */ def get[F[_], A](e: Endpoint[F, A]): Mappable[F, A] = new Method[F, A](FinagleMethod.Get, e) /** - * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The - * resulting [[Endpoint]] succeeds on the request only if its method is `POST` and the underlying - * endpoint succeeds on it. - */ + * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its + * method is `POST` and the underlying endpoint succeeds on it. + */ def post[F[_], A](e: Endpoint[F, A]): Mappable[F, A] = new Method[F, A](FinagleMethod.Post, e) /** - * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The - * resulting [[Endpoint]] succeeds on the request only if its method is `PATCH` and the underlying - * endpoint succeeds on it. - */ + * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its + * method is `PATCH` and the underlying endpoint succeeds on it. + */ def patch[F[_], A](e: Endpoint[F, A]): Mappable[F, A] = new Method[F, A](FinagleMethod.Patch, e) /** - * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The - * resulting [[Endpoint]] succeeds on the request only if its method is `DELETE` and the - * underlying endpoint succeeds on it. - */ + * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its + * method is `DELETE` and the underlying endpoint succeeds on it. + */ def delete[F[_], A](e: Endpoint[F, A]): Mappable[F, A] = new Method[F, A](FinagleMethod.Delete, e) /** - * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The - * resulting [[Endpoint]] succeeds on the request only if its method is `HEAD` and the underlying - * endpoint succeeds on it. - */ + * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its + * method is `HEAD` and the underlying endpoint succeeds on it. + */ def head[F[_], A](e: Endpoint[F, A]): Mappable[F, A] = new Method[F, A](FinagleMethod.Head, e) /** - * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The - * resulting [[Endpoint]] succeeds on the request only if its method is `OPTIONS` and the - * underlying endpoint succeeds on it. - */ + * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its + * method is `OPTIONS` and the underlying endpoint succeeds on it. + */ def options[F[_], A](e: Endpoint[F, A]): Mappable[F, A] = new Method[F, A](FinagleMethod.Options, e) /** - * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The - * resulting [[Endpoint]] succeeds on the request only if its method is `PUT` and the underlying - * endpoint succeeds on it. - */ + * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its + * method is `PUT` and the underlying endpoint succeeds on it. + */ def put[F[_], A](e: Endpoint[F, A]): Mappable[F, A] = new Method[F, A](FinagleMethod.Put, e) /** - * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The - * resulting [[Endpoint]] succeeds on the request only if its method is `TRACE` and the underlying - * router endpoint on it. - */ + * A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its + * method is `TRACE` and the underlying router endpoint on it. + */ def trace[F[_], A](e: Endpoint[F, A]): Mappable[F, A] = new Method[F, A](FinagleMethod.Trace, e) /** - * An evaluating [[Endpoint]] that reads a required HTTP header `name` from the request or raises - * an [[Error.NotPresent]] exception when the header is missing. - */ + * An evaluating [[Endpoint]] that reads a required HTTP header `name` from the request or raises an [[Error.NotPresent]] exception when the header is + * missing. + */ def header[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, A] = new Header[F, Id, A](name) with Header.Required[F, A] /** - * An evaluating [[Endpoint]] that reads an optional HTTP header `name` from the request into an - * `Option`. - */ + * An evaluating [[Endpoint]] that reads an optional HTTP header `name` from the request into an `Option`. + */ def headerOption[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, Option[A]] = new Header[F, Option, A](name) with Header.Optional[F, A] /** - * An evaluating [[Endpoint]] that reads a binary request body, interpreted as a `Array[Byte]`, - * into an `Option`. The returned [[Endpoint]] only matches non-chunked (non-streamed) requests. - */ + * An evaluating [[Endpoint]] that reads a binary request body, interpreted as a `Array[Byte]`, into an `Option`. The returned [[Endpoint]] only matches + * non-chunked (non-streamed) requests. + */ def binaryBodyOption[F[_]: Sync]: Endpoint[F, Option[Array[Byte]]] = new BinaryBody[F, Option[Array[Byte]]] with FullBody.Optional[F, Array[Byte]] /** - * An evaluating [[Endpoint]] that reads a required binary request body, interpreted as an - * `Array[Byte]`, or throws a [[Error.NotPresent]] exception. The returned [[Endpoint]] only - * matches non-chunked (non-streamed) requests. - */ + * An evaluating [[Endpoint]] that reads a required binary request body, interpreted as an `Array[Byte]`, or throws a [[Error.NotPresent]] exception. The + * returned [[Endpoint]] only matches non-chunked (non-streamed) requests. + */ def binaryBody[F[_]: Sync]: Endpoint[F, Array[Byte]] = new BinaryBody[F, Array[Byte]] with FullBody.Required[F, Array[Byte]] /** - * An evaluating [[Endpoint]] that reads an optional request body, interpreted as a `String`, into - * an `Option`. The returned [[Endpoint]] only matches non-chunked (non-streamed) requests. - */ + * An evaluating [[Endpoint]] that reads an optional request body, interpreted as a `String`, into an `Option`. The returned [[Endpoint]] only matches + * non-chunked (non-streamed) requests. + */ def stringBodyOption[F[_]: Sync]: Endpoint[F, Option[String]] = new StringBody[F, Option[String]] with FullBody.Optional[F, String] /** - * An evaluating [[Endpoint]] that reads the required request body, interpreted as a `String`, or - * throws an [[Error.NotPresent]] exception. The returned [[Endpoint]] only matches non-chunked - * (non-streamed) requests. - */ + * An evaluating [[Endpoint]] that reads the required request body, interpreted as a `String`, or throws an [[Error.NotPresent]] exception. The returned + * [[Endpoint]] only matches non-chunked (non-streamed) requests. + */ def stringBody[F[_]: Sync]: Endpoint[F, String] = new StringBody[F, String] with FullBody.Required[F, String] /** - * An [[Endpoint]] that reads an optional request body represented as `CT` (`ContentType`) and - * interpreted as `A`, into an `Option`. The returned [[Endpoint]] only matches non-chunked - * (non-streamed) requests. - */ - def bodyOption[F[_]: Sync, A: ClassTag, CT](implicit D: Decode.Dispatchable[A, CT]): Endpoint[F, Option[A]] = + * An [[Endpoint]] that reads an optional request body represented as `CT` (`ContentType`) and interpreted as `A`, into an `Option`. The returned [[Endpoint]] + * only matches non-chunked (non-streamed) requests. + */ + def bodyOption[F[_]: Sync, A: ClassTag, CT](using D: Decode.Dispatchable[A, CT]): Endpoint[F, Option[A]] = new Body[F, A, Option[A], CT] with FullBody.Optional[F, A] /** - * An [[Endpoint]] that reads the required request body represented as `CT` (`ContentType`) and - * interpreted as `A`, or throws an [[Error.NotPresent]] exception. The returned [[Endpoint]] - * only matches non-chunked (non-streamed) requests. - */ - def body[F[_]: Sync, A: ClassTag, CT](implicit d: Decode.Dispatchable[A, CT]): Endpoint[F, A] = + * An [[Endpoint]] that reads the required request body represented as `CT` (`ContentType`) and interpreted as `A`, or throws an [[Error.NotPresent]] + * exception. The returned [[Endpoint]] only matches non-chunked (non-streamed) requests. + */ + def body[F[_]: Sync, A: ClassTag, CT](using d: Decode.Dispatchable[A, CT]): Endpoint[F, A] = new Body[F, A, A, CT] with FullBody.Required[F, A] /** - * Alias for `body[F, A, Application.Json]`. - */ + * Alias for `body[F, A, Application.Json]`. + */ def jsonBody[F[_]: Sync, A: Decode.Json: ClassTag]: Endpoint[F, A] = body[F, A, Application.Json] /** - * Alias for `bodyOption[F, A, Application.Json]`. - */ + * Alias for `bodyOption[F, A, Application.Json]`. + */ def jsonBodyOption[F[_]: Sync, A: Decode.Json: ClassTag]: Endpoint[F, Option[A]] = bodyOption[F, A, Application.Json] /** - * Alias for `body[F, A, Text.Plain]` - */ + * Alias for `body[F, A, Text.Plain]` + */ def textBody[F[_]: Sync, A: Decode.Text: ClassTag]: Endpoint[F, A] = body[F, A, Text.Plain] /** - * Alias for `bodyOption[A, Text.Plain]` - */ + * Alias for `bodyOption[A, Text.Plain]` + */ def textBodyOption[F[_]: Sync, A: Decode.Text: ClassTag]: Endpoint[F, Option[A]] = bodyOption[F, A, Text.Plain] /** - * An [[Endpoint]] that matches chunked requests and lifts their content into a generic - * **binary** stream passed as a type parameter. This method, along with other `bodyStream` - * endpoints, are integration points with streaming libraries such as fs2 and iteratee. - * - * {{{ - * scala> import io.finch._, io.finch.iteratee._, cats.effect.IO, io.iteratee.Enumerator - * - * scala> val bin = Endpoint[IO].binaryBodyStream[Enumerator] - * bin: Endpoint[IO, Enumerator[IO, Array[Byte]]] = binaryBodyStream - * }}} - */ - def binaryBodyStream[F[_]: Sync, S[_[_], _]](implicit + * An [[Endpoint]] that matches chunked requests and lifts their content into a generic **binary** stream passed as a type parameter. This method, along with + * other `bodyStream` endpoints, are integration points with streaming libraries such as fs2 and iteratee. + * + * {{{ + * scala> import io.finch._, io.finch.iteratee._, cats.effect.IO, io.iteratee.Enumerator + * + * scala> val bin = Endpoint[IO].binaryBodyStream[Enumerator] + * bin: Endpoint[IO, Enumerator[IO, Array[Byte]]] = binaryBodyStream + * }}} + */ + def binaryBodyStream[F[_]: Sync, S[_[_], _]](using LR: LiftReader[S, F] ): Endpoint[F, S[F, Array[Byte]]] = new BinaryBodyStream[F, S] /** - * An [[Endpoint]] that matches chunked requests and lifts their content into a generic - * **string** stream passed as a type parameter. This method, along with other `bodyStream` - * endpoints, are integration points with streaming libraries such as fs2 and iteratee. - * - * {{{ - * scala> import io.finch._, io.finch.iteratee._, cats.effect.IO, io.iteratee.Enumerator - * - * scala> val bin = Endpoint[IO].stringBodyStream[Enumerator] - * bin: Endpoint[IO, Enumerator[IO, String]] = stringBodyStream - * }}} - */ - def stringBodyStream[F[_]: Sync, S[_[_], _]](implicit + * An [[Endpoint]] that matches chunked requests and lifts their content into a generic **string** stream passed as a type parameter. This method, along with + * other `bodyStream` endpoints, are integration points with streaming libraries such as fs2 and iteratee. + * + * {{{ + * scala> import io.finch._, io.finch.iteratee._, cats.effect.IO, io.iteratee.Enumerator + * + * scala> val bin = Endpoint[IO].stringBodyStream[Enumerator] + * bin: Endpoint[IO, Enumerator[IO, String]] = stringBodyStream + * }}} + */ + def stringBodyStream[F[_]: Sync, S[_[_], _]](using LR: LiftReader[S, F] ): Endpoint[F, S[F, String]] = new StringBodyStream[F, S] /** - * An [[Endpoint]] that matches chunked requests and lifts their content into a generic - * stream passed as a type parameter. This method, along with other `bodyStream` - * endpoints, are integration points with streaming libraries such as fs2 and iteratee. - * - * When, for example, JSON library is import, this endpoint can parse an inbound JSON stream. - * - * {{{ - * scala> import io.finch._, io.finch.iteratee._, cats.effect.IO, io.iteratee.Enumerator - * - * scala> import io.finch.circe._, io.circe.generic.auto._ - * - * scala> case class Foo(s: String) - * - * scala> val json = Endpoint[IO].bodyStream[Enumerator, Foo, Application.Json] - * bin: Endpoint[IO, Enumerator[IO, Foo]] = bodyStream - * }}} - */ - def bodyStream[F[_]: Sync, S[_[_], _], A, CT <: String](implicit + * An [[Endpoint]] that matches chunked requests and lifts their content into a generic stream passed as a type parameter. This method, along with other + * `bodyStream` endpoints, are integration points with streaming libraries such as fs2 and iteratee. + * + * When, for example, JSON library is import, this endpoint can parse an inbound JSON stream. + * + * {{{ + * scala> import io.finch._, io.finch.iteratee._, cats.effect.IO, io.iteratee.Enumerator + * + * scala> import io.finch.circe._, io.circe.generic.auto._ + * + * scala> case class Foo(s: String) + * + * scala> val json = Endpoint[IO].bodyStream[Enumerator, Foo, Application.Json] + * bin: Endpoint[IO, Enumerator[IO, Foo]] = bodyStream + * }}} + */ + def bodyStream[F[_]: Sync, S[_[_], _], A, CT <: String](using LR: LiftReader[S, F], A: DecodeStream.Aux[S, F, A, CT] ): Endpoint[F, S[F, A]] = new BodyStream[F, S, A, CT] /** - * See [[bodyStream]]. This is just an alias for `bodyStream[?, ?, Application.Json]`. - */ - def jsonBodyStream[F[_]: Sync, S[_[_], _], A](implicit + * See [[bodyStream]]. This is just an alias for `bodyStream[?, ?, Application.Json]`. + */ + def jsonBodyStream[F[_]: Sync, S[_[_], _], A](using LR: LiftReader[S, F], A: DecodeStream.Aux[S, F, A, Application.Json] ): Endpoint[F, S[F, A]] = bodyStream[F, S, A, Application.Json] /** - * See [[bodyStream]]. This is just an alias for `bodyStream[?, ?, Text.Plain]`. - */ - def textBodyStream[F[_]: Sync, S[_[_], _], A](implicit + * See [[bodyStream]]. This is just an alias for `bodyStream[?, ?, Text.Plain]`. + */ + def textBodyStream[F[_]: Sync, S[_[_], _], A](using LR: LiftReader[S, F], A: DecodeStream.Aux[S, F, A, Text.Plain] ): Endpoint[F, S[F, A]] = bodyStream[F, S, A, Text.Plain] /** - * An evaluating [[Endpoint]] that reads an optional HTTP cookie from the request into an - * `Option`. - */ + * An evaluating [[Endpoint]] that reads an optional HTTP cookie from the request into an `Option`. + */ def cookieOption[F[_]: Sync](name: String): Endpoint[F, Option[FinagleCookie]] = new Cookie[F, Option[FinagleCookie]](name) with Cookie.Optional[F] /** - * An evaluating [[Endpoint]] that reads a required cookie from the request or raises an - * [[Error.NotPresent]] exception when the cookie is missing. - */ + * An evaluating [[Endpoint]] that reads a required cookie from the request or raises an [[Error.NotPresent]] exception when the cookie is missing. + */ def cookie[F[_]: Sync](name: String): Endpoint[F, FinagleCookie] = new Cookie[F, FinagleCookie](name) with Cookie.Required[F] /** - * An evaluating [[Endpoint]] that reads an optional query-string param `name` from the request - * into an `Option`. - */ + * An evaluating [[Endpoint]] that reads an optional query-string param `name` from the request into an `Option`. + */ def paramOption[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, Option[A]] = new Param[F, Option, A](name) with Param.Optional[F, A] /** - * An evaluating [[Endpoint]] that reads a required query-string param `name` from the - * request or raises an [[Error.NotPresent]] exception when the param is missing; an - * [[Error.NotValid]] exception is the param is empty. - */ + * An evaluating [[Endpoint]] that reads a required query-string param `name` from the request or raises an [[Error.NotPresent]] exception when the param is + * missing; an [[Error.NotValid]] exception is the param is empty. + */ def param[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, A] = new Param[F, Id, A](name) with Param.Required[F, A] /** - * An evaluating [[Endpoint]] that reads an optional (in a meaning that a resulting - * `Seq` may be empty) multi-value query-string param `name` from the request into a `Seq`. - */ + * An evaluating [[Endpoint]] that reads an optional (in a meaning that a resulting `Seq` may be empty) multi-value query-string param `name` from the request + * into a `Seq`. + */ def params[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, List[A]] = new Params[F, List, A](name) with Params.AllowEmpty[F, A] /** - * An evaluating [[Endpoint]] that reads a required multi-value query-string param `name` - * from the request into a `NonEmptyList` or raises a [[Error.NotPresent]] exception - * when the params are missing or empty. - */ + * An evaluating [[Endpoint]] that reads a required multi-value query-string param `name` from the request into a `NonEmptyList` or raises a + * [[Error.NotPresent]] exception when the params are missing or empty. + */ def paramsNel[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, NonEmptyList[A]] = new Params[F, NonEmptyList, A](name) with Params.NonEmpty[F, A] /** - * An evaluating [[Endpoint]] that reads an optional file upload from a `multipart/form-data` - * request into an `Option`. - */ + * An evaluating [[Endpoint]] that reads an optional file upload from a `multipart/form-data` request into an `Option`. + */ def multipartFileUploadOption[F[_]: Sync](name: String): Endpoint[F, Option[FinagleMultipart.FileUpload]] = new FileUpload[F, Option](name) with FileUpload.Optional[F] /** - * An evaluating [[Endpoint]] that reads a required file upload from a `multipart/form-data` - * request. - */ + * An evaluating [[Endpoint]] that reads a required file upload from a `multipart/form-data` request. + */ def multipartFileUpload[F[_]: Sync](name: String): Endpoint[F, FinagleMultipart.FileUpload] = new FileUpload[F, Id](name) with FileUpload.Required[F] /** - * An evaluating [[Endpoint]] that optionally reads multiple file uploads from a - * `multipart/form-data` request. - */ + * An evaluating [[Endpoint]] that optionally reads multiple file uploads from a `multipart/form-data` request. + */ def multipartFileUploads[F[_]: Sync](name: String): Endpoint[F, List[FinagleMultipart.FileUpload]] = new FileUpload[F, List](name) with FileUpload.AllowEmpty[F] /** - * An evaluating [[Endpoint]] that requires multiple file uploads from a `multipart/form-data` - * request. - */ + * An evaluating [[Endpoint]] that requires multiple file uploads from a `multipart/form-data` request. + */ def multipartFileUploadsNel[F[_]: Sync](name: String): Endpoint[F, NonEmptyList[FinagleMultipart.FileUpload]] = new FileUpload[F, NonEmptyList](name) with FileUpload.NonEmpty[F] /** - * An evaluating [[Endpoint]] that reads a required attribute from a `multipart/form-data` - * request. - */ + * An evaluating [[Endpoint]] that reads a required attribute from a `multipart/form-data` request. + */ def multipartAttribute[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, A] = new Attribute[F, Id, A](name) with Attribute.Required[F, A] with Attribute.SingleError[F, Id, A] /** - * An evaluating [[Endpoint]] that reads an optional attribute from a `multipart/form-data` - * request. - */ + * An evaluating [[Endpoint]] that reads an optional attribute from a `multipart/form-data` request. + */ def multipartAttributeOption[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, Option[A]] = new Attribute[F, Option, A](name) with Attribute.Optional[F, A] with Attribute.SingleError[F, Option, A] /** - * An evaluating [[Endpoint]] that reads a required attribute from a `multipart/form-data` - * request. - */ + * An evaluating [[Endpoint]] that reads a required attribute from a `multipart/form-data` request. + */ def multipartAttributes[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, List[A]] = new Attribute[F, List, A](name) with Attribute.AllowEmpty[F, A] with Attribute.MultipleErrors[F, List, A] /** - * An evaluating [[Endpoint]] that reads a required attribute from a `multipart/form-data` - * request. - */ + * An evaluating [[Endpoint]] that reads a required attribute from a `multipart/form-data` request. + */ def multipartAttributesNel[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, NonEmptyList[A]] = new Attribute[F, NonEmptyList, A](name) with Attribute.NonEmpty[F, A] with Attribute.MultipleErrors[F, NonEmptyList, A] /** - * Sequentially composes the given `endpoints` by using [[Endpoint!.coproduct]]. - * - * The resulting endpoint will match if at least one of the provided endpoints matches. - * If the sequence of provided endpoints is empty, the empty endpoint is returned, which never matches. - * - * @see [[Endpoint!.coproduct]] for the exact composition semantics. - * @see [[Endpoint.empty]] for the semantics of the empty endpoint. - */ + * Sequentially composes the given `endpoints` by using [[Endpoint!.coproduct]]. + * + * The resulting endpoint will match if at least one of the provided endpoints matches. If the sequence of provided endpoints is empty, the empty endpoint is + * returned, which never matches. + * + * @see + * [[Endpoint!.coproduct]] for the exact composition semantics. + * @see + * [[Endpoint.empty]] for the semantics of the empty endpoint. + */ def coproductAll[F[_], A](endpoints: Endpoint[F, A]*): Endpoint[F, A] = - if (endpoints.isEmpty) empty else endpoints.reduce(_ coproduct _) + if endpoints.isEmpty then empty else endpoints.reduce(_ coproduct _) } diff --git a/core/src/main/scala/io/finch/EndpointModule.scala b/core/src/main/scala/io/finch/EndpointModule.scala index 1aff561db..e02fd8417 100644 --- a/core/src/main/scala/io/finch/EndpointModule.scala +++ b/core/src/main/scala/io/finch/EndpointModule.scala @@ -10,96 +10,93 @@ import cats.effect.{ContextShift, Resource, Sync} import com.twitter.finagle.http.exp.Multipart import com.twitter.finagle.http.{Cookie, Request} import com.twitter.io.Buf -import shapeless.HNil /** - * Enables users to construct [[Endpoint]] instances without specifying the effect type `F[_]` every - * time. - * - * For example, via extending the `Endpoint.Module[F[_]]`: - * - * {{{ - * import io.finch._ - * import io.cats.effect.IO - * - * object Main extends App with Endpoint.Module[IO] { - * def foo = path("foo") - * } - * }}} - * - * It's also possible to instantiate an [[EndpointModule]] for a given effect and import its symbols - * into the score. For example: - * - * {{{ - * import io.finch._ - * import io.cats.effect.IO - * - * object Main extends App { - * val io = Endpoint[IO] - * import io._ - * - * def foo = path("foo") - * } - * }}} - * - * There is a pre-defined [[EndpointModule]] for Cats' `IO`, available via the import: - * - * {{{ - * import io.finch._ - * import io.finch.catsEffect._ - * - * object Main extends App { - * def foo = path("foo") - * } - * }}} - */ + * Enables users to construct [[Endpoint]] instances without specifying the effect type `F[_]` every time. + * + * For example, via extending the `Endpoint.Module[F[_]]`: + * + * {{{ + * import io.finch._ + * import io.cats.effect.IO + * + * object Main extends App with Endpoint.Module[IO] { + * def foo = path("foo") + * } + * }}} + * + * It's also possible to instantiate an [[EndpointModule]] for a given effect and import its symbols into the score. For example: + * + * {{{ + * import io.finch._ + * import io.cats.effect.IO + * + * object Main extends App { + * val io = Endpoint[IO] + * import io._ + * + * def foo = path("foo") + * } + * }}} + * + * There is a pre-defined [[EndpointModule]] for Cats' `IO`, available via the import: + * + * {{{ + * import io.finch._ + * import io.finch.catsEffect._ + * + * object Main extends App { + * def foo = path("foo") + * } + * }}} + */ trait EndpointModule[F[_]] { /** - * An alias for [[Endpoint.empty]]. - */ + * An alias for [[Endpoint.empty]]. + */ def empty[A]: Endpoint[F, A] = Endpoint.empty[F, A] /** - * An alias for [[Endpoint.zero]]. - */ - def zero(implicit F: Applicative[F]): Endpoint[F, HNil] = + * An alias for [[Endpoint.zero]]. + */ + def zero(implicit F: Applicative[F]): Endpoint[F, EmptyTuple] = Endpoint.zero[F] /** - * An alias for [[Endpoint.const]]. - */ + * An alias for [[Endpoint.const]]. + */ def const[A](a: A)(implicit F: Applicative[F]): Endpoint[F, A] = Endpoint.const[F, A](a) /** - * An alias for [[Endpoint.lift()]]. - */ + * An alias for [[Endpoint.lift()]]. + */ def lift[A](a: => A)(implicit F: Sync[F]): Endpoint[F, A] = Endpoint.lift[F, A](a) /** - * An alias for [[Endpoint.liftAsync]]. - */ + * An alias for [[Endpoint.liftAsync]]. + */ def liftAsync[A](fa: => F[A])(implicit F: Sync[F]): Endpoint[F, A] = Endpoint.liftAsync[F, A](fa) /** - * An alias for [[Endpoint.liftOutput]]. - */ + * An alias for [[Endpoint.liftOutput]]. + */ def liftOutput[A](oa: => Output[A])(implicit F: Sync[F]): Endpoint[F, A] = Endpoint.liftOutput[F, A](oa) /** - * An alias for [[Endpoint.liftOutputAsync]]. - */ + * An alias for [[Endpoint.liftOutputAsync]]. + */ def liftOutputAsync[A](foa: => F[Output[A]])(implicit F: Sync[F]): Endpoint[F, A] = Endpoint.liftOutputAsync[F, A](foa) /** - * An alias for [[Endpoint.fromInputStream]]. - */ + * An alias for [[Endpoint.fromInputStream]]. + */ def fromInputStream(stream: Resource[F, InputStream])(implicit F: Sync[F], S: ContextShift[F] @@ -107,8 +104,8 @@ trait EndpointModule[F[_]] { Endpoint.fromInputStream[F](stream) /** - * An alias for [[Endpoint.fromFile]]. - */ + * An alias for [[Endpoint.fromFile]]. + */ def fromFile(file: File)(implicit F: Sync[F], S: ContextShift[F] @@ -116,195 +113,195 @@ trait EndpointModule[F[_]] { Endpoint.fromFile[F](file) /** - * An alias for [[Endpoint.classpathAsset]]. - */ + * An alias for [[Endpoint.classpathAsset]]. + */ def classpathAsset(path: String)(implicit F: Sync[F], S: ContextShift[F]): Endpoint[F, Buf] = Endpoint.classpathAsset[F](path) /** - * An alias for [[Endpoint.classpathAsset]]. - */ + * An alias for [[Endpoint.classpathAsset]]. + */ def filesystemAsset(path: String)(implicit F: Sync[F], S: ContextShift[F]): Endpoint[F, Buf] = Endpoint.filesystemAsset[F](path) /** - * An alias for [[Endpoint.root]]. - */ + * An alias for [[Endpoint.root]]. + */ def root(implicit F: Sync[F]): Endpoint[F, Request] = Endpoint.root[F] /** - * An alias for [[Endpoint.pathAny]]. - */ - def pathAny(implicit F: Applicative[F]): Endpoint[F, HNil] = + * An alias for [[Endpoint.pathAny]]. + */ + def pathAny(implicit F: Applicative[F]): Endpoint[F, EmptyTuple] = Endpoint.pathAny[F] /** - * An alias for [[Endpoint.pathEmpty]]. - */ - def pathEmpty(implicit F: Applicative[F]): Endpoint[F, HNil] = + * An alias for [[Endpoint.pathEmpty]]. + */ + def pathEmpty(implicit F: Applicative[F]): Endpoint[F, EmptyTuple] = Endpoint.pathEmpty[F] /** - * An alias for [[Endpoint.path]]. - */ + * An alias for [[Endpoint.path]]. + */ def path[A: DecodePath: ClassTag](implicit F: Sync[F]): Endpoint[F, A] = Endpoint.path[F, A] /** - * An alias for [[Endpoint.paths]]. - */ + * An alias for [[Endpoint.paths]]. + */ def paths[A: DecodePath: ClassTag](implicit F: Sync[F]): Endpoint[F, List[A]] = Endpoint.paths[F, A] /** - * An alias for [[Endpoint.path]]. - * - * @note This method is implicit such that an implicit conversion `String => Endpoint[F, HNil]` - * works. - */ - implicit def path(s: String)(implicit F: Sync[F]): Endpoint[F, HNil] = + * An alias for [[Endpoint.path]]. + * + * @note + * This method is implicit such that an implicit conversion `String => Endpoint[F, EmptyTuple]` works. + */ + implicit def path(s: String)(implicit F: Sync[F]): Endpoint[F, EmptyTuple] = Endpoint.path[F](s) /** - * An alias for [[Endpoint.get]]. - */ + * An alias for [[Endpoint.get]]. + */ def get[A](e: Endpoint[F, A]): Endpoint.Mappable[F, A] = Endpoint.get[F, A](e) /** - * An alias for [[Endpoint.post]]. - */ + * An alias for [[Endpoint.post]]. + */ def post[A](e: Endpoint[F, A]): Endpoint.Mappable[F, A] = Endpoint.post[F, A](e) /** - * An alias for [[Endpoint.patch]]. - */ + * An alias for [[Endpoint.patch]]. + */ def patch[A](e: Endpoint[F, A]): Endpoint.Mappable[F, A] = Endpoint.patch[F, A](e) /** - * An alias for [[Endpoint.delete]]. - */ + * An alias for [[Endpoint.delete]]. + */ def delete[A](e: Endpoint[F, A]): Endpoint.Mappable[F, A] = Endpoint.delete[F, A](e) /** - * An alias for [[Endpoint.head]]. - */ + * An alias for [[Endpoint.head]]. + */ def head[A](e: Endpoint[F, A]): Endpoint.Mappable[F, A] = Endpoint.head[F, A](e) /** - * An alias for [[Endpoint.options]]. - */ + * An alias for [[Endpoint.options]]. + */ def options[A](e: Endpoint[F, A]): Endpoint.Mappable[F, A] = Endpoint.options[F, A](e) /** - * An alias for [[Endpoint.put]]. - */ + * An alias for [[Endpoint.put]]. + */ def put[A](e: Endpoint[F, A]): Endpoint.Mappable[F, A] = Endpoint.put[F, A](e) /** - * An alias for [[Endpoint.trace]]. - */ + * An alias for [[Endpoint.trace]]. + */ def trace[A](e: Endpoint[F, A]): Endpoint.Mappable[F, A] = Endpoint.trace[F, A](e) /** - * An alias for [[Endpoint.header]]. - */ + * An alias for [[Endpoint.header]]. + */ def header[A: DecodeEntity: ClassTag](name: String)(implicit F: Sync[F]): Endpoint[F, A] = Endpoint.header[F, A](name) /** - * An alias for [[Endpoint.headerOption]]. - */ + * An alias for [[Endpoint.headerOption]]. + */ def headerOption[A: DecodeEntity: ClassTag](name: String)(implicit F: Sync[F]): Endpoint[F, Option[A]] = Endpoint.headerOption[F, A](name) /** - * An alias for [[Endpoint.binaryBodyOption]]. - */ + * An alias for [[Endpoint.binaryBodyOption]]. + */ def binaryBodyOption(implicit F: Sync[F]): Endpoint[F, Option[Array[Byte]]] = Endpoint.binaryBodyOption[F] /** - * An alias for [[Endpoint.binaryBody]]. - */ + * An alias for [[Endpoint.binaryBody]]. + */ def binaryBody(implicit F: Sync[F]): Endpoint[F, Array[Byte]] = Endpoint.binaryBody[F] /** - * An alias for [[Endpoint.stringBodyOption]]. - */ + * An alias for [[Endpoint.stringBodyOption]]. + */ def stringBodyOption(implicit F: Sync[F]): Endpoint[F, Option[String]] = Endpoint.stringBodyOption[F] /** - * An alias for [[Endpoint.stringBody]]. - */ + * An alias for [[Endpoint.stringBody]]. + */ def stringBody(implicit F: Sync[F]): Endpoint[F, String] = Endpoint.stringBody[F] /** - * An alias for [[Endpoint.bodyOption]]. - */ + * An alias for [[Endpoint.bodyOption]]. + */ def bodyOption[A: ClassTag, CT](implicit F: Sync[F], D: Decode.Dispatchable[A, CT]): Endpoint[F, Option[A]] = Endpoint.bodyOption[F, A, CT] /** - * An alias for [[Endpoint.body]]. - */ + * An alias for [[Endpoint.body]]. + */ def body[A: ClassTag, CT](implicit D: Decode.Dispatchable[A, CT], F: Sync[F]): Endpoint[F, A] = Endpoint.body[F, A, CT] /** - * An alias for [[Endpoint.jsonBody]]. - */ + * An alias for [[Endpoint.jsonBody]]. + */ def jsonBody[A: Decode.Json: ClassTag](implicit F: Sync[F]): Endpoint[F, A] = Endpoint.jsonBody[F, A] /** - * An alias for [[Endpoint.jsonBodyOption]]. - */ + * An alias for [[Endpoint.jsonBodyOption]]. + */ def jsonBodyOption[A: Decode.Json: ClassTag](implicit F: Sync[F]): Endpoint[F, Option[A]] = Endpoint.jsonBodyOption[F, A] /** - * An alias for [[Endpoint.textBody]]. - */ + * An alias for [[Endpoint.textBody]]. + */ def textBody[A: Decode.Text: ClassTag](implicit F: Sync[F]): Endpoint[F, A] = Endpoint.textBody[F, A] /** - * An alias for [[Endpoint.textBodyOption]]. - */ + * An alias for [[Endpoint.textBodyOption]]. + */ def textBodyOption[A: Decode.Text: ClassTag](implicit F: Sync[F]): Endpoint[F, Option[A]] = Endpoint.textBodyOption[F, A] /** - * An alias for [[Endpoint.binaryBodyStream]]. - */ + * An alias for [[Endpoint.binaryBodyStream]]. + */ def binaryBodyStream[S[_[_], _]](implicit F: Sync[F], LR: LiftReader[S, F] ): Endpoint[F, S[F, Array[Byte]]] = Endpoint.binaryBodyStream[F, S] /** - * An alias for [[Endpoint.stringBodyStream]]. - */ + * An alias for [[Endpoint.stringBodyStream]]. + */ def stringBodyStream[S[_[_], _]](implicit F: Sync[F], LR: LiftReader[S, F] ): Endpoint[F, S[F, String]] = Endpoint.stringBodyStream[F, S] /** - * An alias for [[Endpoint.bodyStream]]. - */ + * An alias for [[Endpoint.bodyStream]]. + */ def bodyStream[S[_[_], _], A, CT <: String](implicit F: Sync[F], LR: LiftReader[S, F], @@ -312,8 +309,8 @@ trait EndpointModule[F[_]] { ): Endpoint[F, S[F, A]] = Endpoint.bodyStream[F, S, A, CT] /** - * An alias for [[Endpoint.jsonBodyStream]]. - */ + * An alias for [[Endpoint.jsonBodyStream]]. + */ def jsonBodyStream[S[_[_], _], A](implicit F: Sync[F], LR: LiftReader[S, F], @@ -321,8 +318,8 @@ trait EndpointModule[F[_]] { ): Endpoint[F, S[F, A]] = Endpoint.jsonBodyStream[F, S, A] /** - * An alias for [[Endpoint.textBodyStream]]. - */ + * An alias for [[Endpoint.textBodyStream]]. + */ def textBodyStream[S[_[_], _], A](implicit F: Sync[F], LR: LiftReader[S, F], @@ -330,86 +327,86 @@ trait EndpointModule[F[_]] { ): Endpoint[F, S[F, A]] = Endpoint.textBodyStream[F, S, A] /** - * An alias for [[Endpoint.cookieOption]]. - */ + * An alias for [[Endpoint.cookieOption]]. + */ def cookieOption(name: String)(implicit F: Sync[F]): Endpoint[F, Option[Cookie]] = Endpoint.cookieOption[F](name) /** - * An alias for [[Endpoint.cookie]]. - */ + * An alias for [[Endpoint.cookie]]. + */ def cookie(name: String)(implicit F: Sync[F]): Endpoint[F, Cookie] = Endpoint.cookie[F](name) /** - * An alias for [[Endpoint.paramOption]]. - */ + * An alias for [[Endpoint.paramOption]]. + */ def paramOption[A: DecodeEntity: ClassTag](name: String)(implicit F: Sync[F]): Endpoint[F, Option[A]] = Endpoint.paramOption[F, A](name) /** - * An alias for [[Endpoint.param]]. - */ + * An alias for [[Endpoint.param]]. + */ def param[A: DecodeEntity: ClassTag](name: String)(implicit F: Sync[F]): Endpoint[F, A] = Endpoint.param[F, A](name) /** - * An alias for [[Endpoint.params]]. - */ + * An alias for [[Endpoint.params]]. + */ def params[A: DecodeEntity: ClassTag](name: String)(implicit F: Sync[F]): Endpoint[F, List[A]] = Endpoint.params[F, A](name) /** - * An alias for [[Endpoint.paramsNel]]. - */ + * An alias for [[Endpoint.paramsNel]]. + */ def paramsNel[A: DecodeEntity: ClassTag](name: String)(implicit F: Sync[F]): Endpoint[F, NonEmptyList[A]] = Endpoint.paramsNel[F, A](name) /** - * An alias for [[Endpoint.multipartFileUploadOption]]. - */ + * An alias for [[Endpoint.multipartFileUploadOption]]. + */ def multipartFileUploadOption(name: String)(implicit F: Sync[F]): Endpoint[F, Option[Multipart.FileUpload]] = Endpoint.multipartFileUploadOption[F](name) /** - * An alias for [[Endpoint.multipartFileUpload]]. - */ + * An alias for [[Endpoint.multipartFileUpload]]. + */ def multipartFileUpload(name: String)(implicit F: Sync[F]): Endpoint[F, Multipart.FileUpload] = Endpoint.multipartFileUpload[F](name) /** - * An alias for [[Endpoint.multipartFileUploads]]. - */ + * An alias for [[Endpoint.multipartFileUploads]]. + */ def multipartFileUploads(name: String)(implicit F: Sync[F]): Endpoint[F, List[Multipart.FileUpload]] = Endpoint.multipartFileUploads[F](name) /** - * An alias for [[Endpoint.multipartFileUploadsNel]]. - */ + * An alias for [[Endpoint.multipartFileUploadsNel]]. + */ def multipartFileUploadsNel(name: String)(implicit F: Sync[F]): Endpoint[F, NonEmptyList[Multipart.FileUpload]] = Endpoint.multipartFileUploadsNel[F](name) /** - * An alias for [[Endpoint.multipartAttribute]]. - */ + * An alias for [[Endpoint.multipartAttribute]]. + */ def multipartAttribute[A: DecodeEntity: ClassTag](name: String)(implicit F: Sync[F]): Endpoint[F, A] = Endpoint.multipartAttribute[F, A](name) /** - * An alias for [[Endpoint.multipartAttributeOption]]. - */ + * An alias for [[Endpoint.multipartAttributeOption]]. + */ def multipartAttributeOption[A: DecodeEntity: ClassTag](name: String)(implicit F: Sync[F]): Endpoint[F, Option[A]] = Endpoint.multipartAttributeOption[F, A](name) /** - * An alias for [[Endpoint.multipartAttributes]]. - */ + * An alias for [[Endpoint.multipartAttributes]]. + */ def multipartAttributes[A: DecodeEntity: ClassTag](name: String)(implicit F: Sync[F]): Endpoint[F, List[A]] = Endpoint.multipartAttributes[F, A](name) /** - * An alias for [[Endpoint.multipartAttributesNel]]. - */ + * An alias for [[Endpoint.multipartAttributesNel]]. + */ def multipartAttributesNel[A: DecodeEntity: ClassTag](name: String)(implicit F: Sync[F]): Endpoint[F, NonEmptyList[A]] = Endpoint.multipartAttributesNel[F, A](name) } diff --git a/core/src/main/scala/io/finch/EndpointResult.scala b/core/src/main/scala/io/finch/EndpointResult.scala index 34c2d1178..d0dc2386d 100644 --- a/core/src/main/scala/io/finch/EndpointResult.scala +++ b/core/src/main/scala/io/finch/EndpointResult.scala @@ -8,38 +8,37 @@ import com.twitter.finagle.http.Method import com.twitter.util._ /** - * A result returned from an [[Endpoint]]. This models `Option[(Input, Future[Output])]` and - * represents two cases: - * - * - Endpoint is matched (think of 200). - * - Endpoint is not matched (think of 404, 405, etc). - * - * In its current state, `EndpointResult.NotMatched` represented with two cases: - * - * - `EndpointResult.NotMatched` (very generic result usually indicating 404) - * - `EndpointResult.NotMatched.MethodNotAllowed` (indicates 405) - */ + * A result returned from an [[Endpoint]]. This models `Option[(Input, Future[Output])]` and represents two cases: + * + * - Endpoint is matched (think of 200). + * - Endpoint is not matched (think of 404, 405, etc). + * + * In its current state, `EndpointResult.NotMatched` represented with two cases: + * + * - `EndpointResult.NotMatched` (very generic result usually indicating 404) + * - `EndpointResult.NotMatched.MethodNotAllowed` (indicates 405) + */ sealed abstract class EndpointResult[F[_], +A] { /** - * Whether the [[Endpoint]] is matched on a given [[Input]]. - */ + * Whether the [[Endpoint]] is matched on a given [[Input]]. + */ final def isMatched: Boolean = this match { case EndpointResult.Matched(_, _, _) => true case _ => false } /** - * Returns the remainder of the [[Input]] after an [[Endpoint]] is matched. - */ + * Returns the remainder of the [[Input]] after an [[Endpoint]] is matched. + */ final def remainder: Option[Input] = this match { case EndpointResult.Matched(rem, _, _) => Some(rem) case _ => None } /** - * Returns the [[Trace]] if an [[Endpoint]] is matched. - */ + * Returns the [[Trace]] if an [[Endpoint]] is matched. + */ final def trace: Option[Trace] = this match { case EndpointResult.Matched(_, trc, _) => Some(trc) case _ => None @@ -47,10 +46,12 @@ sealed abstract class EndpointResult[F[_], +A] { def awaitOutput(d: Duration = Duration.Inf)(implicit F: Effect[F]): Option[Either[Throwable, Output[A]]] = this match { case EndpointResult.Matched(_, _, out) => - try F.toIO(out).unsafeRunTimed(d) match { - case Some(a) => Some(Right(a)) - case _ => Some(Left(new TimeoutException(s"Output wasn't returned in time: $d"))) - } catch { + try + F.toIO(out).unsafeRunTimed(d) match { + case Some(a) => Some(Right(a)) + case _ => Some(Left(new TimeoutException(s"Output wasn't returned in time: $d"))) + } + catch { case e: Throwable => Some(Left(e)) } case _ => None @@ -91,11 +92,11 @@ object EndpointResult { implicit class EndpointResultOps[F[_], A](val self: EndpointResult[F, A]) extends AnyVal { /** - * Returns the [[Output]] if an [[Endpoint]] is matched. - */ + * Returns the [[Output]] if an [[Endpoint]] is matched. + */ final def output: Option[F[Output[A]]] = self match { - case EndpointResult.Matched(_, _, out) => Some(out) - case _ => None + case EndpointResult.Matched(_, _, out: F[Output[A]]) => Some(out) + case _ => None } } } diff --git a/core/src/main/scala/io/finch/Error.scala b/core/src/main/scala/io/finch/Error.scala index 268f99e6d..64fec20f5 100644 --- a/core/src/main/scala/io/finch/Error.scala +++ b/core/src/main/scala/io/finch/Error.scala @@ -10,25 +10,23 @@ import cats.{Eq, Show} import io.finch.items.RequestItem /** - * A single error from an [[Endpoint]]. - * - * This indicates that one of the Finch's built-in components failed. This includes, but not - * limited by: - * - * - reading a required param, body, header, etc. - * - parsing a string-based endpoint with `.as[T]` combinator - * - validating an endpoint with `.should` or `shouldNot` combinators - */ + * A single error from an [[Endpoint]]. + * + * This indicates that one of the Finch's built-in components failed. This includes, but not limited by: + * + * - reading a required param, body, header, etc. + * - parsing a string-based endpoint with `.as[T]` combinator + * - validating an endpoint with `.should` or `shouldNot` combinators + */ sealed abstract class Error extends Exception with NoStackTrace /** - * Multiple errors from an [[Endpoint]]. - * - * This type of error indicates that an endpoint is able to accumulate multiple [[Error]]s - * into a single instance of [[Errors]] that embeds a non-empty list. - * - * Error accumulation happens as part of the `.product` (or `adjoin`, `::`) combinator. - */ + * Multiple errors from an [[Endpoint]]. + * + * This type of error indicates that an endpoint is able to accumulate multiple [[Error]] s into a single instance of [[Errors]] that embeds a non-empty list. + * + * Error accumulation happens as part of the `.product` (or `adjoin`, `::`) combinator. + */ case class Errors(errors: NonEmptyList[Error]) extends Exception with NoStackTrace { override def getMessage: String = "One or more errors reading request:" + @@ -46,40 +44,45 @@ object Error { } /** - * An exception that indicates a required request item (''header'', ''param'', ''cookie'', - * ''body'') was missing in the request. - * - * @param item the missing request item - */ + * An exception that indicates a required request item (''header'', ''param'', ''cookie'', ''body'') was missing in the request. + * + * @param item + * the missing request item + */ final case class NotPresent(item: RequestItem) extends Error { override def getMessage: String = s"Required ${item.description} not present in the request." } /** - * An exception that indicates a broken [[ValidationRule]] on the request item. - * - * @param item the invalid request item - * @param rule the rule description - */ + * An exception that indicates a broken [[ValidationRule]] on the request item. + * + * @param item + * the invalid request item + * @param rule + * the rule description + */ final case class NotValid(item: RequestItem, rule: String) extends Error { override def getMessage: String = s"Validation failed: ${item.description} $rule." } /** - * An exception that indicates that a request item could be parsed. - * - * @param item the invalid request item - * @param targetType the type the item should be converted into - * @param cause the cause of the parsing error - */ - final case class NotParsed(item: RequestItem, targetType: ClassTag[_], cause: Throwable) extends Error { + * An exception that indicates that a request item could be parsed. + * + * @param item + * the invalid request item + * @param targetType + * the type the item should be converted into + * @param cause + * the cause of the parsing error + */ + final case class NotParsed(item: RequestItem, targetType: ClassTag[?], cause: Throwable) extends Error { override def getMessage: String = { // Note: https://issues.scala-lang.org/browse/SI-2034 val className = targetType.runtimeClass.getName val simpleName = className.substring(className.lastIndexOf(".") + 1) - s"${item.description} cannot be converted to ${simpleName}: ${cause.getMessage}." + s"${item.description} cannot be converted to $simpleName: ${cause.getMessage}." } override def getCause: Throwable = cause diff --git a/core/src/main/scala/io/finch/Input.scala b/core/src/main/scala/io/finch/Input.scala index aca632477..db26793e7 100644 --- a/core/src/main/scala/io/finch/Input.scala +++ b/core/src/main/scala/io/finch/Input.scala @@ -8,50 +8,48 @@ import cats.Eq import cats.effect.Effect import com.twitter.finagle.http.{Method, Request, RequestBuilder} import com.twitter.io.{Buf, Reader} -import shapeless.Witness /** - * An input for [[Endpoint]] that glues two individual pieces together: - * - * - Finagle's [[Request]] needed for evaluating (e.g., `body`, `param`) - * - Finch's route (represented as `Seq[String]`) needed for matching (e.g., `path`) - */ + * An input for [[Endpoint]] that glues two individual pieces together: + * + * - Finagle's [[Request]] needed for evaluating (e.g., `body`, `param`) + * - Finch's route (represented as `Seq[String]`) needed for matching (e.g., `path`) + */ final case class Input(request: Request, route: List[String]) { /** - * Returns the new `Input` wrapping a given `route`. - */ + * Returns the new `Input` wrapping a given `route`. + */ def withRoute(route: List[String]): Input = Input(request, route) /** - * Returns the new `Input` wrapping a given payload. This requires the content-type as a first - * type parameter (won't be inferred). - * - * ``` - * import io.finch._, io.circe._ - * - * val text = Input.post("/").withBody[Text.Plain]("Text Body") - * val json = Input.post("/").withBody[Application.Json](Map("json" -> "object")) - * ``` - * - * Also possible to create chunked inputs passing a stream as an argument. - * - * ``` - * import io.finch._, io.finch.iteratee._, cats.effect.IO, io.iteratee.Enumerator - * import io.finch.circe._, io.circe.generic.auto._ - * - * val enumerateText = Enumerator.enumerate[IO, String]("foo", "bar") - * val text = Input.post("/").withBody[Text.Plain](enumerateText) - * - * val enumerateJson = Enumerate.enumerate[IO, Map[String, String]](Map("foo" - "bar")) - * val json = Input.post("/").withBody[Application.Json](enumerateJson) - * ``` - */ + * Returns the new `Input` wrapping a given payload. This requires the content-type as a first type parameter (won't be inferred). + * + * ``` + * import io.finch._, io.circe._ + * + * val text = Input.post("/").withBody[Text.Plain]("Text Body") + * val json = Input.post("/").withBody[Application.Json](Map("json" -> "object")) + * ``` + * + * Also possible to create chunked inputs passing a stream as an argument. + * + * ``` + * import io.finch._, io.finch.iteratee._, cats.effect.IO, io.iteratee.Enumerator + * import io.finch.circe._, io.circe.generic.auto._ + * + * val enumerateText = Enumerator.enumerate[IO, String]("foo", "bar") + * val text = Input.post("/").withBody[Text.Plain](enumerateText) + * + * val enumerateJson = Enumerate.enumerate[IO, Map[String, String]](Map("foo" - "bar")) + * val json = Input.post("/").withBody[Application.Json](enumerateJson) + * ``` + */ def withBody[CT <: String]: Input.Body[CT] = new Input.Body[CT](this) /** - * Returns the new `Input` with `headers` amended. - */ + * Returns the new `Input` with `headers` amended. + */ def withHeaders(headers: (String, String)*): Input = { val copied = Input.copyRequest(request) headers.foreach { case (k, v) => copied.headerMap.set(k, v) } @@ -60,10 +58,11 @@ final case class Input(request: Request, route: List[String]) { } /** - * Returns the new `Input` wrapping a given `application/x-www-form-urlencoded` payload. - * - * @note In addition to media type, this will also set charset to UTF-8. - */ + * Returns the new `Input` wrapping a given `application/x-www-form-urlencoded` payload. + * + * @note + * In addition to media type, this will also set charset to UTF-8. + */ def withForm(params: (String, String)*): Input = { val postRequest: Request = RequestBuilder().addFormElement(params: _*).url("http://localhost").buildFormPost() @@ -72,8 +71,8 @@ final case class Input(request: Request, route: List[String]) { } /** - * Creates an input for [[Endpoint]] from [[Request]]. - */ + * Creates an input for [[Endpoint]] from [[Request]]. + */ object Input { final private def copyRequest(from: Request): Request = @@ -89,15 +88,15 @@ object Input { } /** - * A helper class that captures the `Content-Type` of the payload. - */ + * A helper class that captures the `Content-Type` of the payload. + */ class Body[CT <: String](i: Input) { - def apply[A](body: A)(implicit e: Encode.Aux[A, CT], w: Witness.Aux[CT]): Input = + def apply[A](body: A)(implicit e: Encode.Aux[A, CT], w: ValueOf[CT]): Input = apply[A](body, StandardCharsets.UTF_8) def apply[A](body: A, charset: Charset)(implicit e: Encode.Aux[A, CT], - W: Witness.Aux[CT] + W: ValueOf[CT] ): Input = { val content = e(body, charset) val copied = copyRequest(i.request) @@ -113,13 +112,13 @@ object Input { def apply[F[_]: Effect, S[_[_], _], A](s: S[F, A])(implicit S: EncodeStream.Aux[F, S, A, CT], - W: Witness.Aux[CT] + W: ValueOf[CT] ): Input = apply[F, S, A](s, StandardCharsets.UTF_8) def apply[F[_], S[_[_], _], A](s: S[F, A], charset: Charset)(implicit F: Effect[F], S: EncodeStream.Aux[F, S, A, CT], - W: Witness.Aux[CT] + W: ValueOf[CT] ): Input = { val content = F.toIO(S(s, charset)).unsafeRunSync() val copied = copyRequestWithReader(i.request, content) @@ -136,18 +135,18 @@ object Input { implicit val inputEq: Eq[Input] = Eq.fromUniversalEquals /** - * Creates an [[Input]] from a given [[Request]]. - */ + * Creates an [[Input]] from a given [[Request]]. + */ def fromRequest(req: Request): Input = { val p = req.path - if (p.length == 1) Input(req, Nil) + if p.length == 1 then Input(req, Nil) else { val route = new ListBuffer[String] var i, j = 1 // drop the first slash - while (j < p.length) { - if (p.charAt(j) == '/') { + while j < p.length do { + if p.charAt(j) == '/' then { route += p.substring(i, j) i = j + 1 } @@ -155,7 +154,7 @@ object Input { j += 1 } - if (j > i) { + if j > i then { route += p.substring(i, j) } @@ -164,31 +163,31 @@ object Input { } /** - * Creates a `GET` input with a given query string (represented as `params`). - */ + * Creates a `GET` input with a given query string (represented as `params`). + */ def get(path: String, params: (String, String)*): Input = fromRequest(Request(Method.Get, Request.queryString(path, params: _*))) /** - * Creates a `PUT` input with a given query string (represented as `params`). - */ + * Creates a `PUT` input with a given query string (represented as `params`). + */ def put(path: String, params: (String, String)*): Input = fromRequest(Request(Method.Put, Request.queryString(path, params: _*))) /** - * Creates a `PATCH` input with a given query string (represented as `params`). - */ + * Creates a `PATCH` input with a given query string (represented as `params`). + */ def patch(path: String, params: (String, String)*): Input = fromRequest(Request(Method.Patch, Request.queryString(path, params: _*))) /** - * Creates a `DELETE` input with a given query string (represented as `params`). - */ + * Creates a `DELETE` input with a given query string (represented as `params`). + */ def delete(path: String, params: (String, String)*): Input = fromRequest(Request(Method.Delete, Request.queryString(path, params: _*))) /** - * Creates a `POST` input with empty payload. - */ + * Creates a `POST` input with empty payload. + */ def post(path: String): Input = fromRequest(Request(Method.Post, path)) } diff --git a/core/src/main/scala/io/finch/LiftReader.scala b/core/src/main/scala/io/finch/LiftReader.scala index dae402a60..cf8774704 100644 --- a/core/src/main/scala/io/finch/LiftReader.scala +++ b/core/src/main/scala/io/finch/LiftReader.scala @@ -3,8 +3,8 @@ package io.finch import com.twitter.io.{Buf, Reader} /** - * Create stream `S[F, A]` from [[Reader]]. - */ + * Create stream `S[F, A]` from [[Reader]]. + */ trait LiftReader[S[_[_], _], F[_]] { final def apply(reader: Reader[Buf]): S[F, Buf] = apply(reader, identity) diff --git a/core/src/main/scala/io/finch/Mappable.scala b/core/src/main/scala/io/finch/Mappable.scala new file mode 100644 index 000000000..055cd86ee --- /dev/null +++ b/core/src/main/scala/io/finch/Mappable.scala @@ -0,0 +1,172 @@ +package io.finch + +import com.twitter.finagle.http.Response +import shapeless.ops.function.FnToProduct +import scala.annotation.targetName +import cats.MonadError +import cats.syntax.all._ + +/** + * Enables a very simple syntax allowing to "map" endpoints to arbitrary functions. The types are resolved at compile time and no reflection is used. + * + * For example: + * + * {{{ + * import io.finch._ + * import io.cats.effect.IO + * + * object Mapping extends Endpoint.Module[IO] { + * def hello = get("hello" :: path[String]) { s: String => + * Ok(s) + * } + * } + * }}} + */ +trait Mappable[F[_], A] extends Endpoint[F, A] { + self => + + @targetName("mapOutputApply") + final def apply[B](f: A => Output[B])(using MonadError[F, Throwable]): Endpoint[F, B] = self.mapOutput(f) + + @targetName("mapHKOutputApply") + final def apply[B](f: A => F[Output[B]])(using MonadError[F, Throwable]): Endpoint[F, B] = self.mapOutputAsync(f) + + @targetName("mapResponseApply") + final def apply(f: A => Response)(using MonadError[F, Throwable]): Endpoint[F, Response] = + self.mapOutput(f.andThen(r => Output.payload(r, r.status))) + + @targetName("mapHKResponseApply") + final def apply(f: A => F[Response])(using MonadError[F, Throwable]): Endpoint[F, Response] = + self.mapOutputAsync(f.andThen(ff => ff.map(r => Output.payload(r, r.status)))) + + @targetName("outputApply") + final def apply[B](o: => Output[B])(using F: MonadError[F, Throwable]): Endpoint[F, B] = self.mapOutput(_ => o) + + @targetName("responseApply") + final def apply(r: => Response)(using F: MonadError[F, Throwable]): Endpoint[F, Response] = self.mapOutput(_ => Output.payload(r, r.status)) + + @targetName("hkOutputApply") + final def apply[B](o: => F[Output[B]])(using F: MonadError[F, Throwable]): Endpoint[F, B] = self.mapOutputAsync(_ => o) + + @targetName("hkResponseApply") + final def apply(r: => F[Response])(using F: MonadError[F, Throwable]): Endpoint[F, Response] = + self.mapOutputAsync(_ => r.map(res => Output.payload(res, res.status))) + +} + +object Mappable { + + extension [F[_], A, B](self: Mappable[F, A *: B *: EmptyTuple]) { + @targetName("mapFn2OutputApply") + def apply[C](f: (A, B) => Output[C])(using MonadError[F, Throwable]): Endpoint[F, C] = self.mapOutput(t => f.tupled(t)) + + @targetName("mapFn2ResponseApply") + def apply(f: (A, B) => Response)(using MonadError[F, Throwable]): Endpoint[F, Response] = self.mapOutput { t => + val r = f.tupled(t) + Output.payload(r, r.status) + } + + @targetName("mapFn2HKOutputApply") + def apply[C](f: (A, B) => F[Output[C]])(using MonadError[F, Throwable]): Endpoint[F, C] = self.mapOutputAsync(t => f.tupled(t)) + + @targetName("mapFn2HKResponseApply") + def apply(f: (A, B) => F[Response])(using MonadError[F, Throwable]): Endpoint[F, Response] = + self.mapOutputAsync(t => f.tupled(t).map(r => Output.payload(r, r.status))) + } + + extension [F[_], A, B, C](self: Mappable[F, A *: B *: C *: EmptyTuple]) { + @targetName("mapFn3OutputApply") + def apply[D](f: (A, B, C) => Output[D])(using MonadError[F, Throwable]): Endpoint[F, D] = self.mapOutput(t => f.tupled(t)) + + @targetName("mapFn3ResponseApply") + def apply(f: (A, B, C) => Response)(using MonadError[F, Throwable]): Endpoint[F, Response] = self.mapOutput { t => + val r = f.tupled(t) + Output.payload(r, r.status) + } + + @targetName("mapFn3HKOutputApply") + def apply[D](f: (A, B, C) => F[Output[D]])(using MonadError[F, Throwable]): Endpoint[F, D] = self.mapOutputAsync(t => f.tupled(t)) + + @targetName("mapFn3HKResponseApply") + def apply(f: (A, B, C) => F[Response])(using MonadError[F, Throwable]): Endpoint[F, Response] = + self.mapOutputAsync(t => f.tupled(t).map(r => Output.payload(r, r.status))) + + } + + extension [F[_], A, B, C, D](self: Mappable[F, A *: B *: C *: D *: EmptyTuple]) { + @targetName("mapFn4OutputApply") + def apply[E](f: (A, B, C, D) => Output[E])(using MonadError[F, Throwable]): Endpoint[F, E] = self.mapOutput(t => f.tupled(t)) + + @targetName("mapFn4ResponseApply") + def apply(f: (A, B, C, D) => Response)(using MonadError[F, Throwable]): Endpoint[F, Response] = self.mapOutput { t => + val r = f.tupled(t) + Output.payload(r, r.status) + } + + @targetName("mapFn4HKOutputApply") + def apply[E](f: (A, B, C, D) => F[Output[E]])(using MonadError[F, Throwable]): Endpoint[F, E] = self.mapOutputAsync(t => f.tupled(t)) + + @targetName("mapFn4HKResponseApply") + def apply(f: (A, B, C, D) => F[Response])(using MonadError[F, Throwable]): Endpoint[F, Response] = + self.mapOutputAsync(t => f.tupled(t).map(r => Output.payload(r, r.status))) + + } + + extension [F[_], A, B, C, D, E](self: Mappable[F, A *: B *: C *: D *: E *: EmptyTuple]) { + @targetName("mapFn5OutputApply") + def apply[G](f: (A, B, C, D, E) => Output[G])(using MonadError[F, Throwable]): Endpoint[F, G] = self.mapOutput(t => f.tupled(t)) + + @targetName("mapFn5ResponseApply") + def apply(f: (A, B, C, D, E) => Response)(using MonadError[F, Throwable]): Endpoint[F, Response] = self.mapOutput { t => + val r = f.tupled(t) + Output.payload(r, r.status) + } + + @targetName("mapFn5HKOutputApply") + def apply[G](f: (A, B, C, D, E) => F[Output[G]])(using MonadError[F, Throwable]): Endpoint[F, G] = self.mapOutputAsync(t => f.tupled(t)) + + @targetName("mapFn5HKResponseApply") + def apply(f: (A, B, C, D, E) => F[Response])(using MonadError[F, Throwable]): Endpoint[F, Response] = + self.mapOutputAsync(t => f.tupled(t).map(r => Output.payload(r, r.status))) + + } + + extension [F[_], A, B, C, D, E, G](self: Mappable[F, A *: B *: C *: D *: E *: G *: EmptyTuple]) { + @targetName("mapFn6OutputApply") + def apply[H](f: (A, B, C, D, E, G) => Output[H])(using MonadError[F, Throwable]): Endpoint[F, H] = self.mapOutput(t => f.tupled(t)) + + @targetName("mapFn6ResponseApply") + def apply(f: (A, B, C, D, E, G) => Response)(using MonadError[F, Throwable]): Endpoint[F, Response] = self.mapOutput { t => + val r = f.tupled(t) + Output.payload(r, r.status) + } + + @targetName("mapFn6HKOutputApply") + def apply[H](f: (A, B, C, D, E, G) => F[Output[H]])(using MonadError[F, Throwable]): Endpoint[F, H] = self.mapOutputAsync(t => f.tupled(t)) + + @targetName("mapFn6HKResponseApply") + def apply(f: (A, B, C, D, E, G) => F[Response])(using MonadError[F, Throwable]): Endpoint[F, Response] = + self.mapOutputAsync(t => f.tupled(t).map(r => Output.payload(r, r.status))) + + } + + extension [F[_], A, B, C, D, E, G, H](self: Mappable[F, A *: B *: C *: D *: E *: G *: H *: EmptyTuple]) { + @targetName("mapFn7OutputApply") + def apply[K](f: (A, B, C, D, E, G, H) => Output[K])(using MonadError[F, Throwable]): Endpoint[F, K] = self.mapOutput(t => f.tupled(t)) + + @targetName("mapFn7ResponseApply") + def apply(f: (A, B, C, D, E, G, H) => Response)(using MonadError[F, Throwable]): Endpoint[F, Response] = self.mapOutput { t => + val r = f.tupled(t) + Output.payload(r, r.status) + } + + @targetName("mapFn7HKOutputApply") + def apply[K](f: (A, B, C, D, E, G, H) => F[Output[K]])(using MonadError[F, Throwable]): Endpoint[F, K] = self.mapOutputAsync(t => f.tupled(t)) + + @targetName("mapFn7HKResponseApply") + def apply(f: (A, B, C, D, E, G, H) => F[Response])(using MonadError[F, Throwable]): Endpoint[F, Response] = + self.mapOutputAsync(t => f.tupled(t).map(r => Output.payload(r, r.status))) + + } + +} diff --git a/core/src/main/scala/io/finch/Output.scala b/core/src/main/scala/io/finch/Output.scala index 0a3efe4ea..898f06794 100644 --- a/core/src/main/scala/io/finch/Output.scala +++ b/core/src/main/scala/io/finch/Output.scala @@ -6,33 +6,33 @@ import cats.{Applicative, Eq} import com.twitter.finagle.http.{Cookie, Response, Status} /** - * An output of [[Endpoint]]. - */ + * An output of [[Endpoint]]. + */ sealed trait Output[+A] { self => /** - * The status code of this [[Output]]. - */ + * The status code of this [[Output]]. + */ def status: Status /** - * The header map of this [[Output]]. - */ + * The header map of this [[Output]]. + */ def headers: Map[String, String] /** - * The cookie list of this [[Output]]. - */ + * The cookie list of this [[Output]]. + */ def cookies: List[Cookie] /** - * The charset of this [[Output]]. - */ + * The charset of this [[Output]]. + */ def charset: Option[Charset] /** - * Returns the payload value of this [[Output]] or throws an exception. - */ + * Returns the payload value of this [[Output]] or throws an exception. + */ def value: A final def map[B](fn: A => B): Output[B] = this match { @@ -47,13 +47,13 @@ sealed trait Output[+A] { self => case e: Output.Empty => e } - final def traverse[F[_], B](fn: A => F[B])(implicit F: Applicative[F]): F[Output[B]] = this match { + final def traverse[F[_], B](fn: A => F[B])(using F: Applicative[F]): F[Output[B]] = this match { case p: Output.Payload[A] => F.map(fn(p.value))(b => p.withValue(b)) case f: Output.Failure => F.pure(f) case e: Output.Empty => F.pure(e) } - final def traverseFlatten[F[_], B](fn: A => F[Output[B]])(implicit F: Applicative[F]): F[Output[B]] = this match { + final def traverseFlatten[F[_], B](fn: A => F[Output[B]])(using F: Applicative[F]): F[Output[B]] = this match { case p: Output.Payload[A] => F.map(fn(p.value))(ob => ob.withHeaders(self.headers).withCookies(self.cookies)) case f: Output.Failure => F.pure(f) @@ -61,42 +61,42 @@ sealed trait Output[+A] { self => } /** - * Overrides `charset` of this [[Output]]. - */ + * Overrides `charset` of this [[Output]]. + */ final def withCharset(charset: Charset): Output[A] = - copy(charset = Some(charset)) + cpy(charset = Some(charset)) /** - * Overrides the `status` code of this [[Output]]. - */ + * Overrides the `status` code of this [[Output]]. + */ final def withStatus(status: Status): Output[A] = - copy(status = status) + cpy(status = status) /** - * Adds given `headers` to this [[Output]]. - */ + * Adds given `headers` to this [[Output]]. + */ final def withHeaders(headers: Map[String, String]): Output[A] = - if (headers.isEmpty) this - else copy(headers = self.headers ++ headers) + if headers.isEmpty then this + else cpy(headers = self.headers ++ headers) /** - * Adds given `cookies` to this [[Output]]. - */ + * Adds given `cookies` to this [[Output]]. + */ final def withCookies(cookies: List[Cookie]): Output[A] = - if (cookies.isEmpty) this - else copy(cookies = self.cookies ++ cookies) + if cookies.isEmpty then this + else cpy(cookies = self.cookies ++ cookies) /** - * Adds a given `header` to this [[Output]]. - */ + * Adds a given `header` to this [[Output]]. + */ final def withHeader(header: (String, String)): Output[A] = withHeaders(Map(header)) /** - * Adds a given `cookie` to this [[Output]]. - */ + * Adds a given `cookie` to this [[Output]]. + */ final def withCookie(cookie: Cookie): Output[A] = withCookies(List(cookie)) - protected def copy( + protected def cpy( status: Status = self.status, charset: Option[Charset] = self.charset, headers: Map[String, String] = self.headers, @@ -107,40 +107,40 @@ sealed trait Output[+A] { self => object Output { /** - * Creates a successful [[Output]] that wraps a payload `value` with given `status`. - */ + * Creates a successful [[Output]] that wraps a payload `value` with given `status`. + */ final def payload[A](value: A, status: Status = Status.Ok): Output[A] = Payload(value, status) /** - * Creates a failure [[Output]] that wraps an exception `cause` causing this. - */ + * Creates a failure [[Output]] that wraps an exception `cause` causing this. + */ final def failure[A](cause: Exception, status: Status = Status.BadRequest): Output[A] = Failure(cause, status) /** - * Creates an empty [[Output]] of given `status`. - */ + * Creates an empty [[Output]] of given `status`. + */ final def empty[A](status: Status): Output[A] = Empty(status) /** - * Creates a unit/empty [[Output]] (i.e., `Output[Unit]`) of given `status`. - */ + * Creates a unit/empty [[Output]] (i.e., `Output[Unit]`) of given `status`. + */ final def unit(status: Status): Output[Unit] = empty(status) /** - * An [[Output]] with `None` as a payload. - */ + * An [[Output]] with `None` as a payload. + */ val None: Output[Option[Nothing]] = Output.payload(Option.empty[Nothing]) /** - * An [[Output]] with [[shapeless.HNil]] as a payload. - */ - val HNil: Output[shapeless.HNil] = Output.payload(shapeless.HNil) + * An [[Output]] with [[EmptyTuple]] as a payload. + */ + val EmptyTuple: Output[EmptyTuple] = Output.payload(scala.EmptyTuple) /** - * A successful [[Output]] that captures a payload `value`. - */ + * A successful [[Output]] that captures a payload `value`. + */ final private[finch] case class Payload[A]( value: A, status: Status = Status.Ok, @@ -151,14 +151,13 @@ object Output { def withValue[B](value: B): Payload[B] = Payload(value, status, charset, headers, cookies) - protected def copy(status: Status, charset: Option[Charset], headers: Map[String, String], cookies: List[Cookie]): Output[A] = + protected def cpy(status: Status, charset: Option[Charset], headers: Map[String, String], cookies: List[Cookie]): Output[A] = Payload(value, status, charset, headers, cookies) } /** - * A failure [[Output]] that captures an [[Exception]] explaining why it's not a payload - * or an empty response. - */ + * A failure [[Output]] that captures an [[Exception]] explaining why it's not a payload or an empty response. + */ final private[finch] case class Failure( cause: Exception, status: Status = Status.BadRequest, @@ -169,13 +168,13 @@ object Output { def value: Nothing = throw cause - protected def copy(status: Status, charset: Option[Charset], headers: Map[String, String], cookies: List[Cookie]): Output[Nothing] = + protected def cpy(status: Status, charset: Option[Charset], headers: Map[String, String], cookies: List[Cookie]): Output[Nothing] = Failure(cause, status, charset, headers, cookies) } /** - * An empty [[Output]] that does not capture any payload. - */ + * An empty [[Output]] that does not capture any payload. + */ final private[finch] case class Empty( status: Status, charset: Option[Charset] = Option.empty, @@ -185,7 +184,7 @@ object Output { def value: Nothing = throw new IllegalStateException("empty output") - protected def copy(status: Status, charset: Option[Charset], headers: Map[String, String], cookies: List[Cookie]): Output[Nothing] = + protected def cpy(status: Status, charset: Option[Charset], headers: Map[String, String], cookies: List[Cookie]): Output[Nothing] = Empty(status, charset, headers, cookies) } @@ -194,8 +193,8 @@ object Output { implicit class OutputOps[A](val o: Output[A]) extends AnyVal { /** - * Converts this [[Output]] to the HTTP response of the given `version`. - */ + * Converts this [[Output]] to the HTTP response of the given `version`. + */ def toResponse[F[_], CT](implicit F: Applicative[F], tr: ToResponse.Aux[F, A, CT], @@ -213,7 +212,7 @@ object Output { o.headers.foreach { case (k, v) => rep.headerMap.set(k, v) } o.cookies.foreach(rep.cookies.add) o.charset.foreach { c => - if (!rep.content.isEmpty || rep.isChunked) { + if !rep.content.isEmpty || rep.isChunked then { rep.charset = c.displayName.toLowerCase } } diff --git a/core/src/main/scala/io/finch/ToResponse.scala b/core/src/main/scala/io/finch/ToResponse.scala index b7cb08fce..1a904593f 100644 --- a/core/src/main/scala/io/finch/ToResponse.scala +++ b/core/src/main/scala/io/finch/ToResponse.scala @@ -9,8 +9,8 @@ import com.twitter.finagle.http.{Response, Status, Version} import shapeless._ /** - * Represents a conversion from `A` to [[Response]]. - */ + * Represents a conversion from `A` to [[Response]]. + */ trait ToResponse[F[_], A] { type ContentType @@ -45,12 +45,12 @@ trait ToResponseInstances { implicit def valueToResponse[F[_], A, CT <: String](implicit F: Applicative[F], A: Encode.Aux[A, CT], - CT: Witness.Aux[CT] + CT: ValueOf[CT] ): Aux[F, A, CT] = instance { (a, cs) => val buf = A(a, cs) val rep = Response(Version.Http11, Status.Ok) - if (!buf.isEmpty) { + if !buf.isEmpty then { rep.content = buf rep.headerMap.setUnsafe("Content-Type", CT.value) } @@ -61,7 +61,7 @@ trait ToResponseInstances { implicit def streamToResponse[F[_], S[_[_], _], A, CT <: String](implicit F: Functor[F], S: EncodeStream.Aux[F, S, A, CT], - CT: Witness.Aux[CT] + CT: ValueOf[CT] ): Aux[F, S[F, A], CT] = instance { (a, cs) => F.map(S(a, cs)) { stream => val rep = Response(Version.Http11, Status.Ok, stream) @@ -76,10 +76,10 @@ trait ToResponseInstances { object ToResponse extends ToResponseInstances { /** - * Enables server-driven content negotiation with client. - * - * Picks corresponding instance of `ToResponse` according to `Accept` header of a request - */ + * Enables server-driven content negotiation with client. + * + * Picks corresponding instance of `ToResponse` according to `Accept` header of a request + */ trait Negotiable[F[_], A, CT] { def apply(accept: List[Accept]): ToResponse.Aux[F, A, CT] } @@ -92,7 +92,7 @@ object ToResponse extends ToResponseInstances { a: Accept.Matcher[CTH] ): Negotiable[F, A, CTH :+: CTT] = new Negotiable[F, A, CTH :+: CTT] { def apply(accept: List[Accept]): ToResponse.Aux[F, A, CTH :+: CTT] = - if (accept.exists(_.matches[CTH])) h.asInstanceOf[ToResponse.Aux[F, A, CTH :+: CTT]] + if accept.exists(_.matches[CTH]) then h.asInstanceOf[ToResponse.Aux[F, A, CTH :+: CTT]] else t(accept).asInstanceOf[ToResponse.Aux[F, A, CTH :+: CTT]] } diff --git a/core/src/main/scala/io/finch/ToService.scala b/core/src/main/scala/io/finch/ToService.scala index e073b7a7b..191f32588 100644 --- a/core/src/main/scala/io/finch/ToService.scala +++ b/core/src/main/scala/io/finch/ToService.scala @@ -7,8 +7,8 @@ import com.twitter.finagle.http.{Request, Response} import com.twitter.util.{Future, Promise} /** - * Representation of `Endpoint.Compiled` as Finagle Service - */ + * Representation of `Endpoint.Compiled` as Finagle Service + */ case class ToService[F[_]](compiled: Endpoint.Compiled[F])(implicit F: Effect[F]) extends Service[Request, Response] { def apply(request: Request): Future[Response] = { val repF = compiled(request).flatMap { case (trc, either) => diff --git a/core/src/main/scala/io/finch/Trace.scala b/core/src/main/scala/io/finch/Trace.scala index ca68a1ac0..a43991b42 100644 --- a/core/src/main/scala/io/finch/Trace.scala +++ b/core/src/main/scala/io/finch/Trace.scala @@ -6,15 +6,16 @@ import scala.collection.mutable.ListBuffer import com.twitter.util.Local /** - * Models a trace of a matched [[Endpoint]]. For example, `/hello/:name`. - * - * @note represented as a linked-list-like structure for efficiency. - */ + * Models a trace of a matched [[Endpoint]]. For example, `/hello/:name`. + * + * @note + * represented as a linked-list-like structure for efficiency. + */ sealed trait Trace { /** - * Concatenates this and `that` [[Trace]]s. - */ + * Concatenates this and `that` [[Trace]] s. + */ final def concat(that: Trace): Trace = { @tailrec def loop(from: Trace, last: Trace.Segment): Unit = from match { @@ -40,8 +41,8 @@ sealed trait Trace { } /** - * Converts this [[Trace]] into a linked list of path segments. - */ + * Converts this [[Trace]] into a linked list of path segments. + */ final def toList: List[String] = { @tailrec def loop(from: Trace, to: ListBuffer[String]): List[String] = from match { @@ -70,7 +71,7 @@ object Trace { var current: Segment = null def prepend(segment: Segment): Unit = - if (result == empty) { + if result == empty then { result = segment current = segment } else { @@ -79,7 +80,7 @@ object Trace { } var rs = r - while (rs.nonEmpty) { + while rs.nonEmpty do { prepend(Segment(rs.head, empty)) rs = rs.tail } @@ -88,22 +89,20 @@ object Trace { } /** - * Within a given context `fn`, capture the [[Trace]] instance under `Trace.captured` for each - * matched endpoint. - * - * Example: - * - * {{{ - * val foo = Endpoint.lift("foo").toService[Text.Plain] - * Trace.capture { foo(Request()).map(_ => Trace.captured) } - * }}} - */ + * Within a given context `fn`, capture the [[Trace]] instance under `Trace.captured` for each matched endpoint. + * + * Example: + * + * {{{ + * val foo = Endpoint.lift("foo").toService[Text.Plain] + * Trace.capture { foo(Request()).map(_ => Trace.captured) } + * }}} + */ def capture[A](fn: => A): A = captureLocal.let(new Capture(empty))(fn) /** - * Retrieve the captured [[Trace]] instance or [[empty]] when run outside of [[Trace.capture]] - * context. - */ + * Retrieve the captured [[Trace]] instance or [[empty]] when run outside of [[Trace.capture]] context. + */ def captured: Trace = captureLocal() match { case Some(c) => c.trace case None => empty diff --git a/core/src/main/scala/io/finch/ValidationRule.scala b/core/src/main/scala/io/finch/ValidationRule.scala index 75dab04e1..301bdbafd 100644 --- a/core/src/main/scala/io/finch/ValidationRule.scala +++ b/core/src/main/scala/io/finch/ValidationRule.scala @@ -1,51 +1,53 @@ package io.finch /** - * A `ValidationRule` enables a reusable way of defining a validation rules in the application - * domain. It might be composed with [[Endpoint]]s using either should` or `shouldNot` methods and - * with other `ValidationRule`s using logical methods `and` and `or`. - * - * {{{ - * case class User(name: String, age: Int) - * val user: Endpoint[User] = ( - * param("name").validate(beLongerThan(3)) :: - * param("age").as[Int].should(beGreaterThan(0) and beLessThan(120)) - * ).as[User] - * }}} - */ + * A `ValidationRule` enables a reusable way of defining a validation rules in the application domain. It might be composed with [[Endpoint]] s using either + * should` or `shouldNot` methods and with other `ValidationRule`s using logical methods `and` and `or`. + * + * {{{ + * case class User(name: String, age: Int) + * val user: Endpoint[User] = ( + * param("name").validate(beLongerThan(3)) :: + * param("age").as[Int].should(beGreaterThan(0) and beLessThan(120)) + * ).as[User] + * }}} + */ trait ValidationRule[A] { self => /** - * Text description of this validation rule. - */ + * Text description of this validation rule. + */ def description: String /** - * Applies the rule to the specified value. - * - * @return true if the predicate of this rule holds for the specified value - */ + * Applies the rule to the specified value. + * + * @return + * true if the predicate of this rule holds for the specified value + */ def apply(value: A): Boolean /** - * Combines this rule with another rule such that the new rule only validates if both the combined - * rules validate. - * - * @param that the rule to combine with this rule - * - * @return a new rule that only validates if both the combined rules validate - */ + * Combines this rule with another rule such that the new rule only validates if both the combined rules validate. + * + * @param that + * the rule to combine with this rule + * + * @return + * a new rule that only validates if both the combined rules validate + */ def and(that: ValidationRule[A]): ValidationRule[A] = ValidationRule(s"${self.description} and ${that.description}")(value => self(value) && that(value)) /** - * Combines this rule with another rule such that the new rule validates if any one of the - * combined rules validates. - * - * @param that the rule to combine with this rule - * - * @return a new rule that validates if any of the combined rules validates - */ + * Combines this rule with another rule such that the new rule validates if any one of the combined rules validates. + * + * @param that + * the rule to combine with this rule + * + * @return + * a new rule that validates if any of the combined rules validates + */ def or(that: ValidationRule[A]): ValidationRule[A] = ValidationRule(s"${self.description} or ${that.description}") { value => self(value) || that(value) @@ -53,20 +55,20 @@ trait ValidationRule[A] { self => } /** - * Allows the creation of reusable validation rules for [[Endpoint]]s. - */ + * Allows the creation of reusable validation rules for [[Endpoint]] s. + */ object ValidationRule { /** - * Implicit conversion that allows the same [[ValidationRule]] to be used for required - * and optional values. If the optional value is non-empty, it gets validated (and validation may - * fail, producing an error), but if it is empty, it is always treated as valid. - * - * @param rule the validation rule to adapt for optional values - * - * @return a new validation rule that applies the specified rule to an optional value in case it - * is not empty - */ + * Implicit conversion that allows the same [[ValidationRule]] to be used for required and optional values. If the optional value is non-empty, it gets + * validated (and validation may fail, producing an error), but if it is empty, it is always treated as valid. + * + * @param rule + * the validation rule to adapt for optional values + * + * @return + * a new validation rule that applies the specified rule to an optional value in case it is not empty + */ implicit def toOptionalRule[A](rule: ValidationRule[A]): ValidationRule[Option[A]] = ValidationRule(rule.description) { case Some(value) => rule(value) @@ -74,13 +76,16 @@ object ValidationRule { } /** - * Creates a new reusable [[ValidationRule]] based on the specified predicate. - * - * @param desc text describing the rule being validated - * @param p returns true if the data is valid - * - * @return a new reusable validation rule. - */ + * Creates a new reusable [[ValidationRule]] based on the specified predicate. + * + * @param desc + * text describing the rule being validated + * @param p + * returns true if the data is valid + * + * @return + * a new reusable validation rule. + */ def apply[A](desc: String)(p: A => Boolean): ValidationRule[A] = new ValidationRule[A] { def description: String = desc def apply(value: A): Boolean = p(value) diff --git a/core/src/main/scala/io/finch/ValidationRules.scala b/core/src/main/scala/io/finch/ValidationRules.scala index 4f6f23aa6..eab2f0492 100644 --- a/core/src/main/scala/io/finch/ValidationRules.scala +++ b/core/src/main/scala/io/finch/ValidationRules.scala @@ -3,26 +3,26 @@ package io.finch trait ValidationRules { /** - * A [[ValidationRule]] that makes sure the numeric value is greater than given `n`. - */ + * A [[ValidationRule]] that makes sure the numeric value is greater than given `n`. + */ def beGreaterThan[A](n: A)(implicit ev: Numeric[A]): ValidationRule[A] = ValidationRule(s"be greater than $n")(ev.gt(_, n)) /** - * A [[ValidationRule]] that makes sure the numeric value is less than given `n`. - */ + * A [[ValidationRule]] that makes sure the numeric value is less than given `n`. + */ def beLessThan[A](n: A)(implicit ev: Numeric[A]): ValidationRule[A] = ValidationRule(s"be less than $n")(ev.lt(_, n)) /** - * A [[ValidationRule]] that makes sure the string value is longer than `n` symbols. - */ + * A [[ValidationRule]] that makes sure the string value is longer than `n` symbols. + */ def beLongerThan(n: Int): ValidationRule[String] = ValidationRule(s"be longer than $n symbols")(_.length > n) /** - * A [[ValidationRule]] that makes sure the string value is shorter than `n` symbols. - */ + * A [[ValidationRule]] that makes sure the string value is shorter than `n` symbols. + */ def beShorterThan(n: Int): ValidationRule[String] = ValidationRule(s"be shorter than $n symbols")(_.length < n) } diff --git a/core/src/main/scala/io/finch/contentTypes.scala b/core/src/main/scala/io/finch/contentTypes.scala index d532f0f53..7083d35a0 100644 --- a/core/src/main/scala/io/finch/contentTypes.scala +++ b/core/src/main/scala/io/finch/contentTypes.scala @@ -1,48 +1,50 @@ package io.finch -import shapeless.Witness - /** - * @see [[https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types]] - */ + * @see + * [[https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types]] + */ object Application { - type Json = Witness.`"application/json"`.T - type Xml = Witness.`"application/xml"`.T - type AtomXml = Witness.`"application/atom+xml"`.T - type Csv = Witness.`"application/csv"`.T - type Javascript = Witness.`"application/javascript"`.T - type OctetStream = Witness.`"application/octet-stream"`.T - type RssXml = Witness.`"application/rss+xml"`.T - type WwwFormUrlencoded = Witness.`"application/x-www-form-urlencoded"`.T - type Ogg = Witness.`"application/ogg"`.T + type Json = "application/json" + type Xml = "application/xml" + type AtomXml = "application/atom+xml" + type Csv = "application/csv" + type Javascript = "application/javascript" + type OctetStream = "application/octet-stream" + type RssXml = "application/rss+xml" + type WwwFormUrlencoded = "application/x-www-form-urlencoded" + type Ogg = "application/ogg" } /** - * @see [[https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types]] - */ + * @see + * [[https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types]] + */ object Text { - type Plain = Witness.`"text/plain"`.T - type Html = Witness.`"text/html"`.T - type Css = Witness.`"text/css"`.T - type EventStream = Witness.`"text/event-stream"`.T + type Plain = "text/plain" + type Html = "text/html" + type Css = "text/css" + type EventStream = "text/event-stream" } /** - * @see [[https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types]] - */ + * @see + * [[https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types]] + */ object Image { - type Gif = Witness.`"image/gif"`.T - type Jpeg = Witness.`"image/jpeg"`.T - type Png = Witness.`"image/png"`.T - type Svg = Witness.`"image/svg+xml"`.T + type Gif = "image/gif" + type Jpeg = "image/jpeg" + type Png = "image/png" + type Svg = "image/svg+xml" } /** - * @see [[https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types]] - */ + * @see + * [[https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types]] + */ object Audio { - type Wave = Witness.`"audio/wave"`.T - type Wav = Witness.`"audio/wav"`.T - type Webm = Witness.`"audio/webm"`.T - type Ogg = Witness.`"audio/ogg"`.T + type Wave = "audio/wave" + type Wav = "audio/wav" + type Webm = "audio/webm" + type Ogg = "audio/ogg" } diff --git a/core/src/main/scala/io/finch/endpoint/body.scala b/core/src/main/scala/io/finch/endpoint/body.scala index e3fe5545e..debc1557f 100644 --- a/core/src/main/scala/io/finch/endpoint/body.scala +++ b/core/src/main/scala/io/finch/endpoint/body.scala @@ -17,11 +17,11 @@ abstract private[finch] class FullBody[F[_], A] extends Endpoint[F, A] { protected def present(contentType: String, content: Buf, cs: Charset): F[Output[A]] final def apply(input: Input): EndpointResult[F, A] = - if (input.request.isChunked) EndpointResult.NotMatched[F] + if input.request.isChunked then EndpointResult.NotMatched[F] else { - val output = F.suspend { + val output = F.defer { val contentLength = input.request.contentLengthOrNull - if (contentLength == null || contentLength == "0") missing + if contentLength == null || contentLength == "0" then missing else present( input.request.mediaTypeOrEmpty, @@ -38,16 +38,16 @@ abstract private[finch] class FullBody[F[_], A] extends Endpoint[F, A] { private[finch] object FullBody { - trait PreparedBody[F[_], A, B] { _: FullBody[F, B] => + trait PreparedBody[F[_], A, B] { self: FullBody[F, B] => protected def prepare(a: A): B } - trait Required[F[_], A] extends PreparedBody[F, A, A] { _: FullBody[F, A] => + trait Required[F[_], A] extends PreparedBody[F, A, A] { self: FullBody[F, A] => protected def prepare(a: A): A = a protected def missing: F[Output[A]] = F.raiseError(Error.NotPresent(items.BodyItem)) } - trait Optional[F[_], A] extends PreparedBody[F, A, Option[A]] { _: FullBody[F, Option[A]] => + trait Optional[F[_], A] extends PreparedBody[F, A, Option[A]] { self: FullBody[F, Option[A]] => protected def prepare(a: A): Option[A] = Some(a) protected def missing: F[Output[Option[A]]] = F.pure(Output.None) } @@ -91,7 +91,7 @@ abstract private[finch] class ChunkedBody[F[_], S[_[_], _], A] extends Endpoint[ protected def prepare(r: Reader[Buf], cs: Charset): Output[S[F, A]] final def apply(input: Input): EndpointResult[F, S[F, A]] = - if (!input.request.isChunked) EndpointResult.NotMatched[F] + if !input.request.isChunked then EndpointResult.NotMatched[F] else EndpointResult.Matched( input, diff --git a/core/src/main/scala/io/finch/endpoint/cookie.scala b/core/src/main/scala/io/finch/endpoint/cookie.scala index a002c38d0..9fad9d924 100644 --- a/core/src/main/scala/io/finch/endpoint/cookie.scala +++ b/core/src/main/scala/io/finch/endpoint/cookie.scala @@ -12,7 +12,7 @@ abstract private[finch] class Cookie[F[_], A](name: String)(implicit protected def present(value: FinagleCookie): F[Output[A]] def apply(input: Input): EndpointResult[F, A] = { - val output = F.suspend { + val output = F.defer { input.request.cookies.get(name) match { case None => missing(name) case Some(value) => present(value) @@ -28,13 +28,13 @@ abstract private[finch] class Cookie[F[_], A](name: String)(implicit private[finch] object Cookie { - trait Optional[F[_]] { _: Cookie[F, Option[FinagleCookie]] => + trait Optional[F[_]] { self: Cookie[F, Option[FinagleCookie]] => protected def missing(name: String): F[Output[Option[FinagleCookie]]] = F.pure(Output.None) protected def present(value: FinagleCookie): F[Output[Option[FinagleCookie]]] = F.pure(Output.payload(Some(value))) } - trait Required[F[_]] { _: Cookie[F, FinagleCookie] => + trait Required[F[_]] { self: Cookie[F, FinagleCookie] => protected def missing(name: String): F[Output[FinagleCookie]] = F.raiseError(Error.NotPresent(items.CookieItem(name))) protected def present(value: FinagleCookie): F[Output[FinagleCookie]] = diff --git a/core/src/main/scala/io/finch/endpoint/endpoint.scala b/core/src/main/scala/io/finch/endpoint/endpoint.scala index ad16838c9..85a0d80a5 100644 --- a/core/src/main/scala/io/finch/endpoint/endpoint.scala +++ b/core/src/main/scala/io/finch/endpoint/endpoint.scala @@ -7,7 +7,6 @@ import cats.effect.{ContextShift, Resource, Sync} import cats.syntax.all._ import com.twitter.finagle.http.{Method => FinagleMethod} import com.twitter.io.Buf -import shapeless.HNil package object endpoint { @@ -16,10 +15,10 @@ package object endpoint { S: ContextShift[F] ) extends Endpoint[F, Buf] { - private def readLoop(left: Buf, stream: InputStream): F[Buf] = F.suspend { + private def readLoop(left: Buf, stream: InputStream): F[Buf] = F.defer { val buffer = new Array[Byte](1024) val n = stream.read(buffer) - if (n == -1) F.pure(left) + if n == -1 then F.pure(left) else readLoop(left.concat(Buf.ByteArray.Owned(buffer, 0, n)), stream) } @@ -31,15 +30,15 @@ package object endpoint { ) } - private[finch] class Asset[F[_]](path: String)(implicit F: Applicative[F]) extends Endpoint[F, HNil] { - final def apply(input: Input): Endpoint.Result[F, HNil] = { + private[finch] class Asset[F[_]](path: String)(implicit F: Applicative[F]) extends Endpoint[F, EmptyTuple] { + final def apply(input: Input): Endpoint.Result[F, EmptyTuple] = { val req = input.request - if (req.method != FinagleMethod.Get || req.path != path) EndpointResult.NotMatched[F] + if req.method != FinagleMethod.Get || req.path != path then EndpointResult.NotMatched[F] else EndpointResult.Matched( input.withRoute(Nil), Trace.fromRoute(input.route), - F.pure(Output.HNil) + F.pure(Output.EmptyTuple) ) } diff --git a/core/src/main/scala/io/finch/endpoint/header.scala b/core/src/main/scala/io/finch/endpoint/header.scala index 7050fd805..8831f1518 100644 --- a/core/src/main/scala/io/finch/endpoint/header.scala +++ b/core/src/main/scala/io/finch/endpoint/header.scala @@ -17,7 +17,7 @@ abstract private[finch] class Header[F[_], G[_], A](name: String)(implicit protected def present(value: A): G[A] final def apply(input: Input): EndpointResult[F, G[A]] = { - val output: F[Output[G[A]]] = F.suspend { + val output: F[Output[G[A]]] = F.defer { input.request.headerMap.getOrNull(name) match { case null => missing(name) case value => @@ -37,13 +37,13 @@ abstract private[finch] class Header[F[_], G[_], A](name: String)(implicit private[finch] object Header { - trait Required[F[_], A] { _: Header[F, Id, A] => + trait Required[F[_], A] { self: Header[F, Id, A] => protected def missing(name: String): F[Output[A]] = F.raiseError(Error.NotPresent(items.HeaderItem(name))) protected def present(value: A): Id[A] = value } - trait Optional[F[_], A] { _: Header[F, Option, A] => + trait Optional[F[_], A] { self: Header[F, Option, A] => protected def missing(name: String): F[Output[Option[A]]] = F.pure(Output.None) protected def present(value: A): Option[A] = Some(value) diff --git a/core/src/main/scala/io/finch/endpoint/method.scala b/core/src/main/scala/io/finch/endpoint/method.scala index 9c779fc19..ee2516eb7 100644 --- a/core/src/main/scala/io/finch/endpoint/method.scala +++ b/core/src/main/scala/io/finch/endpoint/method.scala @@ -6,7 +6,7 @@ import io.finch._ private[finch] class Method[F[_], A](m: FinagleMethod, e: Endpoint[F, A]) extends Endpoint.Mappable[F, A] { self => final def apply(input: Input): EndpointResult[F, A] = - if (input.request.method == m) e(input) + if input.request.method == m then e(input) else e(input) match { case EndpointResult.Matched(_, _, _) => EndpointResult.NotMatched.MethodNotAllowed(m :: Nil) diff --git a/core/src/main/scala/io/finch/endpoint/multipart.scala b/core/src/main/scala/io/finch/endpoint/multipart.scala index a1cbfb1e2..f3b19d681 100644 --- a/core/src/main/scala/io/finch/endpoint/multipart.scala +++ b/core/src/main/scala/io/finch/endpoint/multipart.scala @@ -23,16 +23,16 @@ abstract private[finch] class Attribute[F[_]: Sync, G[_], A](val name: String)(i protected def unparsed(errors: NonEmptyList[Throwable], tag: ClassTag[A]): F[Output[G[A]]] private def all(input: Input): Option[NonEmptyList[String]] = - for { + for m <- Multipart.decodeIfNeeded(input.request) attrs <- m.attributes.get(name) nel <- NonEmptyList.fromList(attrs.toList) - } yield nel + yield nel final def apply(input: Input): EndpointResult[F, G[A]] = - if (input.request.isChunked) EndpointResult.NotMatched[F] + if input.request.isChunked then EndpointResult.NotMatched[F] else { - val output = F.suspend { + val output = F.defer { all(input) match { case None => missing(name) case Some(values) => @@ -54,42 +54,42 @@ abstract private[finch] class Attribute[F[_]: Sync, G[_], A](val name: String)(i private[finch] object Attribute { - trait SingleError[F[_], G[_], A] { _: Attribute[F, G, A] => + trait SingleError[F[_], G[_], A] { self: Attribute[F, G, A] => protected def unparsed(errors: NonEmptyList[Throwable], tag: ClassTag[A]): F[Output[G[A]]] = F.raiseError(Error.NotParsed(items.ParamItem(name), tag, errors.head)) final override def toString: String = s"attribute($name)" } - trait MultipleErrors[F[_], G[_], A] { _: Attribute[F, G, A] => + trait MultipleErrors[F[_], G[_], A] { self: Attribute[F, G, A] => protected def unparsed(errors: NonEmptyList[Throwable], tag: ClassTag[A]): F[Output[G[A]]] = F.raiseError(Errors(errors.map(t => Error.NotParsed(items.ParamItem(name), tag, t)))) final override def toString: String = s"attributes($name)" } - trait Required[F[_], A] { _: Attribute[F, Id, A] => + trait Required[F[_], A] { self: Attribute[F, Id, A] => protected def missing(name: String): F[Output[A]] = F.raiseError(Error.NotPresent(items.ParamItem(name))) protected def present(value: NonEmptyList[A]): F[Output[A]] = F.pure(Output.payload(value.head)) } - trait Optional[F[_], A] { _: Attribute[F, Option, A] => + trait Optional[F[_], A] { self: Attribute[F, Option, A] => protected def missing(name: String): F[Output[Option[A]]] = F.pure(Output.None) protected def present(value: NonEmptyList[A]): F[Output[Option[A]]] = F.pure(Output.payload(Some(value.head))) } - trait AllowEmpty[F[_], A] { _: Attribute[F, List, A] => + trait AllowEmpty[F[_], A] { self: Attribute[F, List, A] => protected def missing(name: String): F[Output[List[A]]] = F.pure(Output.payload(Nil)) protected def present(value: NonEmptyList[A]): F[Output[List[A]]] = F.pure(Output.payload(value.toList)) } - trait NonEmpty[F[_], A] { _: Attribute[F, NonEmptyList, A] => + trait NonEmpty[F[_], A] { self: Attribute[F, NonEmptyList, A] => protected def missing(name: String): F[Output[NonEmptyList[A]]] = F.raiseError(Error.NotPresent(items.ParamItem(name))) protected def present(value: NonEmptyList[A]): F[Output[NonEmptyList[A]]] = @@ -104,14 +104,14 @@ abstract private[finch] class FileUpload[F[_]: Sync, G[_]](name: String) extends protected def present(a: NonEmptyList[FinagleFileUpload]): F[Output[G[FinagleFileUpload]]] final private def all(input: Input): Option[NonEmptyList[FinagleFileUpload]] = - for { + for mp <- Multipart.decodeIfNeeded(input.request) all <- mp.files.get(name) nel <- NonEmptyList.fromList(all.toList) - } yield nel + yield nel final def apply(input: Input): EndpointResult[F, G[FinagleFileUpload]] = - if (input.request.isChunked) EndpointResult.NotMatched[F] + if input.request.isChunked then EndpointResult.NotMatched[F] else { val output = Sync[F].suspend { all(input) match { @@ -129,28 +129,28 @@ abstract private[finch] class FileUpload[F[_]: Sync, G[_]](name: String) extends private[finch] object FileUpload { - trait Required[F[_]] { _: FileUpload[F, Id] => + trait Required[F[_]] { self: FileUpload[F, Id] => protected def missing(name: String): F[Output[FinagleFileUpload]] = F.raiseError(Error.NotPresent(items.ParamItem(name))) protected def present(a: NonEmptyList[FinagleFileUpload]): F[Output[FinagleFileUpload]] = F.pure(Output.payload(a.head)) } - trait Optional[F[_]] { _: FileUpload[F, Option] => + trait Optional[F[_]] { self: FileUpload[F, Option] => protected def missing(name: String): F[Output[Option[FinagleFileUpload]]] = F.pure(Output.payload(None)) protected def present(a: NonEmptyList[FinagleFileUpload]): F[Output[Option[FinagleFileUpload]]] = F.pure(Output.payload(Some(a.head))) } - trait AllowEmpty[F[_]] { _: FileUpload[F, List] => + trait AllowEmpty[F[_]] { self: FileUpload[F, List] => protected def missing(name: String): F[Output[List[FinagleFileUpload]]] = F.pure(Output.payload(Nil)) protected def present(fa: NonEmptyList[FinagleFileUpload]): F[Output[List[FinagleFileUpload]]] = F.pure(Output.payload(fa.toList)) } - trait NonEmpty[F[_]] { _: FileUpload[F, NonEmptyList] => + trait NonEmpty[F[_]] { self: FileUpload[F, NonEmptyList] => protected def missing(name: String): F[Output[NonEmptyList[FinagleFileUpload]]] = F.raiseError(Error.NotPresent(items.ParamItem(name))) protected def present(fa: NonEmptyList[FinagleFileUpload]): F[Output[NonEmptyList[FinagleFileUpload]]] = diff --git a/core/src/main/scala/io/finch/endpoint/param.scala b/core/src/main/scala/io/finch/endpoint/param.scala index 2de558f78..3d1ae9fdb 100644 --- a/core/src/main/scala/io/finch/endpoint/param.scala +++ b/core/src/main/scala/io/finch/endpoint/param.scala @@ -17,7 +17,7 @@ abstract private[finch] class Param[F[_], G[_], A](name: String)(implicit protected def present(value: A): G[A] final def apply(input: Input): EndpointResult[F, G[A]] = { - val output: F[Output[G[A]]] = F.suspend { + val output: F[Output[G[A]]] = F.defer { input.request.params.get(name) match { case None => missing(name) case Some(value) => @@ -37,13 +37,13 @@ abstract private[finch] class Param[F[_], G[_], A](name: String)(implicit private[finch] object Param { - trait Required[F[_], A] { _: Param[F, Id, A] => + trait Required[F[_], A] { self: Param[F, Id, A] => protected def missing(name: String): F[Output[A]] = F.raiseError(Error.NotPresent(items.ParamItem(name))) protected def present(a: A): Id[A] = a } - trait Optional[F[_], A] { _: Param[F, Option, A] => + trait Optional[F[_], A] { self: Param[F, Option, A] => protected def missing(name: String): F[Output[Option[A]]] = F.pure(Output.None) protected def present(a: A): Option[A] = Some(a) } @@ -59,7 +59,7 @@ abstract private[finch] class Params[F[_], G[_], A](name: String)(implicit protected def present(value: Iterable[A]): G[A] final def apply(input: Input): EndpointResult[F, G[A]] = { - val output: F[Output[G[A]]] = F.suspend { + val output: F[Output[G[A]]] = F.defer { input.request.params.getAll(name) match { case value if value.isEmpty => missing(name) case value => @@ -84,12 +84,12 @@ abstract private[finch] class Params[F[_], G[_], A](name: String)(implicit private[finch] object Params { - trait AllowEmpty[F[_], A] { _: Params[F, List, A] => + trait AllowEmpty[F[_], A] { self: Params[F, List, A] => protected def missing(name: String): F[Output[List[A]]] = F.pure(Output.payload(Nil)) protected def present(value: Iterable[A]): List[A] = value.toList } - trait NonEmpty[F[_], A] { _: Params[F, NonEmptyList, A] => + trait NonEmpty[F[_], A] { self: Params[F, NonEmptyList, A] => protected def missing(name: String): F[Output[NonEmptyList[A]]] = F.raiseError(Error.NotPresent(items.ParamItem(name))) protected def present(value: Iterable[A]): NonEmptyList[A] = diff --git a/core/src/main/scala/io/finch/endpoint/path.scala b/core/src/main/scala/io/finch/endpoint/path.scala index cb31e46bc..7c89115bb 100644 --- a/core/src/main/scala/io/finch/endpoint/path.scala +++ b/core/src/main/scala/io/finch/endpoint/path.scala @@ -5,17 +5,16 @@ import scala.reflect.ClassTag import cats.Applicative import io.finch._ import io.netty.handler.codec.http.QueryStringDecoder -import shapeless.HNil private[finch] class MatchPath[F[_]](s: String)(implicit F: Applicative[F] -) extends Endpoint[F, HNil] { - final def apply(input: Input): EndpointResult[F, HNil] = input.route match { +) extends Endpoint[F, EmptyTuple] { + final def apply(input: Input): EndpointResult[F, EmptyTuple] = input.route match { case `s` :: rest => EndpointResult.Matched( input.withRoute(rest), Trace.segment(s), - F.pure(Output.HNil) + F.pure(Output.EmptyTuple) ) case _ => EndpointResult.NotMatched[F] } diff --git a/core/src/main/scala/io/finch/internal/Mapper.scala b/core/src/main/scala/io/finch/internal/Mapper.scala deleted file mode 100644 index 821be9940..000000000 --- a/core/src/main/scala/io/finch/internal/Mapper.scala +++ /dev/null @@ -1,121 +0,0 @@ -package io.finch.internal - -import cats.MonadError -import cats.effect.Async -import cats.syntax.functor._ -import com.twitter.finagle.http.Response -import io.finch.{Endpoint, Output} -import shapeless.HNil -import shapeless.ops.function.FnToProduct - -/** - * A type class that allows the [[Endpoint]] to be mapped to either `A => B` or `A => Future[B]`. - * @groupname LowPriorityMapper Low Priority Mapper Conversions - * @groupprio LowPriorityMapper 0 - * @groupname HighPriorityMapper High priority mapper conversions - * @groupprio HighPriorityMapper 1 - */ -trait Mapper[F[_], A] { - type Out - - /** - * @param e The endpoint to map - * @tparam X Hack to stop the compiler from converting this to a SAM - * @return An endpoint that returns an `Out` - */ - def apply[X](e: Endpoint[F, A]): Endpoint[F, Out] -} - -private[finch] trait LowPriorityMapperConversions { - - type Aux[F[_], A, B] = Mapper[F, A] { type Out = B } - - def instance[F[_], A, B](f: Endpoint[F, A] => Endpoint[F, B]): Mapper.Aux[F, A, B] = new Mapper[F, A] { - type Out = B - def apply[X](e: Endpoint[F, A]): Endpoint[F, B] = f(e) - } - - /** - * @group LowPriorityMapper - */ - implicit def mapperFromOutputFunction[F[_], A, B](f: A => Output[B])(implicit - F: MonadError[F, Throwable] - ): Mapper.Aux[F, A, B] = instance(_.mapOutput(f)) - - /** - * @group LowPriorityMapper - */ - implicit def mapperFromResponseFunction[F[_], A](f: A => Response)(implicit - F: MonadError[F, Throwable] - ): Mapper.Aux[F, A, Response] = instance(_.mapOutput(f.andThen(r => Output.payload(r, r.status)))) -} - -private[finch] trait HighPriorityMapperConversions extends LowPriorityMapperConversions { - - /** - * @group HighPriorityMapper - */ - implicit def mapperFromOutputHFunction[F[_], A, B, FN, OB](f: FN)(implicit - F: MonadError[F, Throwable], - ftp: FnToProduct.Aux[FN, A => OB], - ev: OB <:< Output[B] - ): Mapper.Aux[F, A, B] = instance(_.mapOutput(value => ev(ftp(f)(value)))) - - /** - * @group HighPriorityMapper - */ - implicit def mapperFromResponseHFunction[F[_], A, FN, R](f: FN)(implicit - F: MonadError[F, Throwable], - ftp: FnToProduct.Aux[FN, A => R], - ev: R <:< Response - ): Mapper.Aux[F, A, Response] = instance(_.mapOutput { value => - val r = ev(ftp(f)(value)) - Output.payload(r, r.status) - }) - - /** - * @group HighPriorityMapper - */ - implicit def mapperFromOutputValue[F[_], A](o: => Output[A])(implicit - F: MonadError[F, Throwable] - ): Mapper.Aux[F, HNil, A] = instance(_.mapOutput(_ => o)) - - /** - * @group HighPriorityMapper - */ - implicit def mapperFromResponseValue[F[_]](r: => Response)(implicit - F: MonadError[F, Throwable] - ): Mapper.Aux[F, HNil, Response] = instance(_.mapOutput(_ => Output.payload(r, r.status))) - - implicit def mapperFromKindToEffectOutputFunction[A, B, F[_], G[_]: Async](f: A => F[Output[B]])(implicit conv: ToAsync[F, G]): Mapper.Aux[G, A, B] = - instance(_.mapOutputAsync(a => conv.apply(f(a)))) - - implicit def mapperFromKindToEffectOutputValue[A, B, F[_], G[_]: Async](f: => F[Output[B]])(implicit conv: ToAsync[F, G]): Mapper.Aux[G, A, B] = instance( - _.mapOutputAsync(_ => conv.apply(f)) - ) - - implicit def mapperFromKindToEffectResponsFunction[A, F[_], G[_]: Async](f: A => F[Response])(implicit conv: ToAsync[F, G]): Mapper.Aux[G, A, Response] = - instance(_.mapOutputAsync(f.andThen(fr => conv(fr).map(r => Output.payload(r, r.status))))) - - implicit def mapperFromKindToEffectResponseValue[A, F[_], G[_]: Async](f: => F[Response])(implicit conv: ToAsync[F, G]): Mapper.Aux[G, A, Response] = - instance(_.mapOutputAsync(_ => conv(f).map(r => Output.payload(r, r.status)))) -} - -object Mapper extends HighPriorityMapperConversions { - - implicit def mapperFromKindOutputHFunction[F[_]: Async, G[_], A, B, FN, FOB](f: FN)(implicit - ftp: FnToProduct.Aux[FN, A => FOB], - ev: FOB <:< G[Output[B]], - conv: ToAsync[G, F] - ): Mapper.Aux[F, A, B] = - instance(_.mapOutputAsync(a => conv.apply(ev(ftp(f)(a))))) - - implicit def mapperFromKindResponseHFunction[F[_]: Async, G[_], A, FN, FR](f: FN)(implicit - ftp: FnToProduct.Aux[FN, A => FR], - ev: FR <:< G[Response], - conv: ToAsync[G, F] - ): Mapper.Aux[F, A, Response] = instance(_.mapOutputAsync { value => - val fr = conv(ev(ftp(f)(value))) - fr.map(r => Output.payload(r, r.status)) - }) -} diff --git a/core/src/main/scala/io/finch/internal/OrElse.scala b/core/src/main/scala/io/finch/internal/OrElse.scala new file mode 100644 index 000000000..54a418825 --- /dev/null +++ b/core/src/main/scala/io/finch/internal/OrElse.scala @@ -0,0 +1,21 @@ +package io.finch.internal + +sealed trait OrElse[A, B] { + def fold[C](a: A => C, b: B => C): C +} + +object OrElse extends LowPriorityElse { + + given left[A, B](using aa: A): OrElse[A, B] = new OrElse[A, B] { + def fold[C](a: A => C, b: B => C): C = a(aa) + } + +} + +trait LowPriorityElse { + + given right[A, B](using bb: B): OrElse[A, B] = new OrElse[A, B] { + def fold[C](a: A => C, b: B => C): C = b(bb) + } + +} diff --git a/core/src/main/scala/io/finch/internal/PairJoin.scala b/core/src/main/scala/io/finch/internal/PairJoin.scala index 5ad9fd131..7d8b7056f 100644 --- a/core/src/main/scala/io/finch/internal/PairJoin.scala +++ b/core/src/main/scala/io/finch/internal/PairJoin.scala @@ -1,38 +1,67 @@ package io.finch.internal -import shapeless.ops.adjoin.Adjoin -import shapeless.{::, DepFn2, HNil} +import scala.Tuple.Concat /** - * We need a version of [[shapeless.ops.adjoin.Adjoin]] that provides slightly different behavior in - * the case of singleton results (we simply return the value, not a singleton `HList`). - * @groupname LowPriorityPair Low priority `PairAdjoin` - * @groupprio LowPriorityPair 0 - */ -trait PairAdjoin[A, B] extends DepFn2[A, B] - -private[finch] trait LowPriorityPairAdjoin { - type Aux[A, B, Out0] = PairAdjoin[A, B] { type Out = Out0 } - - /** - * @group LowPriorityPair - */ - implicit def pairAdjoin[A, B, Out0](implicit - adjoin: Adjoin.Aux[A :: B :: HNil, Out0] - ): Aux[A, B, Out0] = - new PairAdjoin[A, B] { - type Out = Out0 - - def apply(a: A, b: B): Out0 = adjoin(a :: b :: HNil) - } + * Compile two values together such that: + * - if both arguments are non-empty tuples, they're concatenated into a bigger tuple + * - if one of the arguments is a non-empty tuple, one prepends the other (a|b *: b|a) + * - if one of the arguments is an empty tuple, the second argument is returned + */ +trait PairAdjoin[A, B] { + type Out + + def apply(a: A, b: B): Out +} + +object PairAdjoin extends LowPriorityAdjoin { + + type Aux[A, B, O] = PairAdjoin[A, B] { type Out = O } + + given emptyAdjoin: PairAdjoin.Aux[EmptyTuple, EmptyTuple, EmptyTuple] = new PairAdjoin[EmptyTuple, EmptyTuple] { + type Out = EmptyTuple + + def apply(a: EmptyTuple, b: EmptyTuple): Out = EmptyTuple + } + + given emptyLeftAdjoin[B]: PairAdjoin.Aux[EmptyTuple, B, B] = new PairAdjoin[EmptyTuple, B] { + type Out = B + + def apply(a: EmptyTuple, b: B): Out = b + } + + given emptyRightAdjoin[A]: PairAdjoin.Aux[A, EmptyTuple, A] = new PairAdjoin[A, EmptyTuple] { + type Out = A + + def apply(a: A, b: EmptyTuple): Out = a + } + + given tuplesAdjoin[A <: Tuple, B <: Tuple]: PairAdjoin.Aux[A, B, Concat[A, B]] = new PairAdjoin[A, B] { + type Out = Concat[A, B] + + def apply(a: A, b: B): Out = a ++ b + } + +} + +private[finch] trait LowPriorityAdjoin extends LowLowPriorityAdjoin { + given rightAdjoin[A <: Tuple, B]: PairAdjoin.Aux[A, B, Concat[A, B *: EmptyTuple]] = new PairAdjoin[A, B] { + type Out = Concat[A, B *: EmptyTuple] + + def apply(a: A, b: B): Out = a ++ (b *: EmptyTuple) + } + + given leftAdjoin[A, B <: Tuple]: PairAdjoin.Aux[A, B, A *: B] = new PairAdjoin[A, B] { + type Out = A *: B + + def apply(a: A, b: B): Out = a *: b + } } -object PairAdjoin extends LowPriorityPairAdjoin { - implicit def singletonPairAdjoin[A, B, C](implicit - adjoin: Adjoin.Aux[A :: B :: HNil, C :: HNil] - ): Aux[A, B, C] = new PairAdjoin[A, B] { - type Out = C +private[finch] trait LowLowPriorityAdjoin { + given constructAdjoin[A, B]: PairAdjoin.Aux[A, B, A *: B *: EmptyTuple] = new PairAdjoin[A, B] { + type Out = A *: B *: EmptyTuple - def apply(a: A, b: B): C = adjoin(a :: b :: HNil).head + def apply(a: A, b: B): Out = a *: b *: EmptyTuple } } diff --git a/core/src/main/scala/io/finch/internal/ParseNumber.scala b/core/src/main/scala/io/finch/internal/ParseNumber.scala index ec012feb6..ef2e03328 100644 --- a/core/src/main/scala/io/finch/internal/ParseNumber.scala +++ b/core/src/main/scala/io/finch/internal/ParseNumber.scala @@ -14,31 +14,31 @@ abstract class ParseNumber[@specialized(Int, Long) A] { var result = 0L var i = 0 - if (s.length > 0) { + if s.length > 0 then { val firstChar = s.charAt(0) - if (firstChar < '0') { - if (firstChar == '-') { + if firstChar < '0' then { + if firstChar == '-' then { negative = true limit = min - } else if (firstChar != '+') return None + } else if firstChar != '+' then return None - if (s.length == 1) return None + if s.length == 1 then return None i += 1 } // skip zeros - while (i < s.length && s.charAt(i) == '0') i += 1 + while i < s.length && s.charAt(i) == '0' do i += 1 val mulMin = limit / 10L - while (i < s.length) { + while i < s.length do { val c = s.charAt(i) - if ('0' <= c && c <= '9') { - if (result < mulMin) return None + if '0' <= c && c <= '9' then { + if result < mulMin then return None result = result * 10L val digit = c - '0' - if (result < limit + digit) return None + if result < limit + digit then return None result = result - digit } else return None @@ -46,7 +46,7 @@ abstract class ParseNumber[@specialized(Int, Long) A] { } } else return None - Some(prepare(if (negative) result else -result)) + Some(prepare(if negative then result else -result)) } // scalastyle:on return } diff --git a/core/src/main/scala/io/finch/internal/ToAsync.scala b/core/src/main/scala/io/finch/internal/ToAsync.scala deleted file mode 100644 index 4cfedaa90..000000000 --- a/core/src/main/scala/io/finch/internal/ToAsync.scala +++ /dev/null @@ -1,37 +0,0 @@ -package io.finch.internal - -import scala.concurrent.{Future => ScalaFuture} -import scala.util.{Failure, Success} - -import cats.effect.Async -import cats.~> -import com.twitter.util.{Future => TwitterFuture, Return, Throw} - -trait ToAsync[A[_], B[_]] extends ~>[A, B] - -object ToAsync { - - implicit def idAsync[E[_]: Async]: ToAsync[E, E] = new ToAsync[E, E] { - def apply[A](a: E[A]): E[A] = a - } - - implicit def twFutureToAsync[E[_]: Async]: ToAsync[TwitterFuture, E] = new ToAsync[TwitterFuture, E] { - def apply[A](a: TwitterFuture[A]): E[A] = - Async[E].async { cb => - a.respond { - case Return(r) => cb(Right(r)) - case Throw(t) => cb(Left(t)) - } - } - } - - implicit def scFutureToAsync[E[_]: Async]: ToAsync[ScalaFuture, E] = new ToAsync[ScalaFuture, E] { - def apply[A](a: ScalaFuture[A]): E[A] = - Async[E].async { cb => - a.onComplete { - case Success(s) => cb(Right(s)) - case Failure(t) => cb(Left(t)) - }(DummyExecutionContext) - } - } -} diff --git a/core/src/main/scala/io/finch/internal/currentTime.scala b/core/src/main/scala/io/finch/internal/currentTime.scala index 6e7087a4a..7bed9331c 100644 --- a/core/src/main/scala/io/finch/internal/currentTime.scala +++ b/core/src/main/scala/io/finch/internal/currentTime.scala @@ -18,7 +18,7 @@ object currentTime { val local = last.get() val time = System.currentTimeMillis() - if (time - local.millis > 1000) { + if time - local.millis > 1000 then { local.millis = time local.header = formatter.format(Instant.ofEpochMilli(time)) } diff --git a/core/src/main/scala/io/finch/internal/package.scala b/core/src/main/scala/io/finch/internal/package.scala index cdcc67300..b6f8f2672 100644 --- a/core/src/main/scala/io/finch/internal/package.scala +++ b/core/src/main/scala/io/finch/internal/package.scala @@ -7,11 +7,10 @@ import com.twitter.finagle.http.{Fields, Message} import com.twitter.io.Buf /** - * This package contains an internal-use only type-classes and utilities that power Finch's API. - * - * It's not recommended to use any of the internal API directly, since it might change without any - * deprecation cycles. - */ + * This package contains an internal-use only type-classes and utilities that power Finch's API. + * + * It's not recommended to use any of the internal API directly, since it might change without any deprecation cycles. + */ package object internal { @inline final private[this] val someTrue: Option[Boolean] = Some(true) @@ -21,13 +20,13 @@ package object internal { val Utf32: Charset = Charset.forName("UTF-32") /** - * Enriches any string with fast `tooX` conversions. - */ + * Enriches any string with fast `tooX` conversions. + */ implicit class TooFastString(val s: String) extends AnyVal { /** - * Converts this string to the optional boolean value. - */ + * Converts this string to the optional boolean value. + */ final def tooBoolean: Option[Boolean] = s match { case "true" => someTrue case "false" => someFalse @@ -35,19 +34,17 @@ package object internal { } /** - * Converts this string to the optional integer value. The maximum allowed length for a number - * string is 32. - */ + * Converts this string to the optional integer value. The maximum allowed length for a number string is 32. + */ final def tooInt: Option[Int] = - if (s.length == 0 || s.length > 32) None + if s.length == 0 || s.length > 32 then None else parseInt(s) /** - * Converts this string to the optional long value. The maximum allowed length for a number - * string is 32. - */ + * Converts this string to the optional long value. The maximum allowed length for a number string is 32. + */ final def tooLong: Option[Long] = - if (s.length == 0 || s.length > 32) None + if s.length == 0 || s.length > 32 then None else parseLong(s) } @@ -59,10 +56,10 @@ package object internal { // Returns message's media type or empty string. def mediaTypeOrEmpty: String = { val ct = self.headerMap.getOrNull(Fields.ContentType) - if (ct == null) "" + if ct == null then "" else { val semi = ct.indexOf(';') - if (semi == -1) ct + if semi == -1 then ct else ct.substring(0, semi) } } @@ -71,14 +68,14 @@ package object internal { def charsetOrUtf8: Charset = { val contentType = self.headerMap.getOrNull(Fields.ContentType) - if (contentType == null) StandardCharsets.UTF_8 + if contentType == null then StandardCharsets.UTF_8 else { val charsetEq = contentType.indexOf("charset=") - if (charsetEq == -1) StandardCharsets.UTF_8 + if charsetEq == -1 then StandardCharsets.UTF_8 else { val from = charsetEq + "charset=".length val semi = contentType.indexOf(';', from) - val to = if (semi == -1) contentType.length else semi + val to = if semi == -1 then contentType.length else semi Charset.forName(contentType.substring(from, to)) } } diff --git a/core/src/main/scala/io/finch/package.scala b/core/src/main/scala/io/finch/package.scala index 83ad58f5e..b132a4faf 100644 --- a/core/src/main/scala/io/finch/package.scala +++ b/core/src/main/scala/io/finch/package.scala @@ -3,13 +3,10 @@ package io import cats.effect.IO /** - * This is a root package of the Finch library, which provides an immutable layer of functions and - * types atop of Finagle for writing lightweight HTTP services. - */ + * This is a root package of the Finch library, which provides an immutable layer of functions and types atop of Finagle for writing lightweight HTTP services. + */ package object finch extends Outputs with ValidationRules { - type ToAsync[F[_], E[_]] = internal.ToAsync[F, E] - object catsEffect extends EndpointModule[IO] object items { diff --git a/core/src/test/scala/io/finch/BodySpec.scala b/core/src/test/scala/io/finch/BodySpec.scala index 5cbf4a189..9920ae2c4 100644 --- a/core/src/test/scala/io/finch/BodySpec.scala +++ b/core/src/test/scala/io/finch/BodySpec.scala @@ -40,14 +40,14 @@ class BodySpec extends FinchSpec { } it should "respond with a value when present and required" in { - check { f: Foo => + check { (f: Foo) => val i = Input.post("/").withBody[Text.Plain](f) body[Foo, Text.Plain].apply(i).awaitValueUnsafe() === Some(f) } } it should "respond with Some(value) when it'ss present and optional" in { - check { f: Foo => + check { (f: Foo) => val i = Input.post("/").withBody[Text.Plain](f) bodyOption[Foo, Text.Plain].apply(i).awaitValueUnsafe().flatten === Some(f) } @@ -62,17 +62,17 @@ class BodySpec extends FinchSpec { } it should "never evaluate until run" in { - check { f: Foo => + check { (f: Foo) => val i = Input.post("/").withBody[Text.Plain](f) - implicit val ed = new EvalDecode[Foo](Decode[Foo, Text.Plain]) + implicit val ed = new EvalDecode[Foo](Foo.decodeTextFoo) textBody[Foo].apply(i) !ed.evaluated } } it should "respect Content-Type header and pick corresponding decoder for coproduct" in { - check { f: Foo => + check { (f: Foo) => val plain = Input.post("/").withBody[Text.Plain](f) val csv = Input.post("/").withBody[Application.Csv](f) val endpoint = body[Foo, Text.Plain :+: Application.Csv :+: CNil] diff --git a/core/src/test/scala/io/finch/BootstrapSpec.scala b/core/src/test/scala/io/finch/BootstrapSpec.scala index f4a3d98ab..4aa58f2d4 100644 --- a/core/src/test/scala/io/finch/BootstrapSpec.scala +++ b/core/src/test/scala/io/finch/BootstrapSpec.scala @@ -14,7 +14,7 @@ class BootstrapSpec extends FinchSpec { behavior of "Bootstrap" it should "handle both Error and Errors" in { - check { e: Either[Error, Errors] => + check { (e: Either[Error, Errors]) => val exception = e.fold[Exception](identity, identity) val ee = Endpoint[IO].liftAsync[Unit](IO.raiseError(exception)) @@ -31,7 +31,7 @@ class BootstrapSpec extends FinchSpec { } it should "respond 404 if endpoint is not matched" in { - check { req: Request => + check { (req: Request) => val s = Endpoint[IO].empty[Unit].compileAs[Text.Plain] val (_, Right(rep)) = s(req).unsafeRunSync() @@ -77,7 +77,7 @@ class BootstrapSpec extends FinchSpec { } it should "match the request version" in { - check { req: Request => + check { (req: Request) => val s = Endpoint[IO].const(()).compileAs[Text.Plain] val (_, Right(rep)) = s(req).unsafeRunSync() @@ -110,7 +110,7 @@ class BootstrapSpec extends FinchSpec { } it should "capture Trace for failures and successes" in { - check { req: Request => + check { (req: Request) => val p = req.path.split("/").drop(1) val endpoint = p.map(s => path(s)).foldLeft(Endpoint[IO].const(HNil: HNil))((p, e) => p :: e) diff --git a/core/src/test/scala/io/finch/EncodeSpec.scala b/core/src/test/scala/io/finch/EncodeSpec.scala index 90d05040d..1b4d0dbf6 100644 --- a/core/src/test/scala/io/finch/EncodeSpec.scala +++ b/core/src/test/scala/io/finch/EncodeSpec.scala @@ -14,7 +14,7 @@ class EncodeSpec extends FinchSpec { checkAll("Encode.Text[Either[UUID, Float]]", EncodeLaws.text[Either[UUID, Float]].all) it should "round trip Unit" in { - check { cs: Charset => + check { (cs: Charset) => implicitly[Encode[Unit]].apply((), cs) === Buf.Empty } } diff --git a/core/src/test/scala/io/finch/EndToEndSpec.scala b/core/src/test/scala/io/finch/EndToEndSpec.scala index e4349c12c..4c8f249b9 100644 --- a/core/src/test/scala/io/finch/EndToEndSpec.scala +++ b/core/src/test/scala/io/finch/EndToEndSpec.scala @@ -16,7 +16,7 @@ class EndToEndSpec extends FinchSpec { Application.Javascript :+: Application.OctetStream :+: Application.RssXml :+: Application.WwwFormUrlencoded :+: Application.Xml :+: Text.Plain :+: Text.Html :+: Text.EventStream :+: CNil - implicit private def encodeHNil[CT <: String]: Encode.Aux[HNil, CT] = Encode.instance((_, _) => Buf.Utf8("hnil")) + implicit private def encodeEmptyTuple[CT <: String]: Encode.Aux[EmptyTuple, CT] = Encode.instance((_, _) => Buf.Utf8("hnil")) private val allContentTypes = Seq( "application/json", @@ -37,10 +37,10 @@ class EndToEndSpec extends FinchSpec { Encode.text((_, cs) => Buf.ByteArray.Owned("ERR!".getBytes(cs.name))) val service: Service[Request, Response] = ( - get("foo" :: path[String]) { s: String => Ok(Foo(s)) } :+: + get("foo" :: path[String])((s: String) => Ok(Foo(s))) :+: get("bar")(Created("bar")) :+: get("baz")(BadRequest(new IllegalArgumentException("foo")): Output[Unit]) :+: - get("qux" :: param[Foo]("foo")) { f: Foo => Created(f) } + get("qux" :: param[Foo]("foo"))((f: Foo) => Created(f)) ).toServiceAs[Text.Plain] val rep1 = Await.result(service(Request("/foo/bar"))) @@ -70,7 +70,7 @@ class EndToEndSpec extends FinchSpec { } it should "ignore Accept header when single type is used for serve" in { - check { req: Request => + check { (req: Request) => val s = Bootstrap.serve[Text.Plain](pathAny).toService val rep = Await.result(s(req)) @@ -79,7 +79,7 @@ class EndToEndSpec extends FinchSpec { } it should "respect Accept header when coproduct type is used for serve" in { - check { req: Request => + check { (req: Request) => val s = Bootstrap.serve[AllContentTypes](pathAny).toService val rep = Await.result(s(req)) @@ -104,7 +104,7 @@ class EndToEndSpec extends FinchSpec { } it should "select last encoder when Accept header is missing/empty" in { - check { req: Request => + check { (req: Request) => req.headerMap.remove(Fields.Accept) val s = Bootstrap.serve[AllContentTypes](pathAny).toService val rep = Await.result(s(req)) diff --git a/core/src/test/scala/io/finch/EndpointSpec.scala b/core/src/test/scala/io/finch/EndpointSpec.scala index 5017fee38..a1a4a6ece 100644 --- a/core/src/test/scala/io/finch/EndpointSpec.scala +++ b/core/src/test/scala/io/finch/EndpointSpec.scala @@ -38,33 +38,33 @@ class EndpointSpec extends FinchSpec { private[this] val emptyRequest = Request() it should "support very basic map" in { - check { i: Input => + check { (i: Input) => path[String].map(_ * 2).apply(i).awaitValueUnsafe() === i.route.headOption.map(_ * 2) } } it should "correctly run mapF" in { - check { e: Endpoint[IO, String] => + check { (e: Endpoint[IO, String]) => val fn: String => Int = _.length e.transformF(_.map(fn)) <-> e.map(fn) } } it should "support transformOutput" in { - check { i: Input => + check { (i: Input) => val fn = (fs: IO[Output[String]]) => fs.map(_.map(_ * 2)) path[String].transformOutput(fn).apply(i).awaitValueUnsafe() === i.route.headOption.map(_ * 2) } } it should "propagate the default (Ok) output" in { - check { i: Input => + check { (i: Input) => path[String].apply(i).awaitOutputUnsafe() === i.route.headOption.map(s => Ok(s)) } } it should "propagate the default (Ok) output through its map'd/mapAsync'd version" in { - check { i: Input => + check { (i: Input) => val expected = i.route.headOption.map(s => Ok(s.length)) path[String].map(s => s.length).apply(i).awaitOutputUnsafe() === expected && @@ -76,13 +76,13 @@ class EndpointSpec extends FinchSpec { def expected(i: Int): Output[Int] = Created(i).withHeader("A" -> "B").withCookie(new Cookie("C", "D")) - check { i: Input => + check { (i: Input) => path[String].mapOutputAsync(s => IO.pure(expected(s.length))).apply(i).awaitOutputUnsafe() === i.route.headOption.map(s => expected(s.length)) } - check { i: Input => - val e = i.route.dropRight(1).map(s => path(s)).foldLeft[Endpoint[IO, HNil]](zero)((acc, ee) => acc :: ee) + check { (i: Input) => + val e = i.route.dropRight(1).map(s => path(s)).foldLeft[Endpoint[IO, EmptyTuple]](zero)((acc, ee) => acc :: ee) val v = (e :: path[String]).mapOutputAsync(s => IO.pure(expected(s.length))).apply(i) v.awaitOutputUnsafe() === i.route.lastOption.map(s => expected(s.length)) @@ -90,7 +90,7 @@ class EndpointSpec extends FinchSpec { } it should "match one patch segment" in { - check { i: Input => + check { (i: Input) => val v = i.route.headOption.flatMap(s => path(s).apply(i).remainder) v.isEmpty || v === Some(i.withRoute(i.route.tail)) @@ -98,20 +98,20 @@ class EndpointSpec extends FinchSpec { } it should "always match the entire input with *" in { - check { i: Input => + check { (i: Input) => pathAny.apply(i).remainder === Some(i.copy(route = Nil)) } } it should "match empty path" in { - check { i: Input => + check { (i: Input) => (i.route.isEmpty && pathEmpty.apply(i).isMatched) || (!i.route.isEmpty && !pathEmpty.apply(i).isMatched) } } it should "match the HTTP method" in { - def matchMethod(m: Method, f: Endpoint[IO, HNil] => Endpoint[IO, HNil]): Input => Boolean = { i: Input => + def matchMethod(m: Method, f: Endpoint[IO, EmptyTuple] => Endpoint[IO, EmptyTuple]): Input => Boolean = { (i: Input) => val v = f(zero)(i) (i.request.method === m && v.remainder === Some(i)) || (i.request.method != m && v.remainder === None) @@ -128,14 +128,14 @@ class EndpointSpec extends FinchSpec { } it should "always match the identity instance" in { - check { i: Input => + check { (i: Input) => zero.apply(i).remainder === Some(i) } } it should "match the entire input" in { - check { i: Input => - val e = i.route.map(s => path(s)).foldLeft[Endpoint[IO, HNil]](zero)((acc, e) => acc :: e) + check { (i: Input) => + val e = i.route.map(s => path(s)).foldLeft[Endpoint[IO, EmptyTuple]](zero)((acc, e) => acc :: e) e(i).remainder === Some(i.copy(route = Nil)) } } @@ -147,7 +147,7 @@ class EndpointSpec extends FinchSpec { } it should "match the input if one of the endpoints succeed" in { - def matchOneOfTwo(f: String => Endpoint[IO, HNil]): Input => Boolean = { i: Input => + def matchOneOfTwo(f: String => Endpoint[IO, EmptyTuple]): Input => Boolean = { (i: Input) => val v = i.route.headOption.map(f).flatMap(e => e(i).remainder) v.isEmpty || v === Some(i.withRoute(i.route.tail)) } @@ -157,7 +157,7 @@ class EndpointSpec extends FinchSpec { } it should "have the correct string representation" in { - def standaloneMatcher[A]: A => Boolean = { a: A => + def standaloneMatcher[A]: A => Boolean = { (a: A) => path(a.toString).toString == a.toString } @@ -167,8 +167,8 @@ class EndpointSpec extends FinchSpec { def methodMatcher( m: Method, - f: Endpoint[IO, HNil] => Endpoint[IO, HNil] - ): String => Boolean = { s: String => f(s).toString === m.toString.toUpperCase + " /" + s } + f: Endpoint[IO, EmptyTuple] => Endpoint[IO, EmptyTuple] + ): String => Boolean = { (s: String) => f(s).toString === m.toString.toUpperCase + " /" + s } check(methodMatcher(Method.Get, get)) check(methodMatcher(Method.Post, post)) @@ -182,7 +182,7 @@ class EndpointSpec extends FinchSpec { check((s: String, i: Int) => path(s).map(_ => i).toString === s) check((s: String, t: String) => (path(s) :+: path(t)).toString === s"($s :+: $t)") check((s: String, t: String) => (path(s) :: path(t)).toString === s"$s :: $t") - check { s: String => path(s).product[String](pathAny.map(_ => "foo")).toString === s } + check((s: String) => path(s).product[String](pathAny.map(_ => "foo")).toString === s) check((s: String, t: String) => path(s).mapAsync(_ => IO.pure(t)).toString === s) pathEmpty.toString shouldBe "" @@ -204,13 +204,13 @@ class EndpointSpec extends FinchSpec { } it should "always respond with the same output if it's a constant Endpoint" in { - check { s: String => + check { (s: String) => const(s).apply(Input.get("/")).awaitValueUnsafe() === Some(s) && lift(s).apply(Input.get("/")).awaitValueUnsafe() === Some(s) && liftAsync(IO.pure(s)).apply(Input.get("/")).awaitValueUnsafe() === Some(s) } - check { o: Output[String] => + check { (o: Output[String]) => liftOutput(o).apply(Input.get("/")).awaitOutputUnsafe() === Some(o) && liftOutputAsync(IO.pure(o)).apply(Input.get("/")).awaitOutputUnsafe() === Some(o) } diff --git a/core/src/test/scala/io/finch/ExtractPathLaws.scala b/core/src/test/scala/io/finch/ExtractPathLaws.scala index a5b519045..fc0947c16 100644 --- a/core/src/test/scala/io/finch/ExtractPathLaws.scala +++ b/core/src/test/scala/io/finch/ExtractPathLaws.scala @@ -16,7 +16,7 @@ abstract class ExtractPathLaws[F[_]: Effect, A] extends Laws with MissingInstanc def all(implicit A: Arbitrary[Input]): RuleSet = new DefaultRuleSet( name = "all", parent = None, - "extractOne" -> Prop.forAll { input: Input => + "extractOne" -> Prop.forAll { (input: Input) => val i = input.withRoute(input.route.map(s => new QueryStringEncoder(s).toString)) val o = one(i) val v = i.route.headOption.flatMap(s => decode(s)) @@ -24,7 +24,7 @@ abstract class ExtractPathLaws[F[_]: Effect, A] extends Laws with MissingInstanc o.awaitValueUnsafe() == v && (v.isEmpty || o.remainder.contains(i.withRoute(i.route.tail))) }, - "extractTail" -> Prop.forAll { input: Input => + "extractTail" -> Prop.forAll { (input: Input) => val i = input.withRoute(input.route.map(s => new QueryStringEncoder(s).toString)) val o = tail(i) diff --git a/core/src/test/scala/io/finch/FinchSpec.scala b/core/src/test/scala/io/finch/FinchSpec.scala index 6ba7bf3d3..012cfbb63 100644 --- a/core/src/test/scala/io/finch/FinchSpec.scala +++ b/core/src/test/scala/io/finch/FinchSpec.scala @@ -192,7 +192,7 @@ trait FinchSpec extends AnyFlatSpec with Matchers with Checkers with AllInstance ) def genAccept: Gen[Accept] = { - def witness[T <: String](implicit w: Witness.Aux[T]): String = w.value + def witness[T <: String](using w: ValueOf[T]): String = w.value Gen .oneOf( witness[Application.Json], @@ -291,10 +291,8 @@ trait FinchSpec extends AnyFlatSpec with Matchers with Checkers with AllInstance A.arbitrary.map(a => Endpoint[F].const(a)), Arbitrary.arbitrary[Throwable].map(e => Endpoint[F].liftOutputAsync(Effect[F].raiseError[Output[A]](e))), /** - * Note that we don't provide instances of arbitrary endpoints wrapping - * `Input => Output[A]` since `Endpoint` isn't actually lawful in this - * respect. - */ + * Note that we don't provide instances of arbitrary endpoints wrapping `Input => Output[A]` since `Endpoint` isn't actually lawful in this respect. + */ Arbitrary.arbitrary[Input => A].map { f => new Endpoint[F, A] { final def apply(input: Input): Endpoint.Result[F, A] = @@ -305,11 +303,10 @@ trait FinchSpec extends AnyFlatSpec with Matchers with Checkers with AllInstance ) /** - * Equality instance for [[io.finch.Endpoint]]. - * - * We attempt to verify that two endpoints are the same by applying them to a - * fixed number of randomly generated inputs. - */ + * Equality instance for [[io.finch.Endpoint]]. + * + * We attempt to verify that two endpoints are the same by applying them to a fixed number of randomly generated inputs. + */ implicit def eqEndpoint[F[_]: Effect, A: Eq]: Eq[Endpoint[F, A]] = new Eq[Endpoint[F, A]] { private[this] def count: Int = 16 diff --git a/core/src/test/scala/io/finch/InputSpec.scala b/core/src/test/scala/io/finch/InputSpec.scala index fd5a78d03..575752b2b 100644 --- a/core/src/test/scala/io/finch/InputSpec.scala +++ b/core/src/test/scala/io/finch/InputSpec.scala @@ -95,7 +95,7 @@ class InputSpec extends FinchSpec { } it should "parse route correctly" in { - check { i: Input => + check { (i: Input) => i.route === i.request.path.split("/").toList.drop(1) } } diff --git a/core/src/test/scala/io/finch/MethodSpec.scala b/core/src/test/scala/io/finch/MethodSpec.scala index 10d20ec9d..da2d2e44c 100644 --- a/core/src/test/scala/io/finch/MethodSpec.scala +++ b/core/src/test/scala/io/finch/MethodSpec.scala @@ -1,12 +1,9 @@ package io.finch -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.{Future => ScalaFuture} - import cats.Id +import cats.catsInstancesForId import cats.effect.IO import com.twitter.finagle.http.Response -import com.twitter.util.{Future => TwitterFuture} import org.scalacheck.Arbitrary import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -29,57 +26,24 @@ class MethodSpec extends FinchSpec with ScalaCheckDrivenPropertyChecks { checkValue((i: String) => get(zero)(IO.pure(Ok(i)))) } - it should "map TwitterFuture[Output[A]] value to endpoint" in { - - checkValue((i: String) => get(zero)(TwitterFuture.value(Ok(i)))) - } - - it should "map ScalaFuture[Output[A]] value to endpoint" in { - checkValue((i: String) => get(zero)(ScalaFuture.successful(Ok(i)))) - } - it should "map F[Response] value to endpoint" in { checkValue((i: Response) => get(zero)(IO.pure(Ok(i).toResponse[Id, Text.Plain]))) } - it should "map TwitterFuture[Response] value to endpoint" in { - checkValue((i: Response) => get(zero)(TwitterFuture.value(Ok(i).toResponse[Id, Text.Plain]))) - } - - it should "map ScalaFuture[Response] value to endpoint" in { - checkValue((i: Response) => get(zero)(ScalaFuture.successful(Ok(i).toResponse[Id, Text.Plain]))) - } - it should "map A => Output function to endpoint" in { - checkFunction(get(path[Int]) { i: Int => Ok(i) }) + checkFunction(get(path[Int])((i: Int) => Ok(i))) } it should "map A => Response function to endpoint" in { - checkFunction(get(path[Int]) { i: Int => Ok(i).toResponse[Id, Text.Plain] }) + checkFunction(get(path[Int])((i: Int) => Ok(i).toResponse[Id, Text.Plain])) } it should "map A => F[Output[A]] function to endpoint" in { - checkFunction(get(path[Int]) { i: Int => IO.pure(i).map(Ok) }) - } - - it should "map A => TwitterFuture[Output[A]] function to endpoint" in { - checkFunction(get(path[Int]) { i: Int => TwitterFuture.value(i).map(Ok) }) - } - - it should "map A => ScalaFuture[Output[A]] function to endpoint" in { - checkFunction(get(path[Int]) { i: Int => ScalaFuture.successful(i).map(Ok) }) + checkFunction(get(path[Int])((i: Int) => IO.pure(i).map(Ok))) } it should "map A => F[Response] function to endpoint" in { - checkFunction(get(path[Int]) { i: Int => IO.pure(i).map(Ok(_).toResponse[Id, Text.Plain]) }) - } - - it should "map A => TwitterFuture[Response] function to endpoint" in { - checkFunction(get(path[Int]) { i: Int => TwitterFuture.value(i).map(Ok(_).toResponse[Id, Text.Plain]) }) - } - - it should "map A => ScalaFuture[Response] function to endpoint" in { - checkFunction(get(path[Int]) { i: Int => ScalaFuture.successful(i).map(Ok(_).toResponse[Id, Text.Plain]) }) + checkFunction(get(path[Int])((i: Int) => IO.pure(i).map(Ok(_).toResponse[Id, Text.Plain]))) } it should "map (A, B) => Output function to endpoint" in { @@ -94,75 +58,19 @@ class MethodSpec extends FinchSpec with ScalaCheckDrivenPropertyChecks { checkFunction2(get(path[Int] :: path[Int])((x: Int, y: Int) => IO.pure(Ok(s"$x$y")))) } - it should "map (A, B) => TwitterFuture[Output[String]] function to endpoint" in { - checkFunction2(get(path[Int] :: path[Int])((x: Int, y: Int) => TwitterFuture.value(Ok(s"$x$y")))) - } - - it should "map (A, B) => ScalaFuture[Output[String]] function to endpoint" in { - checkFunction2(get(path[Int] :: path[Int])((x: Int, y: Int) => ScalaFuture.successful(Ok(s"$x$y")))) - } - it should "map (A, B) => F[Response] function to endpoint" in { checkFunction2(get(path[Int] :: path[Int]) { (x: Int, y: Int) => IO.pure(Ok(s"$x$y").toResponse[Id, Text.Plain]) }) } - it should "map (A, B) => TwitterFuture[Response] function to endpoint" in { - checkFunction2(get(path[Int] :: path[Int]) { (x: Int, y: Int) => - TwitterFuture.value(Ok(s"$x$y").toResponse[Id, Text.Plain]) - }) - } - - it should "map (A, B) => ScalaFuture[Response] function to endpoint" in { - checkFunction2(get(path[Int] :: path[Int]) { (x: Int, y: Int) => - ScalaFuture.successful(Ok(s"$x$y").toResponse[Id, Text.Plain]) - }) - } - - behavior of "Custom Type Program[_]" - - case class Program[A](value: A) - - implicit val conv = new ToAsync[Program, IO] { - def apply[A](a: Program[A]): IO[A] = IO(a.value) - } - - it should "map Program[Output[_]] value to endpoint" in { - checkValue((i: String) => get(zero)(Program(Ok(i)))) - } - - it should "map A => Program[Output[_]] function to endpoint" in { - checkFunction(get(path[Int]) { i: Int => Program(Ok(i)) }) - } - - it should "map (A, B) => Program[Output[_]] function to endpoint" in { - checkFunction2(get(path[Int] :: path[Int]) { (x: Int, y: Int) => - Program(Ok(s"$x$y")) - }) - } - - it should "map Program[Response] value to endpoint" in { - checkValue((i: Response) => get(zero)(Program(i))) - } - - it should "map A => Program[Response] function to endpoint" in { - checkFunction(get(path[Int]) { i: Int => Program(Ok(i).toResponse[Id, Text.Plain]) }) - } - - it should "map (A, B) => Program[Response] function to endpoint" in { - checkFunction2(get(path[Int] :: path[Int]) { (x: Int, y: Int) => - Program(Ok(s"$x$y").toResponse[Id, Text.Plain]) - }) - } - private def checkValue[A: Arbitrary](f: A => Endpoint[IO, A]): Unit = forAll { (input: A) => val e = f(input) e(Input.get("/")).awaitValueUnsafe() shouldBe Some(input) } - private def checkFunction(e: Endpoint[IO, _]): Unit = + private def checkFunction(e: Endpoint[IO, ?]): Unit = forAll { (input: Int) => e(Input.get(s"/$input")).awaitValueUnsafe() match { case Some(r: Response) => r.contentString shouldBe input.toString @@ -171,7 +79,7 @@ class MethodSpec extends FinchSpec with ScalaCheckDrivenPropertyChecks { } } - private def checkFunction2(e: Endpoint[IO, _]): Unit = + private def checkFunction2(e: Endpoint[IO, ?]): Unit = forAll { (x: Int, y: Int) => e(Input.get(s"/$x/$y")).awaitValueUnsafe() match { case Some(r: Response) => r.contentString shouldBe s"$x$y" diff --git a/core/src/test/scala/io/finch/MissingInstances.scala b/core/src/test/scala/io/finch/MissingInstances.scala index a3cceaab5..ba5c2f29f 100644 --- a/core/src/test/scala/io/finch/MissingInstances.scala +++ b/core/src/test/scala/io/finch/MissingInstances.scala @@ -7,8 +7,8 @@ import cats.effect.{ContextShift, IO} import com.twitter.io.Buf /** - * Type class instances for non-Finch types. - */ + * Type class instances for non-Finch types. + */ trait MissingInstances { implicit def eqEither[A](implicit A: Eq[A]): Eq[Either[Throwable, A]] = Eq.instance { case (Right(a), Right(b)) => A.eqv(a, b) diff --git a/core/src/test/scala/io/finch/MultipartSpec.scala b/core/src/test/scala/io/finch/MultipartSpec.scala index 04ef3d897..79b5c75ad 100644 --- a/core/src/test/scala/io/finch/MultipartSpec.scala +++ b/core/src/test/scala/io/finch/MultipartSpec.scala @@ -41,7 +41,7 @@ class MultipartSpec extends FinchSpec { ) it should "file upload (single)" in { - check { b: Buf => + check { (b: Buf) => val i = withFileUpload("foo", b) val fu = multipartFileUpload("foo").apply(i).awaitValueUnsafe() val fuo = multipartFileUploadOption("foo").apply(i).awaitValueUnsafe().flatten diff --git a/core/src/test/scala/io/finch/OutputSpec.scala b/core/src/test/scala/io/finch/OutputSpec.scala index 2888c7ed1..ea9aaada7 100644 --- a/core/src/test/scala/io/finch/OutputSpec.scala +++ b/core/src/test/scala/io/finch/OutputSpec.scala @@ -5,6 +5,7 @@ import java.nio.charset.{Charset, StandardCharsets} import scala.util.{Failure, Success, Try} import cats.Id +import cats.catsInstancesForId import com.twitter.finagle.http.Status import com.twitter.io.Buf @@ -13,7 +14,7 @@ class OutputSpec extends FinchSpec { behavior of "Output" it should "propagate status to response" in { - check { o: Output[String] => o.toResponse[Id, Text.Plain].status == o.status } + check((o: Output[String]) => o.toResponse[Id, Text.Plain].status == o.status) } it should "propagate overridden status to response" in { @@ -75,33 +76,33 @@ class OutputSpec extends FinchSpec { } it should "propagate cause to response" in { - check { of: Output.Failure => + check { (of: Output.Failure) => (of: Output[Unit]).toResponse[Id, Text.Plain].content === Encode[Exception, Text.Plain].apply(of.cause, of.charset.getOrElse(StandardCharsets.UTF_8)) } } it should "propagate empytiness to response" in { - check { of: Output.Empty => + check { (of: Output.Empty) => (of: Output[Unit]).toResponse[Id, Text.Plain].content === Buf.Empty } } it should "propagate payload to response" in { - check { op: Output.Payload[String] => + check { (op: Output.Payload[String]) => op.toResponse[Id, Text.Plain].content === Encode[String, Text.Plain].apply(op.value, op.charset.getOrElse(StandardCharsets.UTF_8)) } } it should "create an empty endpoint with given status when calling unit" in { - check { s: Status => + check { (s: Status) => Output.unit(s).toResponse[Id, Text.Plain].status === s } } it should "throw an exception on calling value on an Empty output" in { - check { e: Output.Empty => + check { (e: Output.Empty) => Try(e.value) match { case Failure(f) => f.getMessage === "empty output" case _ => false @@ -110,7 +111,7 @@ class OutputSpec extends FinchSpec { } it should "throw an exception on calling value on a Failure output" in { - check { f: Output.Failure => + check { (f: Output.Failure) => Try(f.value) match { case Failure(ex) => ex.getMessage === f.cause.getMessage case _ => false @@ -137,7 +138,7 @@ class OutputSpec extends FinchSpec { } it should "traverse arbitrary outputs" in { - check { oa: Output[String] => + check { (oa: Output[String]) => oa.traverse[Try, String](_ => Success(oa.value)) === Success(oa) } } diff --git a/core/src/test/scala/io/finch/TraceSpec.scala b/core/src/test/scala/io/finch/TraceSpec.scala index 31fc5f95f..e66972dcc 100644 --- a/core/src/test/scala/io/finch/TraceSpec.scala +++ b/core/src/test/scala/io/finch/TraceSpec.scala @@ -5,7 +5,7 @@ class TraceSpec extends FinchSpec { behavior of "Trace" it should "round-trip concat/toList" in { - check { l: List[String] => + check { (l: List[String]) => val trace = l.foldLeft(Trace.empty)((t, s) => t.concat(Trace.segment(s))) trace.toList === l } @@ -18,7 +18,7 @@ class TraceSpec extends FinchSpec { } it should "create fromRoute" in { - check { l: List[String] => + check { (l: List[String]) => Trace.fromRoute(l).toList === l } } diff --git a/core/src/test/scala/io/finch/internal/HttpContentSpec.scala b/core/src/test/scala/io/finch/internal/HttpContentSpec.scala index 0d62ccb87..3f53f809a 100644 --- a/core/src/test/scala/io/finch/internal/HttpContentSpec.scala +++ b/core/src/test/scala/io/finch/internal/HttpContentSpec.scala @@ -10,20 +10,20 @@ class HttpContentSpec extends FinchSpec { behavior of "HttpContet" it should "asByteArrayWithBeginAndEnd" in { - check { b: Buf => + check { (b: Buf) => val (array, begin, end) = b.asByteArrayWithBeginAndEnd Buf.ByteArray.Owned.extract(b) === array.slice(begin, end) } } it should "asByteBuffer" in { - check { b: Buf => + check { (b: Buf) => b.asByteBuffer === Buf.ByteBuffer.Owned.extract(b) } } it should "asByteArray" in { - check { b: Buf => + check { (b: Buf) => b.asByteArray === Buf.ByteArray.Owned.extract(b) } } diff --git a/core/src/test/scala/io/finch/internal/HttpMessageSpec.scala b/core/src/test/scala/io/finch/internal/HttpMessageSpec.scala index 504282410..4b2b5e20a 100644 --- a/core/src/test/scala/io/finch/internal/HttpMessageSpec.scala +++ b/core/src/test/scala/io/finch/internal/HttpMessageSpec.scala @@ -15,7 +15,7 @@ class HttpMessageSpec extends FinchSpec { behavior of "HttpMessage" it should "charsetOrUtf8" in { - check { cs: Charset => + check { (cs: Charset) => val req = Request() req.contentType = "application/json" req.charset = cs.displayName() @@ -23,7 +23,7 @@ class HttpMessageSpec extends FinchSpec { req.charsetOrUtf8 === slowCharset(req) } - check { cs: Charset => + check { (cs: Charset) => val req = Request() req.contentType = "application/json; charset=" + cs.displayName() @@ -34,7 +34,7 @@ class HttpMessageSpec extends FinchSpec { } it should "mediaTypeOrEmpty" in { - check { cs: Option[Charset] => + check { (cs: Option[Charset]) => val req = Request() req.contentType = "application/json" cs.foreach(c => req.charset = c.displayName()) diff --git a/core/src/test/scala/io/finch/internal/ToEffectLaws.scala b/core/src/test/scala/io/finch/internal/ToEffectLaws.scala deleted file mode 100644 index 13a59bd09..000000000 --- a/core/src/test/scala/io/finch/internal/ToEffectLaws.scala +++ /dev/null @@ -1,42 +0,0 @@ -package io.finch.internal - -import cats.instances.AllInstances -import cats.laws._ -import cats.laws.discipline._ -import cats.{Applicative, Eq} -import io.finch.MissingInstances -import org.scalacheck.{Arbitrary, Prop} -import org.typelevel.discipline.Laws - -abstract trait ToEffectLaws[F[_], G[_], A] extends Laws with MissingInstances with AllInstances { - - def F: Applicative[F] - def G: Applicative[G] - def extract: G[A] => A - - def T: ToAsync[F, G] - - def nTransformation(f: => F[A], g: G[A], fn: A => A): IsEq[A] = - extract(G.map(T(f))(fn)) <-> extract(T(F.map(f)(fn))) - - def all(implicit A: Arbitrary[A], E: Eq[A]): RuleSet = new DefaultRuleSet( - name = "all", - parent = None, - "natural transformation" -> Prop.forAll { a: A => nTransformation(F.pure(a), G.pure(a), identity) } - ) - -} - -object ToEffectLaws { - - def apply[F[_]: Applicative, G[_]: Applicative, A](e: G[A] => A)(implicit - t: ToAsync[F, G] - ): ToEffectLaws[F, G, A] = - new ToEffectLaws[F, G, A] { - val F: Applicative[F] = implicitly[Applicative[F]] - val G: Applicative[G] = implicitly[Applicative[G]] - val T: ToAsync[F, G] = t - val extract: G[A] => A = e - } - -} diff --git a/core/src/test/scala/io/finch/internal/ToEffectSpec.scala b/core/src/test/scala/io/finch/internal/ToEffectSpec.scala deleted file mode 100644 index 9210e42fa..000000000 --- a/core/src/test/scala/io/finch/internal/ToEffectSpec.scala +++ /dev/null @@ -1,23 +0,0 @@ -package io.finch.internal - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.{Future => ScalaFuture} - -import cats.Applicative -import cats.effect.IO -import com.twitter.util.{Future => TwitterFuture} -import io.finch.FinchSpec - -class ToEffectSpec extends FinchSpec { - - implicit val twitterFutureApplicative: Applicative[TwitterFuture] = new Applicative[TwitterFuture] { - def pure[A](x: A): TwitterFuture[A] = TwitterFuture.value(x) - def ap[A, B](ff: TwitterFuture[A => B])(fa: TwitterFuture[A]): TwitterFuture[B] = - fa.flatMap(a => ff.map(_.apply(a))) - } - - checkAll("ToEffect[TwitterFuture, IO]", ToEffectLaws.apply[TwitterFuture, IO, Int](_.unsafeRunSync()).all) - checkAll("ToEffect[ScalaFuture, IO]", ToEffectLaws[ScalaFuture, IO, Int](_.unsafeRunSync()).all) - checkAll("ToEffect[IO, IO]", ToEffectLaws[IO, IO, Int](_.unsafeRunSync()).all) - -} diff --git a/core/src/test/scala/io/finch/internal/TooFastStringSpec.scala b/core/src/test/scala/io/finch/internal/TooFastStringSpec.scala index ee5a248b4..f9f6480b2 100644 --- a/core/src/test/scala/io/finch/internal/TooFastStringSpec.scala +++ b/core/src/test/scala/io/finch/internal/TooFastStringSpec.scala @@ -10,7 +10,7 @@ import org.scalatestplus.scalacheck.Checkers class TooFastStringSpec extends AnyFlatSpec with Matchers with Checkers { "TooFastString" should "parse boolean correctly" in { - check { b: Boolean => + check { (b: Boolean) => b.toString.tooBoolean === Some(b) } @@ -19,7 +19,7 @@ class TooFastStringSpec extends AnyFlatSpec with Matchers with Checkers { } it should "parse int correctly" in { - check { i: Int => + check { (i: Int) => i.toString.tooInt === Some(i) } @@ -36,7 +36,7 @@ class TooFastStringSpec extends AnyFlatSpec with Matchers with Checkers { } it should "parse long correctly" in { - check { l: Long => + check { (l: Long) => l.toString.tooLong === Some(l) } diff --git a/project/build.properties b/project/build.properties index 0837f7a13..10fd9eee0 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.3.13 +sbt.version=1.5.5 diff --git a/project/plugins.sbt b/project/plugins.sbt index fa300ed2f..9e3cd4fcc 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -14,5 +14,5 @@ addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.4") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.0") addSbtPlugin("com.47deg" % "sbt-microsites" % "1.2.1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2") -addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.4.10") +//addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.4.10") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.21") diff --git a/test/src/main/scala/io/finch/test/ServiceIntegrationSuite.scala b/test/src/main/scala/io/finch/test/ServiceIntegrationSuite.scala index 10869b3e3..134e03fdd 100644 --- a/test/src/main/scala/io/finch/test/ServiceIntegrationSuite.scala +++ b/test/src/main/scala/io/finch/test/ServiceIntegrationSuite.scala @@ -27,11 +27,11 @@ trait ServiceIntegrationSuite extends ServiceSuite { self: FixtureTestSuite => try self.withFixture(test.toNoArgTest(FixtureParam(client))) finally Await.ready( - for { + for _ <- server.close() _ <- client.close() _ <- service.close() - } yield () + yield () ) } }