Skip to content

Commit

Permalink
add ConsulDiscoverable typeclass and Scala 2 macro implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
bpholt committed Oct 30, 2024
1 parent 140f6c8 commit 6383bc3
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 2 deletions.
8 changes: 8 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,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
@@ -1,8 +1,43 @@
package com.dwolla.consul.smithy4s

import cats.syntax.all._
import com.dwolla.test.HelloService
import munit.FunSuite
import org.http4s.Uri
import org.http4s.Uri.Host
import scala.reflect.runtime.currentMirror
import scala.reflect.runtime.universe._
import scala.tools.reflect.ToolBox

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

private val toolbox = currentMirror.mkToolBox()

private def typecheck(input: String): Either[Throwable, Tree] =
for {
parsed <- Either.catchNonFatal(toolbox.parse(input))
typechecked <- Either.catchNonFatal(toolbox.typecheck(parsed))
} yield typechecked

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") {
val typechecked = typecheck("""com.dwolla.consul.smithy4s.ConsulDiscoverable[com.dwolla.consul.smithy4s.NotSmithy]""")

assertEquals(
typechecked.leftMap(_.getMessage),
"reflective typecheck has failed: Instances are only available for Smithy4s Services annotated with @discoverable".asLeft
)
}
}

trait NotSmithy[F[_]] {
def foo(int: Int): F[Unit]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.dwolla.consul.smithy4s

import cats.syntax.all._
import com.dwolla.consul.smithy._
import org.http4s.Uri.Host
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 {
def makeInstance[Alg[_[_]]](c: whitebox.Context): c.Expr[ConsulDiscoverable[Alg]] = {
import c.universe.{Try => _, _}

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 com.dwolla.consul.smithy4s.ConsulDiscoverable[$tpe] {
override def host: org.http4s.Uri.Host = org.http4s.Uri.Host.unsafeFromString(${host.value})
override def uriAuthority: org.http4s.Uri.Authority = org.http4s.Uri.Authority(host = host)
override def uri: org.http4s.Uri = org.http4s.Uri(scheme = com.dwolla.consul.smithy4s.DiscoveryMacros.consulScheme.some, authority = uriAuthority.some)
}
""")

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"
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 6383bc3

Please sign in to comment.