Skip to content

Commit

Permalink
URL string interpolation macro (zio#2331)
Browse files Browse the repository at this point in the history
  • Loading branch information
vigoo authored and 987Nabil committed Jul 28, 2023
1 parent 6c15296 commit 56f9ee7
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 28 deletions.
56 changes: 33 additions & 23 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ ThisBuild / githubWorkflowAddedJobs :=
steps = List(WorkflowStep.Use(UseRef.Public("release-drafter", "release-drafter", s"v${releaseDrafterVersion}"))),
cond = Option("${{ github.base_ref == 'main' }}"),
),
) ++ ScoverageWorkFlow(50, 60) ++ JmhBenchmarkWorkflow(1) // ++ BenchmarkWorkFlow()
) ++ ScoverageWorkFlow(50, 60) ++ JmhBenchmarkWorkflow(1) // ++ BenchmarkWorkFlow()

ThisBuild / githubWorkflowTargetTags ++= Seq("v*")
ThisBuild / githubWorkflowPublishTargetBranches += RefPredicate.StartsWith(Ref.Tag("v"))
Expand All @@ -50,11 +50,11 @@ ThisBuild / githubWorkflowPublish :=
name = Some("Release Shaded"),
env = Map(
Shading.env.PUBLISH_SHADED -> "true",
"PGP_PASSPHRASE" -> "${{ secrets.PGP_PASSPHRASE }}",
"PGP_SECRET" -> "${{ secrets.PGP_SECRET }}",
"SONATYPE_PASSWORD" -> "${{ secrets.SONATYPE_PASSWORD }}",
"SONATYPE_USERNAME" -> "${{ secrets.SONATYPE_USERNAME }}",
"CI_SONATYPE_RELEASE" -> "${{ secrets.CI_SONATYPE_RELEASE }}",
"PGP_PASSPHRASE" -> "${{ secrets.PGP_PASSPHRASE }}",
"PGP_SECRET" -> "${{ secrets.PGP_SECRET }}",
"SONATYPE_PASSWORD" -> "${{ secrets.SONATYPE_PASSWORD }}",
"SONATYPE_USERNAME" -> "${{ secrets.SONATYPE_USERNAME }}",
"CI_SONATYPE_RELEASE" -> "${{ secrets.CI_SONATYPE_RELEASE }}",
),
),
)
Expand Down Expand Up @@ -89,8 +89,9 @@ ThisBuild / githubWorkflowBuildPostamble :=
name = Some("zio-http-shaded Tests"),
commands = List("zioHttpShadedTests/test"),
cond = Some(s"matrix.scala == '$Scala213'"),
env = Map(Shading.env.PUBLISH_SHADED -> "true")
))
env = Map(Shading.env.PUBLISH_SHADED -> "true"),
),
),
).steps

inThisBuild(
Expand All @@ -116,7 +117,7 @@ lazy val root = (project in file("."))
)

lazy val zioHttp = (project in file("zio-http"))
.enablePlugins(Shading.plugins() : _*)
.enablePlugins(Shading.plugins(): _*)
.settings(stdSettings("zio-http"))
.settings(publishSetting(true))
.settings(settingsWithHeaderLicense)
Expand All @@ -135,44 +136,53 @@ lazy val zioHttp = (project in file("zio-http"))
),
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, n)) if n <= 12 => Seq(`scala-compact-collection`)
case Some((2, n)) if n <= 12 =>
Seq(`scala-compact-collection`)
case _ => Seq.empty
}
},
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, _)) =>
Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value)
case _ => Seq.empty
}
},
)

