-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add ConsulDiscoverable typeclass and Scala 2 macro implementation #217
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package 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 |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.dwolla.consul.smithy4s | ||
|
||
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("""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 |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.dwolla.consul.smithy4s | ||
|
||
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,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 _root_.com.dwolla.consul.smithy4s.ConsulDiscoverable[$tpe] { | ||
override def host: _root_.org.http4s.Uri.Host = _root_.org.http4s.Uri.Host.unsafeFromString(${host.value}) | ||
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 = _root_.scala.Option(_root_.com.dwolla.consul.smithy4s.DiscoveryMacros.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 |
---|---|---|
@@ -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[_[_]]] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm open to bikeshedding on the names here. This typeclass will have macro-instances for Smithy4s services annotated with the This will typically be used like this: def smithyClient[F[_], Alg[_[_, _, _, _, _]]](client: Client[F], // http4s client
consulMiddleware: ConsulMiddleware[F], // middleware from this library
service: Service[Alg], // generated by smithy4s
)
(implicit C: ConsulDiscoverable[service.Impl]): Resource[F, service.Impl[F]] =
SimpleRestJsonBuilder.apply(service)
.client(NatchezMiddleware.clientWithAttributes(client)(
"rpc.system" -> "smithy",
"peer.service" -> ConsulDiscoverable[service.Impl].host,
))
.uri(ConsulDiscoverable[service.Impl].uri)
.middleware(consulMiddleware)
.resource There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And just to be clear, this does not work, because the macro can't resolve what def smithyClient[F[_], Alg[_[_, _, _, _, _]]](client: Client[F], // http4s client
consulMiddleware: ConsulMiddleware[F], // middleware from this library
service: Service[Alg], // generated by smithy4s
): Resource[F, service.Impl[F]] =
SimpleRestJsonBuilder.apply(service)
.client(NatchezMiddleware.clientWithAttributes(client)(
"rpc.system" -> "smithy",
"peer.service" → HostFromService(service),
))
.uri(UriFromService(service))
.middleware(consulMiddleware)
.resource Implementing it as a typeclass does work, because there is a stable location (outside the function above) where the service is known and the |
||
def host: Host | ||
def uriAuthority: Uri.Authority | ||
def uri: Uri | ||
} | ||
|
||
object ConsulDiscoverable extends ConsulDiscoverablePlatform { | ||
def apply[Alg[_[_]] : ConsulDiscoverable]: ConsulDiscoverable[Alg] = implicitly | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My mental model of how macros work is that it's like the code block is copy/pasted into the spot where the macro is invoked. The imports, etc., that are in effect in that spot are what controls how types are resolved. So e.g. a prior version used the
.some
enhancement method from Cats (uriAuthority.some
) instead of wrapping the value in anOption
as below, which worked in places where the Cats syntax had been imported, but failed otherwise.To avoid that kind of issue or other issues with ambiguous imports, I fully qualified everything in the quasiquote so that it should work regardless of context.