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

com.augustnagro.magnum.DerivingUtil.scala Maven / Gradle / Ivy

The newest version!
package com.augustnagro.magnum

import scala.deriving.Mirror
import scala.compiletime.{
  constValue,
  constValueTuple,
  erasedValue,
  error,
  summonFrom,
  summonInline
}
import scala.quoted.*
import scala.reflect.ClassTag

/** Not useful for typical user code; provided to help implement custom DbCodecs
  * and associated typeclasses
  */
object DerivingUtil:
  /** For a Simple (non-ADT) enum type E, constructs a sequence of mappings from
    * sql string representation to enum value. For example,
    *
    * {{{
    *   @Table(PostgresDbType, SqlNameMapper.CamelToUpperSnakeCase)
    *   enum Color { case Red, @SqlName("greeeeeen") Green, Blue }
    * }}}
    *
    * Results in
    *
    * {{{
    *   Seq("Red" -> Color.Red, "greeeeeen" -> Color.Green, "Blue" -> Color.Blue)
    * }}}
    *
    * Will produce a compile error if the enum is not simple (non-adt).
    *
    * @tparam E
    *   the enum type, like Color
    * @tparam Mels
    *   enum Mirror's MirroredElemLabels
    * @tparam Mets
    *   enum Mirror's MirroredElemTypes
    */
  def buildSqlNameMapForEnum[
      E: Type,
      Mels: Type,
      Mets: Type
  ](using q: Quotes): Expr[Seq[(String, E)]] =
    import q.reflect.*
    val tableAnnot = TypeRepr.of[Table].typeSymbol
    val defaultNameMapper: Expr[SqlNameMapper] =
      TypeRepr
        .of[E]
        .typeSymbol
        .getAnnotation(tableAnnot) match
        case Some(term) =>
          val tableExpr = term.asExprOf[Table]
          '{ $tableExpr.nameMapper }
        case None =>
          '{ SqlNameMapper.SameCase }

    val sumValueExprs: Vector[Expr[E]] = sumValues[E, Mets]()
    val scalaNames = getScalaNames[Mels]()

    val sqlNameAnnot = TypeRepr.of[SqlName].typeSymbol
    val enumCaseSymbols = TypeRepr.of[E].typeSymbol.children

    val sqlNameExprs: Vector[Expr[(String, E)]] = scalaNames
      .zip(sumValueExprs)
      .map((scalaName, sumExpr) =>
        val nameAnnot = enumCaseSymbols
          .find(sym => sym.name == scalaName && sym.hasAnnotation(sqlNameAnnot))
          .flatMap(sym => sym.getAnnotation(sqlNameAnnot))
        nameAnnot match
          case Some(term) =>
            val sqlNameExpr: Expr[SqlName] = term.asExprOf[SqlName]
            '{ ($sqlNameExpr.name.toString, $sumExpr) }
          case None =>
            val scalaNameExpr = Expr(scalaName)
            '{ ($defaultNameMapper.toColumnName($scalaNameExpr), $sumExpr) }
      )
    Expr.ofSeq(sqlNameExprs)
  end buildSqlNameMapForEnum

  private def getScalaNames[Mels: Type](res: Vector[String] = Vector.empty)(
      using Quotes
  ): Vector[String] =
    import quotes.reflect.*
    Type.of[Mels] match
      case '[mel *: melTail] =>
        val melString = Type.valueOfConstant[mel].get.toString
        getScalaNames[melTail](res :+ melString)
      case '[EmptyTuple] => res

  private def sumValues[E: Type, Mets: Type](
      res: Vector[Expr[E]] = Vector.empty
  )(using Quotes): Vector[Expr[E]] =
    import quotes.reflect.*
    Type.of[Mets] match
      case '[met *: metTail] =>
        val expr = Expr.summon[Mirror.ProductOf[met]] match
          case Some(m) if isSingleton[met] =>
            '{ $m.fromProduct(EmptyTuple).asInstanceOf[E] }
          case _ =>
            report.errorAndAbort("Can only derive simple (non-adt) enums")
        sumValues[E, metTail](res :+ expr)
      case '[EmptyTuple] => res

  private def isSingleton[T: Type](using Quotes): Boolean =
    import quotes.reflect.*
    Expr.summon[Mirror.ProductOf[T]] match
      case Some('{
            $mp: Mirror.ProductOf[T] {
              type MirroredElemTypes = mets
            }
          }) =>
        tupleArity[mets]() == 0
      case _ => false

  private def tupleArity[T: Type](res: Int = 0)(using Quotes): Int =
    import quotes.reflect.*
    Type.of[T] match
      case '[x *: xs]    => tupleArity[xs](res + 1)
      case '[EmptyTuple] => res

  /** Finds the first SqlName annotation on type T */
  def sqlTableNameAnnot[T: Type](using Quotes): Option[Expr[SqlName]] =
    import quotes.reflect._
    val annot = TypeRepr.of[SqlName]
    TypeRepr
      .of[T]
      .typeSymbol
      .annotations
      .find(_.tpe =:= annot)
      .map(term => term.asExprOf[SqlName])

  /** Finds the first Table annotation on type T */
  def tableAnnot[T: Type](using Quotes): Option[Expr[Table]] =
    import quotes.reflect.*
    val annot = TypeRepr.of[Table]
    TypeRepr
      .of[T]
      .typeSymbol
      .annotations
      .find(_.tpe =:= annot)
      .map(term => term.asExprOf[Table])

end DerivingUtil




© 2015 - 2025 Weber Informatics LLC | Privacy Policy