Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ val router = Router[ByteBuffer, Future].route[Api](ApiImpl)

Use it to route requests to your Api implementation:
```scala
val result = router(Request[ByteBuffer]("Api" :: "fun" :: Nil, bytes))
val result = router(Request[ByteBuffer](RequestPath(apiName = "Api", methodName = "fun"), bytes))
// Now result contains the serialized Int result returned by the method ApiImpl.fun
```

Expand Down Expand Up @@ -187,7 +187,7 @@ For logging, you can define a `LogHandler`, which can log each request including
Define it when creating the `Client`:
```scala
object MyLogHandler extends LogHandler[ClientResult[_]] {
def logRequest[T](path: List[String], argumentObject: Any, result: ClientResult[T]): ClientResult[T] = ???
def logRequest[T](path: RequestPath, argumentObject: Any, result: ClientResult[T]): ClientResult[T] = ???
}

val client = Client[PickleType, ClientResult](Transport, MyLogHandler)
Expand All @@ -196,7 +196,7 @@ val client = Client[PickleType, ClientResult](Transport, MyLogHandler)
Define it when creating the `Router`:
```scala
object MyLogHandler extends LogHandler[ServerResult[_]] {
def logRequest[T](path: List[String], argumentObject: Any, result: ServerResult[T]): ServerResult[T] = ???
def logRequest[T](path: RequestPath, argumentObject: Any, result: ServerResult[T]): ServerResult[T] = ???
}

