All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sttp.tapir.macros.FormCodecMacros.scala Maven / Gradle / Ivy

There is a newer version: 1.11.11
Show newest version
package sttp.tapir.macros

import sttp.tapir.generic.Configuration
import sttp.tapir.internal.{CaseClass, CaseClassField}
import sttp.tapir.{Codec, CodecFormat, DecodeResult, Schema}

import scala.quoted.*

trait FormCodecMacros {
  inline given formCaseClassCodec[T](using inline c: Configuration): Codec[String, T, CodecFormat.XWwwFormUrlencoded] =
    ${ FormCodecMacros.formCaseClassCodecImpl[T]('c) }
}

private[tapir] object FormCodecMacros {
  def formCaseClassCodecImpl[T: Type](
      conf: Expr[Configuration]
  )(using q: Quotes): Expr[Codec[String, T, CodecFormat.XWwwFormUrlencoded]] = {
    import quotes.reflect.*
    val caseClass = new CaseClass[q.type, T](using summon[Type[T]], q)
    val encodedNameAnnotationSymbol = TypeTree.of[Schema.annotations.encodedName].tpe.typeSymbol

    def summonCodec[f: Type](field: CaseClassField[q.type, T]) = Expr.summon[Codec[List[String], f, CodecFormat.TextPlain]].getOrElse {
      report.errorAndAbort(s"Cannot find Codec[List[String], T, CodecFormat.TextPlain]] for field: ${field}, of type: ${field.tpe}")
    }

    def encodeDefBody(tTerm: Term): Term = {
      val fieldsEncode = caseClass.fields.map { field =>
        val encodedName = field.extractStringArgFromAnnotation(encodedNameAnnotationSymbol)
        val fieldEncode: Expr[List[(String, String)]] = field.tpe.asType match
          case '[f] =>
            val codec = summonCodec[f](field)

            '{
              val transformedName: String = ${ Expr(encodedName) }.getOrElse($conf.toEncodedName(${ Expr(field.name) }))
              $codec.encode(${ tTerm.select(field.symbol).asExprOf[f] }).map(v => (transformedName, v))
            }

        fieldEncode
      }

      '{ List(${ Varargs(fieldsEncode) }: _*).flatten }.asTerm
    }

    val encodeDefSymbol = Symbol.newMethod(
      Symbol.spliceOwner,
      "encode",
      MethodType(List("t"))(_ => List(caseClass.tpe), _ => TypeRepr.of[Seq[(String, String)]])
    )
    val encodeDef = DefDef(
      encodeDefSymbol,
      { case List(List(tTerm: Term)) =>
        Some(encodeDefBody(tTerm).changeOwner(encodeDefSymbol))
      }
    )
    val encodeExpr = Block(List(encodeDef), Closure(Ref(encodeDefSymbol), None)).asExprOf[T => Seq[(String, String)]]

    //

    def decodeDefBody(paramsTerm: Term): Term = {
      def fieldsDecode(paramsMap: Expr[Map[String, Seq[String]]]) = caseClass.fields.map { field =>
        val encodedName = field.extractStringArgFromAnnotation(encodedNameAnnotationSymbol)
        val fieldDecode = field.tpe.asType match
          case '[f] =>
            val codec = summonCodec[f](field)

            '{
              val transformedName = ${ Expr(encodedName) }.getOrElse($conf.toEncodedName(${ Expr(field.name) }))
              $codec.decode($paramsMap.get(transformedName).toList.flatten)
            }

        fieldDecode
      }

      '{
        val paramsMap: Map[String, Seq[String]] =
          ${ paramsTerm.asExprOf[Seq[(String, String)]] }.groupBy(_._1).transform((_, v) => v.map(_._2))
        val decodeResults = List(${ Varargs(fieldsDecode('paramsMap)) }: _*)
        DecodeResult.sequence(decodeResults).map(values => ${ caseClass.instanceFromValues('{ values }) })
      }.asTerm
    }

    val decodeDefSymbol = Symbol.newMethod(
      Symbol.spliceOwner,
      "decode",
      MethodType(List("params"))(_ => List(TypeRepr.of[Seq[(String, String)]]), _ => TypeRepr.of[DecodeResult[T]])
    )
    val decodeDef = DefDef(
      decodeDefSymbol,
      { case List(List(paramsTerm: Term)) =>
        Some(decodeDefBody(paramsTerm).changeOwner(decodeDefSymbol))
      }
    )
    val decodeExpr = Block(List(decodeDef), Closure(Ref(decodeDefSymbol), None)).asExprOf[Seq[(String, String)] => DecodeResult[T]]

    '{
      Codec.formSeqUtf8
        .mapDecode($decodeExpr)($encodeExpr)
        .schema(${ Expr.summon[Schema[T]].getOrElse(report.errorAndAbort(s"Cannot find a given Schema[${Type.show[T]}].")) })
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy