Skip to content

Commit

Permalink
Merge pull request #217 from Dwolla/consul-discoverable-typeclass
Browse files Browse the repository at this point in the history
add ConsulDiscoverable typeclass and Scala 2 macro implementation
  • Loading branch information
bpholt authored Oct 30, 2024
2 parents 140f6c8 + 214e0ec commit c3d4f59
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 5 deletions.
9 changes: 9 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ lazy val `smithy4s-consul-middleware` = crossProject(JSPlatform, JVMPlatform)
libraryDependencies ++= Seq(
"org.http4s" %%% "http4s-client" % http4sVersion,
"com.disneystreaming.smithy4s" %%% "smithy4s-core" % smithy4sVersion.value,
"org.typelevel" %% "scalac-compat-annotation" % "0.1.4",
),
)
.jsSettings(
Expand All @@ -142,6 +143,14 @@ lazy val `smithy4s-consul-middleware-tests` = crossProject(JSPlatform, JVMPlatfo
"com.comcast" %%% "ip4s-test-kit" % "3.6.0" % Test,
)
},
libraryDependencies ++= {
(scalaBinaryVersion.value) match {
case "2.12" | "2.13" =>
Seq("org.scala-lang" % "scala-compiler" % scalaVersion.value % Test)
case _ =>
Nil
}
},
Compile / smithy4sInputDirs := List(
baseDirectory.value.getParentFile / "src" / "main" / "smithy",
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package some_other_package

import com.dwolla.consul.smithy4s._
import com.dwolla.test.HelloService
import munit.FunSuite
import org.http4s.Uri
import org.http4s.Uri.Host

trait ConsulDiscoverableSpecPerScalaVersion { self: FunSuite =>
val serviceUri: Uri = UriFromService(HelloService)
private val serviceHost: Host = HostFromService(HelloService)
private val serviceUriAuthority: Uri.Authority = UriAuthorityFromService(HelloService)

test("ConsulDiscoverable typeclass macro constructs a working instance of the typeclass") {
assertEquals(ConsulDiscoverable[HelloService].host, serviceHost)
assertEquals(ConsulDiscoverable[HelloService].uriAuthority, serviceUriAuthority)
assertEquals(ConsulDiscoverable[HelloService].uri, serviceUri)
}

test("ConsulDiscoverable typeclass macro returns no instance when the type parameter isn't a Smithy Service") {
assertEquals(
compileErrors("""ConsulDiscoverable[NotASmithy4sService]"""),
"""error: Instances are only available for Smithy4s Services annotated with @discoverable
|ConsulDiscoverable[NotASmithy4sService]
| ^""".stripMargin
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,3 @@ class UriFromServiceSpec extends FunSuite {
}
}
}

trait NotASmithy4sService[F[_]]
object NotASmithy4sService
trait NotASmithy4sServiceAndHasNoCompanionObject[F[_]]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package some_other_package

import munit._

// TODO implement tests with macro-derived implementation once Scala 3 macro is available
trait ConsulDiscoverableSpecPerScalaVersion { self: FunSuite =>
test("ConsulDiscoverable typeclass macro returns no instance when the type parameter isn't a Smithy Service") {
assertEquals(
compileErrors("import com.dwolla.consul.smithy4s._\nConsulDiscoverable[NotASmithy4sService]"),
"""error: Instances are only available for Smithy4s Services annotated with @discoverable
|ConsulDiscoverable[NotASmithy4sService]
| ^""".stripMargin
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package some_other_package

import munit._

class ConsulDiscoverableSpec
extends FunSuite
with ConsulDiscoverableSpecPerScalaVersion
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.dwolla.consul.smithy4s

trait NotASmithy4sService[F[_]]
object NotASmithy4sService

trait NotASmithy4sServiceAndHasNoCompanionObject[F[_]]
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.dwolla.consul.smithy4s

import cats.syntax.all._
import com.dwolla.consul.smithy._
import org.http4s.Uri.{Host, Scheme}
import org.typelevel.scalaccompat.annotation.nowarn212
import smithy4s.Hints

import scala.reflect.macros.whitebox
import scala.util.Try

trait ConsulDiscoverablePlatform {
implicit def derivedInstance[Alg[_[_]]]: ConsulDiscoverable[Alg] =
macro ConsulDiscoverableMacros.makeInstance[Alg]
}

object ConsulDiscoverableMacros {
@nowarn212("msg=local val (liftableScheme|liftableHost) in method makeInstance is never used")
def makeInstance[Alg[_[_]]](c: whitebox.Context): c.Expr[ConsulDiscoverable[Alg]] = {
import c.universe.{Try => _, _}

implicit val liftableScheme: Liftable[Scheme] = Liftable { scheme: Scheme =>
q"""_root_.org.http4s.Uri.Scheme.unsafeFromString(${scheme.value})"""
}
implicit val liftableHost: Liftable[Host] = Liftable { host: Host =>
q"""_root_.org.http4s.Uri.Host.unsafeFromString(${host.value})"""
}

val consulScheme = DiscoveryMacros.consulScheme.some

def findHintsInTree(tpe: Tree): Either[String, Hints] =
Try {
tpe.collect {
case x: TypTree =>
c.eval(c.Expr[Hints](q"${x.symbol.companion}.hints"))
}
.headOption
.toRight(s"could not find hints for $tpe")
}
.toEither
.leftMap(_.toString ++ s" (is $tpe a Smithy4s Service?)")
.flatten

def getDiscoverableFromHints(tpe: Tree): Hints => Either[String, Discoverable] =
_.get(Discoverable.tagInstance)
.toRight(s"could not find Discoverable hint for $tpe")

val getHostFromDiscoverable: PartialFunction[Discoverable, Either[String, Host]] = {
case Discoverable(ServiceName(serviceName)) =>
Host.fromString(serviceName)
.leftMap(_.message)
}

def hostToConsulDiscoverableExpr(tpe: Tree): Host => c.Expr[ConsulDiscoverable[Alg]] = host =>
c.Expr[ConsulDiscoverable[Alg]](
q"""
new _root_.com.dwolla.consul.smithy4s.ConsulDiscoverable[$tpe] {
override def host: _root_.org.http4s.Uri.Host = $host
override def uriAuthority: _root_.org.http4s.Uri.Authority = _root_.org.http4s.Uri.Authority(host = host)
override def uri: _root_.org.http4s.Uri = _root_.org.http4s.Uri(scheme = $consulScheme, authority = _root_.scala.Option(uriAuthority))
}
""")

c.macroApplication match {
case TypeApply(_, List(tpe)) if tpe.symbol.companion != NoSymbol =>
val maybeExpr = findHintsInTree(tpe)
.flatMap(getDiscoverableFromHints(tpe))
.flatMap(getHostFromDiscoverable)
.map(hostToConsulDiscoverableExpr(tpe))

maybeExpr.fold(c.abort(c.enclosingPosition, _), identity)
case TypeApply(_, List(tpe)) if tpe.symbol.companion == NoSymbol =>
c.abort(c.enclosingPosition, s"$tpe is not a Smithy4s Service")
case other => c.abort(c.enclosingPosition, s"found $other, which is not a Smithy4s Service")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import org.http4s.syntax.all._
import scala.reflect.macros.blackbox

object DiscoveryMacros {
private val consulScheme: Scheme = scheme"consul"
private[smithy4s] val consulScheme: Scheme = scheme"consul"

def makeUri[Alg[_[_, _, _, _, _]]](c: blackbox.Context)
(service: c.Expr[smithy4s.Service[Alg]]): c.Expr[Uri] = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.dwolla.consul.smithy4s

trait ConsulDiscoverablePlatform
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.dwolla.consul.smithy4s

import org.http4s.Uri
import org.http4s.Uri.Host

import scala.annotation.implicitNotFound

@implicitNotFound("Instances are only available for Smithy4s Services annotated with @discoverable")
trait ConsulDiscoverable[Alg[_[_]]] {
def host: Host
def uriAuthority: Uri.Authority
def uri: Uri
}

object ConsulDiscoverable extends ConsulDiscoverablePlatform {
def apply[Alg[_[_]] : ConsulDiscoverable]: ConsulDiscoverable[Alg] = implicitly
}

0 comments on commit c3d4f59

Please sign in to comment.