val router = Router[PickleType, ServerResult](MyLogHandler)
Expand Down Expand Up @@ -231,7 +231,7 @@ trait Api {
```

For each declared method in this trait (in this case `fun`):
* Calculate method path: `List("Api", "fun")` (`PathName` annotations on the trait or method are taken into account).
* Calculate method path: `RequestPath("Api", "fun")` (`PathName` annotations on the trait or method are taken into account).
* Serialize the method parameters as a tuple: `(a, b, c)`.

### Server
Expand Down
70 changes: 44 additions & 26 deletions sloth/src/main/scala-2/internal/Macros.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package sloth.internal

import sloth.RequestPath

import scala.reflect.macros.blackbox.Context

object Validator {
Expand All @@ -16,8 +18,11 @@ class Translator[C <: Context](val c: C) {
import c.universe._
import Validator._

val slothPkg = q"_root_.sloth"
val internalPkg = q"_root_.sloth.internal"
object implicits {
implicit val liftRequestPath: Liftable[RequestPath] = Liftable[RequestPath] { r =>
q"new _root_.sloth.RequestPath(${r.apiName}, ${r.methodName})"
}
}

def abort(msg: String) = c.abort(c.enclosingPosition, msg)

Expand All @@ -34,15 +39,20 @@ class Translator[C <: Context](val c: C) {

//TODO rename overloaded methods to fun1, fun2, fun3 or append TypeSignature instead of number?
private def validateAllMethods(methods: List[(MethodSymbol, Type)]): List[Either[String, (MethodSymbol, Type)]] =
methods.groupBy(m => methodPathPart(m._1)).map {
methods.groupBy(m => methodPathInfo(m._1)).map {
case (_, x :: Nil) => Right(x)
case (k, _) => Left(s"""method $k is overloaded (rename the method or add a @PathName("other-name"))""")
case ((name,meta), _) => Left(s"""Method $name (meta=$meta) is overloaded (rename the method or add @PathName("other-name") or @MetaName("meta-name") or a @Meta annotation)""")
}.toList

private def findPathName(annotations: Seq[Annotation]) = annotations.reverse.map(_.tree).collectFirst {
case Apply(Select(New(annotation), _), Literal(Constant(name)) :: Nil) if annotation.tpe =:= typeOf[sloth.PathName] => name.toString
}

private def findMeta(annotations: Seq[Annotation]) = annotations.map(_.tree).collect {
case Apply(Select(New(annotation), _), _) if annotation.tpe <:< typeOf[sloth.Meta] => annotation.tpe.typeSymbol.name.toString
case Apply(Select(New(annotation), _), Literal(Constant(name)) :: Nil) if annotation.tpe =:= typeOf[sloth.MetaName] => name.toString
}.toVector

private def eitherSeq[A, B](list: List[Either[A, B]]): Either[List[A], List[B]] = list.partition(_.isLeft) match {
case (Nil, rights) => Right(for (Right(i) <- rights) yield i)
case (lefts, _) => Left(for (Left(s) <- lefts) yield s)
Expand Down Expand Up @@ -71,11 +81,17 @@ class Translator[C <: Context](val c: C) {
}

//TODO what about fqn for trait to not have overlaps?
def traitPathPart(tpe: Type): String =
findPathName(tpe.typeSymbol.annotations).getOrElse(tpe.typeSymbol.name.toString)

def methodPathPart(m: MethodSymbol): String =
findPathName(m.annotations).getOrElse(m.name.toString)
def traitPathInfo(tpe: Type): (String, Vector[String]) =
(
findPathName(tpe.typeSymbol.annotations).getOrElse(tpe.typeSymbol.name.toString),
findMeta(tpe.typeSymbol.annotations)
)

def methodPathInfo(m: MethodSymbol): (String, Vector[String]) =
(
findPathName(m.annotations).getOrElse(m.name.toString),
findMeta(m.annotations)
)

def paramAsValDef(p: Symbol): ValDef = q"val ${p.name.toTermName}: ${p.typeSignature}"
def paramsAsValDefs(m: Type): List[List[ValDef]] = m.paramLists.map(_.map(paramAsValDef))
Expand Down Expand Up @@ -125,14 +141,15 @@ object TraitMacro {
(c: Context)
(impl: c.Tree)
(implicit traitTag: c.WeakTypeTag[Trait], resultTag: c.WeakTypeTag[Result[_]]): c.Expr[Trait] = Translator(c) { t =>
import t.implicits._
import c.universe._

val validMethods = t.supportedMethodsInType(traitTag.tpe, resultTag.tpe)

val traitPathPart = t.traitPathPart(traitTag.tpe)
val (traitPathPart, traitMeta) = t.traitPathInfo(traitTag.tpe)
val methodImplList = validMethods.collect { case (symbol, method) =>
val methodPathPart = t.methodPathPart(symbol)
val path = traitPathPart :: methodPathPart :: Nil
val (methodPathPart, methodMeta) = t.methodPathInfo(symbol)
val path = RequestPath(traitPathPart, methodPathPart, traitMeta ++ methodMeta)
val parameters = t.paramsAsValDefs(method)
val paramsType = t.paramsType(method)
val paramListValue = t.wrapAsParamsType(method)
Expand Down Expand Up @@ -179,14 +196,15 @@ object RouterMacro {
(value: c.Expr[Trait])
(impl: c.Tree)
(implicit traitTag: c.WeakTypeTag[Trait], pickleTypeTag: c.WeakTypeTag[PickleType], resultTag: c.WeakTypeTag[Result[_]]): c.Expr[Router] = Translator(c) { t =>
import t.implicits._
import c.universe._

val validMethods = t.supportedMethodsInType(traitTag.tpe, resultTag.tpe)

val traitPathPart = t.traitPathPart(traitTag.tpe)
val (traitPathPart, traitMeta) = t.traitPathInfo(traitTag.tpe)
val methodTuples = validMethods.map { case (symbol, method) =>
val methodPathPart = t.methodPathPart(symbol)
val path = traitPathPart :: methodPathPart :: Nil
val (methodPathPart, methodMeta) = t.methodPathInfo(symbol)
val path = RequestPath(traitPathPart, methodPathPart, traitMeta ++ methodMeta)
val paramsType = t.paramsType(method)
val argParams = t.objectToParams(method, TermName("args"))
val innerReturnType = t.getInnerTypeOutOfReturnType(resultTag.tpe, method.finalResultType)
Expand All @@ -195,15 +213,15 @@ object RouterMacro {
value.${symbol.name.toTermName}(...$argParams)
}"""

q"($methodPathPart, $payloadFunction)"
q"($path, $payloadFunction)"
}

q"""
val value = $value
val implRouter = ${c.prefix}
val impl = $impl

implRouter.orElse($traitPathPart, scala.collection.immutable.Map(..$methodTuples))
implRouter.orElse(scala.collection.immutable.Map(..$methodTuples))
"""
}

Expand Down Expand Up @@ -236,10 +254,10 @@ object ChecksumMacro {
case class ParamSignature(name: String, tpe: Type) {
def checksum: Int = (name, typeChecksum(tpe)).hashCode
}
case class MethodSignature(name: String, params: List[ParamSignature], result: Type) {
def checksum: Int = (name, params.map(_.checksum), typeChecksum(result)).hashCode
case class MethodSignature(name: String, meta: Seq[String], params: List[ParamSignature], result: Type) {
def checksum: Int = (name, meta, params.map(_.checksum), typeChecksum(result)).hashCode
}
case class ApiSignature(name: String, methods: Set[MethodSignature]) {
case class ApiSignature(name: String, meta: Seq[String], methods: Set[MethodSignature]) {
def checksum: Int = (name, methods.map(_.checksum)).hashCode
}

Expand Down Expand Up @@ -273,21 +291,21 @@ object ChecksumMacro {
tpe.typeSymbol.fullName,
caseAccessors.map(a => (a.name.toString, typeChecksum(a.typeSignatureIn(tpe).finalResultType))),
directSubClasses.map(typeChecksum).toSet
).hashCode
).hashCode
}

val definedMethods = t.definedMethodsInType(traitTag.tpe)

val dataMethods:Set[MethodSignature] = definedMethods.map { case (symbol, method) =>
val name = t.methodPathPart(symbol)
val dataMethods: Set[MethodSignature] = definedMethods.map { case (symbol, method) =>
val (name, meta) = t.methodPathInfo(symbol)
val resultType = method.finalResultType
val params = paramsOfType(method)

MethodSignature(name, params, resultType)
MethodSignature(name, meta, params, resultType)
}.toSet

val name = t.traitPathPart(traitTag.tpe)
val apiSignature = ApiSignature(name, dataMethods)
val (name, meta) = t.traitPathInfo(traitTag.tpe)
val apiSignature = ApiSignature(name, meta, dataMethods)

val checksum = apiSignature.checksum

Expand Down
44 changes: 36 additions & 8 deletions sloth/src/main/scala-3/internal/Macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ import scala.annotation.meta.param
import scala.NonEmptyTuple
import scala.quoted.runtime.StopMacroExpansion

private implicit val toExprVectorString: ToExpr[Vector[String]] = new ToExpr[Vector[String]] {
def apply(vector: Vector[String])(using Quotes): Expr[Vector[String]] = {
import quotes.reflect._
'{Vector(${Varargs(vector.map(Expr(_)))}: _*)}
}
}

private implicit val toExprRequestPath: ToExpr[RequestPath] = new ToExpr[RequestPath] {
def apply(path: RequestPath)(using Quotes): Expr[RequestPath] = {
import quotes.reflect._
'{ RequestPath(${Expr(path.apiName)}, ${Expr(path.methodName)}, ${Expr(path.meta)}) }
}
}

private def getPathName(using Quotes)(symbol: quotes.reflect.Symbol): String = {
import quotes.reflect.*

Expand All @@ -16,6 +30,17 @@ private def getPathName(using Quotes)(symbol: quotes.reflect.Symbol): String = {
}.getOrElse(symbol.name)
}

private def getMeta(using Quotes)(symbol: quotes.reflect.Symbol): Vector[String] = {
import quotes.reflect.*

symbol.annotations.collect {
case Apply(Select(New(annotation), _), _) if annotation.tpe <:< TypeRepr.of[Meta] =>
annotation.tpe.typeSymbol.name
case Apply(Select(New(annotation), _), Literal(constant) :: Nil) if annotation.tpe =:= TypeRepr.of[MetaName] =>
constant.value.asInstanceOf[String]
}.toVector
}

private def getTypeConstructor(using Quotes)(tpe: quotes.reflect.TypeRepr): quotes.reflect.TypeRepr = {
import quotes.reflect.*

Expand Down Expand Up @@ -80,8 +105,8 @@ def createTypeTreeTuple(using Quotes)(tupleTypesList: List[quotes.reflect.TypeRe
private def checkMethodErrors[Trait: Type, Result[_]: Type](using q: Quotes)(methods: Seq[quotes.reflect.Symbol]): Unit = {
import quotes.reflect.*

val duplicateErrors = methods.groupBy(getPathName).collect { case (name, symbols) if symbols.size > 1 =>
val message = s"Method $name is overloaded, please rename one of the methods or use the PathName annotation to disambiguate"
val duplicateErrors = methods.groupBy(m => (getPathName(m), getMeta(m))).collect { case ((name, meta), symbols) if symbols.size > 1 =>
val message = s"""Method $name (meta=$meta) is overloaded (rename the method or add @PathName("other-name") or @MetaName("meta-name") or a @Meta annotation)"""
(message, symbols.flatMap(_.pos).lastOption)
}

Expand Down Expand Up @@ -149,6 +174,7 @@ object TraitMacro {
checkMethodErrors[Trait, Result](methods)

val traitPathPart = getPathName(TypeRepr.of[Trait].typeSymbol)
val traitMeta = getMeta(TypeRepr.of[Trait].typeSymbol)

def decls(cls: Symbol): List[Symbol] = methods.map { method =>
val methodType = TypeRepr.of[Trait].memberType(method)
Expand All @@ -161,7 +187,9 @@ object TraitMacro {
val result = ValDef.let(Symbol.spliceOwner, implInstance.asTerm) { implRef =>
val body = (cls.declaredMethods.zip(methods)).map { case (method, origMethod) =>
val methodPathPart = getPathName(origMethod)
val path = traitPathPart :: methodPathPart :: Nil
val methodMeta = getMeta(origMethod)
val path = RequestPath(traitPathPart, methodPathPart, traitMeta ++ methodMeta)
val pathExpr = Expr(path)

DefDef(method, { argss =>
// check argss and method.paramSyms have same length outside and inside
Expand All @@ -170,7 +198,6 @@ object TraitMacro {
argss.zip(method.paramSymss).forall { case (a,b) => a.length == b.length }

Option.when(sameLength) {
val pathExpr = Expr(path)
val tupleExpr = argss.flatten match {
case Nil => '{()}
case arg :: Nil => arg.asExpr
Expand Down Expand Up @@ -238,15 +265,16 @@ object RouterMacro {
checkMethodErrors[Trait, Result](methods)

val traitPathPart = getPathName(TypeRepr.of[Trait].typeSymbol)
val traitMeta = getMeta(TypeRepr.of[Trait].typeSymbol)

type FunctionInput = PickleType
type FunctionOutput = Either[ServerFailure, Result[PickleType]]

val result = ValDef.let(Symbol.spliceOwner, implInstance.asTerm) { implRef =>
val methodDefinitions = methods.map { method =>
val methodPathPart = getPathName(method)
val path = traitPathPart :: methodPathPart :: Nil

val methodMeta = getMeta(method)
val path = RequestPath(traitPathPart, methodPathPart, traitMeta ++ methodMeta)
val pathExpr = Expr(path)

val returnType = getInnerTypeOutOfReturnType[Trait, Result](method)
Expand Down Expand Up @@ -308,12 +336,12 @@ object RouterMacro {
})

'{
(${Expr(methodPathPart)}, ${lambda.asExprOf[FunctionInput => FunctionOutput]})
(${pathExpr}, ${lambda.asExprOf[FunctionInput => FunctionOutput]})
}
}

'{
${prefix}.orElse(${Expr(traitPathPart)}, Map.from(${Expr.ofList(methodDefinitions)}))
${prefix}.orElse(Map.from(${Expr.ofList(methodDefinitions)}))
}.asTerm
}

Expand Down
17 changes: 16 additions & 1 deletion sloth/src/main/scala/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,19 @@ package sloth

import scala.annotation.StaticAnnotation

class PathName(val name: String) extends StaticAnnotation
final class PathName(val name: String) extends StaticAnnotation

final class MetaName extends StaticAnnotation

trait Meta extends StaticAnnotation
object Meta {
object http {
final class Get extends Meta
final class Head extends Meta
final class Post extends Meta
final class Put extends Meta
final class Delete extends Meta
final class Options extends Meta
final class Patch extends Meta
}
}
2 changes: 1 addition & 1 deletion sloth/src/main/scala/Failures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sealed trait ServerFailure {
def toException = ServerException(this)
}
object ServerFailure {
case class PathNotFound(path: List[String]) extends ServerFailure
case class PathNotFound(path: RequestPath) extends ServerFailure
case class HandlerError(ex: Throwable) extends ServerFailure
case class DeserializerError(ex: Throwable) extends ServerFailure
}
Expand Down
4 changes: 2 additions & 2 deletions sloth/src/main/scala/LogHandler.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package sloth

trait LogHandler[Result[_]] {
def logRequest[A, T](path: List[String], argumentObject: A, result: Result[T]): Result[T]
def logRequest[A, T](path: RequestPath, argumentObject: A, result: Result[T]): Result[T]
}
object LogHandler {
def empty[Result[_]]: LogHandler[Result] = new LogHandler[Result] {
def logRequest[A, T](path: List[String], argumentObject: A, result: Result[T]): Result[T] = result
def logRequest[A, T](path: RequestPath, argumentObject: A, result: Result[T]): Result[T] = result
}
}
4 changes: 3 additions & 1 deletion sloth/src/main/scala/Request.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package sloth

case class Request[T](path: List[String], payload: T)
case class RequestPath(apiName: String, methodName: String, meta: Vector[String] = Vector.empty)

case class Request[T](path: RequestPath, payload: T)
Loading