/**
* Special subproject to sanity test the shaded version of zio-http.
* Run using `sbt -Dpublish.shaded zioHttpShadedTests/test`.
* This will trigger `publishLocal` on zio-http and then run tests using the shaded artifact as a dependency, instead of zio-http classes.
* Special subproject to sanity test the shaded version of zio-http. Run using
* `sbt -Dpublish.shaded zioHttpShadedTests/test`. This will trigger
* `publishLocal` on zio-http and then run tests using the shaded artifact as a
* dependency, instead of zio-http classes.
*/
lazy val zioHttpShadedTests = if(Shading.shadingEnabled) {
lazy val zioHttpShadedTests = if (Shading.shadingEnabled) {
(project in file("zio-http-shaded-tests"))
.settings(stdSettings("zio-http-shaded-tests"))
.settings(
Compile / sources := Nil,
Test / sources := (
Compile / sources := Nil,
Test / sources := (
baseDirectory.value / ".." / "zio-http" / "src" / "test" / "scala" ** "*.scala" ---
// Exclude tests of netty specific internal stuff
baseDirectory.value / ".." / "zio-http" / "src" / "test" / "scala" ** "netty" ** "*.scala"
).get,
Test / scalaSource := (baseDirectory.value / ".." / "zio-http" / "src" / "test" / "scala"),
).get,
Test / scalaSource := (baseDirectory.value / ".." / "zio-http" / "src" / "test" / "scala"),
Test / resourceDirectory := (baseDirectory.value / ".." / "zio-http" / "src" / "test" / "resources"),
testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"),
libraryDependencies ++= Seq(
`zio-test-sbt`,
`zio-test`,
"dev.zio" %% "zio-http-shaded" % version.value,
)
),
)
.settings(publishSetting(false))
.settings(Test / test := (Test / test).dependsOn(zioHttp / publishLocal).value)
} else {
(project in file(".")).settings(
Compile / sources := Nil,
Test / sources := Nil,
name := "noop",
publish / skip := true,
Test / sources := Nil,
name := "noop",
publish / skip := true,
)
}

Expand Down Expand Up @@ -203,7 +213,7 @@ lazy val zioHttpExample = (project in file("zio-http-example"))
.dependsOn(zioHttp, zioHttpCli)

