Skip to content

Commit

Permalink
Merge pull request softwaremill#1742 from borissmidt/master
Browse files Browse the repository at this point in the history
softwaremill#1737 Input annotated class can be empty.
  • Loading branch information
adamw committed Dec 22, 2022
2 parents ee62d08 + 17e627a commit f4d8389
Show file tree
Hide file tree
Showing 6 changed files with 23 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ private[tapir] abstract class EndpointAnnotationsMacro(val c: blackbox.Context)
protected val securitySchemeNameType = c.weakTypeOf[securitySchemeName]

protected def validateCaseClass[A](util: CaseClassUtil[c.type, A]): Unit = {
if (util.fields.isEmpty) {
c.abort(c.enclosingPosition, "Case class must have at least one field")
}
if (1 < util.fields.flatMap(bodyAnnotation).size) {
c.abort(c.enclosingPosition, "No more than one body annotation is allowed")
}
Expand Down Expand Up @@ -114,8 +111,10 @@ private[tapir] abstract class EndpointAnnotationsMacro(val c: blackbox.Context)
}

q"(t: $tupleType) => $className(..$ctorArgs)"
} else {
} else if (inputIdxToFieldIdx.size == 1) {
q"(t: ${util.fields.head.info}) => $className(t)"
} else {
q"(t: ${}) => $className()"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ private[tapir] class EndpointOutputAnnotationsMacro(override val c: blackbox.Con
def generateEndpointOutput[A: c.WeakTypeTag]: c.Expr[EndpointOutput[A]] = {
val util = new CaseClassUtil[c.type, A](c, "response endpoint")
validateCaseClass(util)

if (util.fields.isEmpty) {
c.abort(c.enclosingPosition, "Case class must have at least one field")
}
val outputs = util.fields map { field =>
val output = util
.extractOptStringArgFromAnnotation(field, headerType)
Expand Down
9 changes: 4 additions & 5 deletions core/src/main/scala-2/sttp/tapir/internal/MapToMacro.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ private[tapir] object MapToMacro {
val caseClassUtil = new CaseClassUtil[c.type, CASE_CLASS](c, "mapTo mapping")
val tupleType = weakTypeOf[TUPLE]
val tupleTypeArgs = tupleType.dealias.typeArgs

if (caseClassUtil.fields.size == 1) {
if (caseClassUtil.fields.size == 0) {
q"(t: ${tupleType.dealias}) => ${caseClassUtil.className}()"
} else if (caseClassUtil.fields.size == 1) {
verifySingleFieldCaseClass(c)(caseClassUtil, tupleType)
// Compilation failure if `CaseClass` gets passed as `[Wrapper.CaseClass]` caused by invalid `className`
// retrieval below, workaround available (see: https://github.com/softwaremill/tapir/issues/2540)
Expand All @@ -54,10 +55,8 @@ private[tapir] object MapToMacro {

val caseClassUtil = new CaseClassUtil[c.type, CASE_CLASS](c, "mapTo mapping")
val tupleType = weakTypeOf[TUPLE]

if (caseClassUtil.fields.size == 1) {
verifySingleFieldCaseClass(c)(caseClassUtil, tupleType)

} else {
verifyCaseClassMatchesTuple(c)(caseClassUtil, tupleType, tupleType.dealias.typeArgs)
}
Expand Down Expand Up @@ -85,7 +84,7 @@ private[tapir] object MapToMacro {
tupleTypeArgs: List[c.Type]
): Unit = {
val tupleSymbol = tupleType.typeSymbol
if (!tupleSymbol.fullName.startsWith("scala.Tuple")) {
if (!tupleSymbol.fullName.startsWith("scala.Tuple") && caseClassUtil.fields.nonEmpty) {
c.abort(c.enclosingPosition, s"Expected source type to be a tuple, but got: $tupleType")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,9 @@ private[tapir] class AnnotationsMacros[T <: Product: Type](using q: Quotes) {
Select.unique(tExpr.asTerm, field.name).asExprOf[Any]
}

if (inputIdxToFieldIdx.size > 1) {
if (inputIdxToFieldIdx.size == 0) {
'{ (t: T) => ().asInstanceOf[A] }
} else if (inputIdxToFieldIdx.size > 1) {
'{ (t: T) => ${ Expr.ofTupleFromSeq(tupleArgs('t)) }.asInstanceOf[A] }
} else {
'{ (t: T) => ${ tupleArgs('t).head }.asInstanceOf[A] }
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/scala-3/sttp/tapir/internal/MappingMacros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ private[tapir] object MappingMacros {
case _ => mc.fromProduct(Tuple1(t))
}
def from(out: Out): In = Tuple.fromProduct(out) match {
case EmptyTuple => EmptyTuple.asInstanceOf[In]
case Tuple1(value) => value.asInstanceOf[In]
case value => value.asInstanceOf[In]
}
Expand All @@ -23,6 +24,7 @@ private[tapir] object MappingMacros {

inline def checkFields[A, B](using m: Mirror.ProductOf[A]): Unit =
inline (erasedValue[m.MirroredElemTypes], erasedValue[B]) match {
case _: (EmptyTuple, Unit) => ()
case _: (B *: EmptyTuple, B) => ()
case _: (B, B) => ()
case e => ComplietimeErrors.reportIncorrectMapping[B, A]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ object TapirRequestTest16 {
val testAttributeKey: AttributeKey[String] = AttributeKey[String]
}

@endpointInput("some/path")
final case class TapirRequestTest17()

class DeriveEndpointIOTest extends AnyFlatSpec with Matchers with TableDrivenPropertyChecks with Tapir {

"@endpointInput" should "derive correct input for @query, @cookie, @header" in {
Expand Down Expand Up @@ -317,6 +320,11 @@ class DeriveEndpointIOTest extends AnyFlatSpec with Matchers with TableDrivenPro
}
}

it should "accept empty case classes when annotated with @endpointInput" in {
val expectedInput = ("some" / "path").mapTo[TapirRequestTest17]
compareTransputs(EndpointInput.derived[TapirRequestTest17], expectedInput) shouldBe true
}

it should "not compile if there is field without annotation" in {
assertDoesNotCompile("""
final case class Test(
Expand Down

0 comments on commit f4d8389

Please sign in to comment.