Skip to content

Commit

Permalink
fix compilation errors for methods with varargs
Browse files Browse the repository at this point in the history
  • Loading branch information
yakivy committed Dec 3, 2023
1 parent 2b30d5f commit 340ac20
Show file tree
Hide file tree
Showing 17 changed files with 414 additions and 234 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
core:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: coursier/cache-action@v6
- uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: '8'
- run: ./mill __.__.test
32 changes: 22 additions & 10 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = 3.4.3
version = 3.7.12
runner.dialect = scala3
maxColumn = 120
assumeStandardLibraryStripMargin = true
Expand All @@ -7,23 +7,35 @@ indent {
callSite = 4
}
indentOperator.exemptScope = aloneEnclosed
align {
openParenCallSite = false
openParenDefnSite = false
tokens = []
align.tokens = []
rewrite {
rules = [Imports, SortModifiers]
trailingCommas.style = keep
imports {
expand = true
sort = original
}
}
newlines {
source = keep
avoidForSimpleOverflow = [tooLong, slc]
topLevelStatementBlankLines = [
{
maxNest = 0
blanks = 1
},
{
minBreaks = 2
blanks = 1
}
]
}
docstrings {
wrap = no
style = Asterisk
removeEmpty = true
}
optIn {
annotationNewlines = true
breakChainOnFirstMethodDot = true
selfAnnotationNewline = true
}
binPack.parentConstructors = keep
project.git = false

fileOverride {
Expand Down
9 changes: 0 additions & 9 deletions .travis.yml

This file was deleted.

20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
## Poppet
[![Maven Central](https://img.shields.io/maven-central/v/com.github.yakivy/poppet-core_2.13.svg)](https://mvnrepository.com/search?q=poppet)
[![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/https/oss.sonatype.org/com.github.yakivy/poppet-core_2.13.svg)](https://oss.sonatype.org/content/repositories/snapshots/com/github/yakivy/poppet-core_2.13/)
[![Build Status](https://app.travis-ci.com/yakivy/poppet.svg?branch=master)](https://app.travis-ci.com/github/yakivy/poppet)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
<a href="https://typelevel.org/cats/"><img src="https://typelevel.org/cats/img/cats-badge.svg" height="40px" align="right" alt="Cats friendly" /></a>

Poppet is a minimal, type-safe RPC Scala library.

Essential differences from [autowire](https://github.com/lihaoyi/autowire):
- has no explicit macro application `.call`, result of a consumer is an instance of original trait
- has no restricted HKT `Future`, you can specify any monad (has `cats.Monad` typeclass) as a processor HKT, and an arbitrary HKT for trait methods
- has no forced codec dependencies `uPickle`, you can choose from the list of predefined codecs or easily implement your own
- has no restricted HKT `Future`, you can specify any monad (has `cats.Monad` typeclass) as HKT for the provider/consumer
- has no forced codec dependencies `uPickle`, you can choose from the list of predefined codecs or easily implement your own codec
- has robust failure handling mechanism
- supports Scala 3 (method/class generation with macros is still an experimental feature)
- supports Scala 3 (however method/class generation with macros is still an experimental feature)

### Table of contents
1. [Quick start](#quick-start)
Expand All @@ -29,9 +28,9 @@ Essential differences from [autowire](https://github.com/lihaoyi/autowire):
Put cats and poppet dependencies in the build file, let's assume you are using SBT:
```scala
val version = new {
val cats = "2.9.0"
val circe = "0.14.3"
val poppet = "0.3.1"
val cats = "2.10.0"
val circe = "0.14.6"
val poppet = "0.3.4"
}

libraryDependencies ++= Seq(
Expand Down Expand Up @@ -158,7 +157,7 @@ curl --location --request POST '${providerUrl}' \
```

### Limitations
You can generate consumer/provider almost from any Scala trait (or Java interface 😲). It can have non-abstract members, methods with default arguments, methods with multiple argument lists, etc... But there are several limitations:
You can generate consumer/provider almost from any Scala trait (or Java interface 😲). It can have non-abstract members, methods with default arguments, methods with multiple argument lists, varargs, etc... But there are several limitations:
- you cannot overload methods with the same argument names, because for the sake of simplicity argument names are being used as a part of the request, for more info check [manual calls](#manual-calls) section:
```scala
//compiles
Expand Down Expand Up @@ -230,12 +229,15 @@ Provider[..., ...]()
- add action (including argument name) to codec
- throw an exception on duplicated service processor
- separate `.service[S]` and `.service[G[_], S]` to simplify codec resolution
- add possibility to update service name, try to unify service name between Scala 2.x and 3.x
- don't create ObjectMapper in the lib, use implicit one
- check that passed class is a trait and doesn't have arguments to prevent obscure error from compiler
- check that all abstract methods are public

### Changelog

#### 0.3.4:
- fix compilation errors for methods with varargs

#### 0.3.3:
- fix several compilation errors for Scala 3

Expand Down
20 changes: 10 additions & 10 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ import mill.playlib._
import com.github.lolgab.mill.crossplatform._

object versions {
val publish = "0.3.3"
val publish = "0.3.4"

val scala212 = "2.12.17"
val scala213 = "2.13.10"
val scala3 = "3.2.1"
val scalaJs = "1.11.0"
val scalaNative = "0.4.8"
val scala212 = "2.12.18"
val scala213 = "2.13.12"
val scala3 = "3.3.0"
val scalaJs = "1.13.2"
val scalaNative = "0.4.16"
val scalatest = "3.2.14"
val cats = "2.9.0"
val cats = "2.10.0"

val upickle = "2.0.0"
val circe = "0.14.3"
val playJson = "2.9.3"
val jackson = "2.13.4"
val circe = "0.14.6"
val playJson = "2.9.4"
val jackson = "2.13.5"

val catsEffect = "3.4.1"
val http4s = "0.23.12"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import scala.language.experimental.macros
import scala.reflect.macros.blackbox

trait ConsumerProcessorObjectBinCompat {
implicit def apply[F[_], I, S]: ConsumerProcessor[F, I, S] =
macro ConsumerProcessorObjectBinCompat.applyImpl[F, I, S]
implicit def generate[F[_], I, S]: ConsumerProcessor[F, I, S] =
macro ConsumerProcessorObjectBinCompat.generateImpl[F, I, S]
}

object ConsumerProcessorObjectBinCompat {
def applyImpl[F[_], I, S](
def generateImpl[F[_], I, S](
c: blackbox.Context)(
implicit FT: c.WeakTypeTag[F[_]], IT: c.WeakTypeTag[I], ST: c.WeakTypeTag[S]
): c.Expr[ConsumerProcessor[F, I, S]] = {
Expand All @@ -23,7 +23,7 @@ object ConsumerProcessorObjectBinCompat {
val arguments = mInS.paramLists.map(ps => ps.map(p => q"${Ident(p.name)}: ${p.typeSignature}"))
val (returnKind, returnType) = ProcessorMacro.separateReturnType(c)(FT.tpe, mInS.finalResultType, false)
val codedArgument: c.universe.Symbol => Tree = a => q"""_root_.scala.Predef.implicitly[
_root_.poppet.core.Codec[${a.typeSignature},${IT.tpe}]
_root_.poppet.core.Codec[${ProcessorMacro.unwrapVararg(c)(a.typeSignature)},${IT.tpe}]
].apply(${Ident(a.name)}).fold($$fh.apply, $fmonad.pure)"""
val withCodedArguments: Tree => Tree = tree => mInS.paramLists.flatten match {
case Nil => tree
Expand Down
6 changes: 6 additions & 0 deletions core/src-2/poppet/core/ProcessorMacro.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ object ProcessorMacro {
}
}

def unwrapVararg(c: blackbox.Context)(t: c.Type): c.Type = {
import c.universe._
if (t.typeSymbol == definitions.RepeatedParamClass) appliedType(typeOf[Seq[_]], t.typeArgs)
else t
}

def separateReturnType(
c: blackbox.Context)(fType: c.Type, returnType: c.Type, fromReturn: Boolean
): (c.Type, c.Type) = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import scala.language.experimental.macros
import scala.reflect.macros.blackbox

trait ProviderProcessorObjectBinCompat {
implicit def apply[F[_], I, S]: ProviderProcessor[F, I, S] =
macro ProviderProcessorObjectBinCompat.applyImpl[F, I, S]
implicit def generate[F[_], I, S]: ProviderProcessor[F, I, S] =
macro ProviderProcessorObjectBinCompat.generateImpl[F, I, S]
}

object ProviderProcessorObjectBinCompat {
def applyImpl[F[_], I, S](
def generateImpl[F[_], I, S](
c: blackbox.Context)(
implicit FT: c.WeakTypeTag[F[_]], IT: c.WeakTypeTag[I], ST: c.WeakTypeTag[S]
): c.Expr[ProviderProcessor[F, I, S]] = {
Expand All @@ -21,7 +21,7 @@ object ProviderProcessorObjectBinCompat {
val argumentNames = m.paramLists.flatten.map(_.name.toString)
val (returnKind, returnType) = ProcessorMacro.separateReturnType(c)(FT.tpe, mInS.finalResultType, true)
val codedArgument: c.universe.Symbol => Tree = a => q"""_root_.scala.Predef.implicitly[
_root_.poppet.core.Codec[$IT,${a.typeSignature}]
_root_.poppet.core.Codec[$IT,${ProcessorMacro.unwrapVararg(c)(a.typeSignature)}]
].apply(as(${a.name.toString})).fold($$fh.apply, $fmonad.pure)"""
val withCodedArguments: Tree => Tree = tree => mInS.paramLists.flatten match {
case Nil => tree
Expand All @@ -38,17 +38,26 @@ object ProviderProcessorObjectBinCompat {
returnKind, returnType, mInS.finalResultType,
FT.tpe, IT.tpe, appliedType(FT.tpe, IT.tpe),
)
val groupedArguments = m.paramLists.map(pl => pl.map(p => Ident(p.name)))
val groupedArguments = m.paramLists.map(pl => pl.map(p => p.typeSignature.typeSymbol -> Ident(p.name)))
q"""new _root_.poppet.provider.core.MethodProcessor[$FT, $IT](
${ST.tpe.typeSymbol.fullName},
${m.name.toString},
_root_.scala.List(..$argumentNames),
as => ${withCodedArguments(q"""
$fmonad.flatMap($returnKindCodec.apply(${groupedArguments.foldLeft[Tree](
q"$$service.${m.name.toTermName}")((acc, pl) => Apply(acc, pl)
)}))(_root_.scala.Predef.implicitly[
_root_.poppet.core.Codec[$returnType,${IT.tpe}]
].apply(_).fold($$fh.apply, $fmonad.pure))
$fmonad.flatMap(
$returnKindCodec.apply(${
groupedArguments.foldLeft[Tree](q"$$service.${m.name.toTermName}") { (acc, pl) =>
pl.lastOption match {
case Some((s, i)) if s == definitions.RepeatedParamClass =>
q"$acc(..${pl.init.map(_._2)}, $i: _*)"
case _ => q"$acc(..${pl.map(_._2)})"
}
}
})
)(
_root_.scala.Predef.implicitly[_root_.poppet.core.Codec[$returnType,${IT.tpe}]]
.apply(_).fold($$fh.apply, $fmonad.pure)
)
""")}
)"""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ object ConsumerProcessorObjectBinCompat {
): q.reflect.Term = {
import q.reflect._
val methodReturnTpe = methodSymbol.tree.asInstanceOf[DefDef].returnTpt.tpe
def codedArgument(a: Tree): Expr[F[I]] = resolveTypeMember(
def codedArgument(a: Tree): Expr[F[I]] = unwrapVararg(resolveTypeMember(
TypeRepr.of[S], Ref(a.symbol).tpe.widen
).asType match { case '[at] =>
)).asType match { case '[at] =>
'{ summonInline[Codec[at,I]].apply(${Ref.term(a.symbol.termRef).asExprOf[at]}).fold($fh.apply, $MF.pure) }
}
def codedArguments(terms: List[Tree]): Expr[F[Map[String, I]]] = {
Expand Down
11 changes: 11 additions & 0 deletions core/src-3/poppet/core/ProcessorMacro.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ object ProcessorMacro {
methods
}

def unwrapVararg(using q: Quotes)(tpe: q.reflect.TypeRepr) = {
import q.reflect._
tpe match {
case AnnotatedType(tpeP, t) if t.tpe.typeSymbol == defn.RepeatedAnnot =>
TypeRepr.of[Seq].appliedTo(tpeP.typeArgs)
case tpe if tpe.typeSymbol == defn.RepeatedParamClass =>
TypeRepr.of[Seq].appliedTo(tpe.typeArgs)
case _ => tpe
}
}

def resolveTypeMember(
using q: Quotes)(
owner: q.reflect.TypeRepr,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ object ProviderProcessorObjectBinCompat {
val serviceName = TypeRepr.of[S].show
val methodProcessors = getAbstractMethods[S].map { m =>
def decodeArg(arg: ValDef): Expr[Map[String, I] => F[Any]] = {
resolveTypeMember(TypeRepr.of[S], arg.tpt.tpe).asType match { case '[at] => '{ input =>
unwrapVararg(resolveTypeMember(TypeRepr.of[S], arg.tpt.tpe)).asType match { case '[at] => '{ input =>
summonInline[Codec[I, at]]
.apply(input(${Literal(StringConstant(arg.name)).asExprOf[String]}))
.fold($fh.apply, $MF.pure)
Expand All @@ -48,18 +48,29 @@ object ProviderProcessorObjectBinCompat {
TypeRepr.of[F], TypeRepr.of[I], TypeRepr.of[F[I]],
)
val callService: Expr[Map[String, I] => F[I]] = returnType.asType match { case '[rt] =>
val paramTypes = m.termParamss.map(_.params.map(t => resolveTypeMember(TypeRepr.of[S], t.tpt.tpe)))
val paramTypes = m.termParamss.map(_.params.map(arg =>
arg -> resolveTypeMember(TypeRepr.of[S], arg.tpt.tpe)
))
'{ input =>
$MF.flatMap(
$decodeArgs(input))(ast => $MF.flatMap(
${Apply(TypeApply(
Select.unique(returnKindCodec, "apply"),
List(TypeTree.of[rt])),
List(paramTypes.foldLeft[(Term, Int)](
Select(service.asTerm, m.symbol) -> 0)((acc, item) =>
Apply(acc._1, item.zipWithIndex.map{ t => t._1.asType match { case '[at] =>
'{ast.apply(${Literal(IntConstant(t._2 + acc._2)).asExprOf[Int]}).asInstanceOf[at]}.asTerm
}}) -> (item.size + acc._2)
Select(service.asTerm, m.symbol) -> 0)((acc, item) => (
Apply(acc._1, item.zipWithIndex.map{ case ((arg, t), i) =>
t.asType match { case '[at] =>
val term = '{ast.apply(${Literal(IntConstant(i + acc._2)).asExprOf[Int]}).asInstanceOf[at]}.asTerm
arg.tpt.tpe match {
case AnnotatedType(tpeP, t) if t.tpe.typeSymbol == defn.RepeatedAnnot =>
Typed(term, Inferred(defn.RepeatedParamClass.typeRef.appliedTo(tpeP.typeArgs)))
case _ => term
}
}
}),
(item.size + acc._2)
)
)._1)
).asExprOf[F[rt]]})(
${returnTypeCodec.asExprOf[Codec[rt, I]]}.apply(_).fold($fh.apply, $MF.pure)
Expand Down
4 changes: 3 additions & 1 deletion core/src/poppet/consumer/core/ConsumerProcessor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ trait ConsumerProcessor[F[_], I, S] {
def apply(client: Request[I] => F[Response[I]], fh: FailureHandler[F]): S
}

object ConsumerProcessor extends ConsumerProcessorObjectBinCompat
object ConsumerProcessor extends ConsumerProcessorObjectBinCompat {
def apply[F[_], I, S](implicit instance: ConsumerProcessor[F, I, S]): ConsumerProcessor[F, I, S] = instance
}
2 changes: 1 addition & 1 deletion core/src/poppet/provider/core/Provider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Provider[F[_] : Monad, I](
private val indexedProcessors: Map[String, Map[String, Map[String, Map[String, I] => F[I]]]] =
processors.groupBy(_.service).mapValues(
_.groupBy(_.name).mapValues(
_.map(m => m.arguments.sorted.mkString(",") -> m.f).toMap
_.map(m => m.arguments.toList.sorted.mkString(",") -> m.f).toMap
).toMap
).toMap

Expand Down
4 changes: 3 additions & 1 deletion core/src/poppet/provider/core/ProviderProcessor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ class MethodProcessor[F[_], I](
val service: String, val name: String, val arguments: List[String], val f: Map[String, I] => F[I]
)

object ProviderProcessor extends ProviderProcessorObjectBinCompat
object ProviderProcessor extends ProviderProcessorObjectBinCompat {
def apply[F[_], I, S](implicit instance: ProviderProcessor[F, I, S]): ProviderProcessor[F, I, S] = instance
}
Loading

0 comments on commit 340ac20

Please sign in to comment.