lazy val zioHttpTestkit = (project in file("zio-http-testkit"))
.enablePlugins(Shading.plugins() : _*)
.enablePlugins(Shading.plugins(): _*)
.settings(stdSettings("zio-http-testkit"))
.settings(publishSetting(true))
.settings(Shading.shadingSettings())
Expand Down
2 changes: 0 additions & 2 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,4 @@ object Dependencies {
val `zio-test` = "dev.zio" %% "zio-test" % ZioVersion % "test"
val `zio-test-sbt` = "dev.zio" %% "zio-test-sbt" % ZioVersion % "test"

val reflect = Def.map(scalaVersion)("org.scala-lang" % "scala-reflect" % _)

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

package zio.http

import scala.language.experimental.macros
import scala.reflect.macros.blackbox

trait UrlInterpolator {

implicit class UrlInterpolatorHelper(val sc: StringContext) {
def url(args: Any*): URL = macro UrlInterpolatorMacro.url
}
}

private[http] object UrlInterpolatorMacro {
def url(c: blackbox.Context)(args: c.Expr[Any]*): c.Expr[URL] = {
import c.universe._
c.prefix.tree match {
case Apply(_, List(Apply(_, Literal(Constant(p: String)) :: Nil))) =>
val result = URL.decode(p) match {
case Left(error) => c.abort(c.enclosingPosition, s"Invalid URL: ${error.getMessage}")
case Right(url) =>
if (url.isAbsolute) {
val uri = url.encode
q"_root_.zio.http.URL.fromAbsoluteURI(new _root_.java.net.URI($uri)).get"
} else {
val uri = url.encode
q"_root_.zio.http.URL.fromRelativeURI(new _root_.java.net.URI($uri)).get"
}
}
c.Expr[URL](result)
case Apply(_, List(Apply(_, staticPartLiterals))) =>
val staticParts = staticPartLiterals.map { case Literal(Constant(p: String)) => p }
val injectedPartExamples =
args.map { arg =>
val typ = arg.actualType
if (typ =:= c.typeOf[String]) {
"string"
} else if (typ =:= c.typeOf[Byte]) {
"123"
} else if (typ =:= c.typeOf[Short]) {
"1234"
} else if (typ =:= c.typeOf[Int]) {
"1234"
} else if (typ =:= c.typeOf[Long]) {
"1234"
} else if (typ =:= c.typeOf[Boolean]) {
"true"
} else if (typ =:= c.typeOf[Float]) {
"1.23"
} else if (typ =:= c.typeOf[Double]) {
"1.23"
} else if (typ =:= c.typeOf[java.util.UUID]) {
"123e4567-e89b-12d3-a456-426614174000"
} else {
c.abort(c.enclosingPosition, s"Unsupported type in url interpolator: $typ")
}
}
val exampleParts = staticParts.zipAll(injectedPartExamples, "", "").flatMap { case (a, b) => List(a, b) }
val example = exampleParts.mkString
URL.decode(example) match {
case Left(error) =>
c.abort(c.enclosingPosition, s"Invalid URL: ${error.getMessage}")
case Right(url) =>
val parts =
staticParts.map { s => Literal(Constant(s)) }
.zipAll(args.map(_.tree), Literal(Constant("")), Literal(Constant("")))
.flatMap { case (a, b) => List(a, b) }

val concatenated =
parts.foldLeft[Tree](q"""""""") { case (acc, part) =>
q"$acc + $part"
}

val result = if (url.isAbsolute) {
q"_root_.zio.http.URL.fromAbsoluteURI(new _root_.java.net.URI($concatenated)).get"
} else {
q"_root_.zio.http.URL.fromRelativeURI(new _root_.java.net.URI($concatenated)).get"
}

c.Expr[URL](result)
}
}
}
}
114 changes: 114 additions & 0 deletions zio-http/src/main/scala-3/zio/http/UrlInterpolator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2021 - 2023 Sporta Technologies PVT LTD & the ZIO HTTP contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package zio.http

import scala.quoted.*

trait UrlInterpolator {

extension(inline sc: StringContext) {
inline def url(inline args: Any*): URL = ${ UrlInterpolatorMacro.url('sc, 'args) }
}

}

private[http] object UrlInterpolatorMacro {

def url(sc: Expr[StringContext], args: Expr[Seq[Any]])(using Quotes): Expr[URL] = {
import quotes.reflect.*
import report.*

val ctx = sc.valueOrAbort
val staticParts = ctx.parts

val argExprs = args match {
case Varargs(exprs) => exprs
case _ => errorAndAbort(s"Unexpected arguments", args)
}

val result = if (argExprs.isEmpty) {
URL.decode(staticParts.mkString) match {
case Left(error) => errorAndAbort(s"Invalid URL: $error", sc)
case Right(url) =>
val uri = Expr(url.encode)
if (url.isAbsolute) {
'{ URL.fromAbsoluteURI(new java.net.URI($uri)).get }
} else {
'{ URL.fromRelativeURI(new java.net.URI($uri)).get }
}
}
} else {
val injectedPartExamples =
argExprs.map { arg =>
val typ = arg.asTerm.tpe.asType
typ match {
case '[String] =>
"string"
case '[Byte] =>
"123"
case '[Short] =>
"1234"
case '[Int] =>
"1234"
case '[Long] =>
"1234"
case '[Boolean] =>
"true"
case '[Float] =>
"1.23"
case '[Double] =>
"1.23"
case '[java.util.UUID] =>
"123e4567-e89b-12d3-a456-426614174000"
case _ =>
errorAndAbort(s"Injected field ${arg.show} has an unsupported type", arg)
}
}

val exampleParts = staticParts.zipAll(injectedPartExamples, "", "").flatMap { case (a, b) => List(a, b) }
val example = exampleParts.mkString

URL.decode(example) match {
case Left(error) =>
errorAndAbort(s"Invalid URL: $error", sc)
case Right(url) =>
val parts =
staticParts.map { s => Expr(s) }
.zipAll(argExprs, Expr(""), Expr(""))
.flatMap { case (a, b) => List(a, b) }

val concatenated =
parts.foldLeft[Expr[String]](Expr("")) { case (acc, part) =>
'{$acc + $part}
}

if (url.isAbsolute) {
'{
URL.fromAbsoluteURI(new java.net.URI($concatenated)).get
}
} else {
'{
URL.fromRelativeURI(new java.net.URI($concatenated)).get
}
}
}
}

result
}

}
4 changes: 2 additions & 2 deletions zio-http/src/main/scala/zio/http/URL.scala
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ object URL {
}
}

private def fromAbsoluteURI(uri: URI): Option[URL] = {
private[http] def fromAbsoluteURI(uri: URI): Option[URL] = {
for {
scheme <- Scheme.decode(uri.getScheme)
host <- Option(uri.getHost)
Expand All @@ -284,7 +284,7 @@ object URL {
} yield URL(path3, connection, QueryParams.decode(uri.getRawQuery), Fragment.fromURI(uri))
}

private def fromRelativeURI(uri: URI): Option[URL] = for {
private[http] def fromRelativeURI(uri: URI): Option[URL] = for {
path <- Option(uri.getRawPath)
} yield URL(Path.decode(path), Location.Relative, QueryParams.decode(uri.getRawQuery), Fragment.fromURI(uri))

Expand Down
2 changes: 1 addition & 1 deletion zio-http/src/main/scala/zio/http/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import java.util.UUID

import zio.http.codec.PathCodec

package object http {
package object http extends UrlInterpolator {

/**
* A smart constructor that attempts to construct a handler from the specified
Expand Down
Loading

0 comments on commit 56f9ee7

Please sign in to comment.