Skip to content

Commit

Permalink
Utilities for extracting GenCodec field / type names
Browse files Browse the repository at this point in the history
  • Loading branch information
sebaciv committed Feb 13, 2025
1 parent cdb4c8c commit 996e3b7
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.avsystem.commons
package serialization

import com.avsystem.commons.annotation.explicitGenerics

/**
* Typeclass holding name of a type that will be used in [[GenCodec]] serialization
*
* @see [[com.avsystem.commons.serialization.GenCodecUtils.codecTypeName]]
*/
final class GencodecTypeName[T](val name: String)
object GencodecTypeName {
def apply[T](implicit tpeName: GencodecTypeName[T]): GencodecTypeName[T] = tpeName

implicit def materialize[T]: GencodecTypeName[T] =
macro com.avsystem.commons.macros.serialization.GenCodecUtilMacros.codecTypeName[T]
}

object GenCodecUtils {
/**
* Allows to extract case class name that will be used in [[GenCodec]] serialization format when dealing with sealed
* hierarchies.
*
* {{{
* @name("SomethingElse")
* final case class Example(something: String)
* object Example extends HasGenCodec[Example]
*
* GenCodecUtils.codecTypeName[Example] // "SomethingElse"
* }}}
*
* @return name of case class, possibility adjusted by [[com.avsystem.commons.serialization.name]] annotation
*/
@explicitGenerics
def codecTypeName[T]: String =
macro com.avsystem.commons.macros.serialization.GenCodecUtilMacros.codecTypeNameRaw[T]

/**
* Allows to extract case class field name that will be used in [[GenCodec]] serialization format
* {{{
* final case class Example(something: String, @name("otherName") somethingElse: Int)
* object Example extends HasGenCodec[Example]
*
* GenCodecUtils.codecFieldName[Example](_.somethingElse) // "otherName"
* }}}
*
* @return name of case class field, possibility adjusted by [[com.avsystem.commons.serialization.name]] annotation
*/
def codecFieldName[T](accessor: T => Any): String =
macro com.avsystem.commons.macros.serialization.GenCodecUtilMacros.codecFieldName[T]

/**
* @return number of sealed hierarchy subclasses or `0` if specified type is not a hierarchy
*/
@explicitGenerics
def knownSubtypesCount[T]: Int =
macro com.avsystem.commons.macros.serialization.GenCodecUtilMacros.knownSubtypesCount[T]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.avsystem.commons
package serialization

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

class GenCodecUtilsTest extends AnyFunSuite with Matchers {
import GenCodecUtilsTest.*

test("plain case class") {
val name = GenCodecUtils.codecTypeName[Foo]
name shouldBe "Foo"
}

test("case class with name annotation") {
val name = GenCodecUtils.codecTypeName[Bar]
name shouldBe "OtherBar"
}

test("case class with name annotation - using GencodecTypeName typeclass") {
val name = GencodecTypeName[Bar].name
name shouldBe "OtherBar"
}

test("plain field") {
val name = GenCodecUtils.codecFieldName[Bar](_.str)
name shouldBe "str"
}

test("field with name annotation") {
val name = GenCodecUtils.codecFieldName[Foo](_.str)
name shouldBe "otherStr"
}

test("accessor chain disallowed") {
"GenCodecUtils.codecFieldName[Complex](_.str.str)" shouldNot compile
}

test("subtypes count hierarchy") {
GenCodecUtils.knownSubtypesCount[ExampleHierarchy] shouldBe 3
}

test("subtypes count leaf") {
GenCodecUtils.knownSubtypesCount[CaseOther] shouldBe 0
}
}

object GenCodecUtilsTest {
final case class Foo(@name("otherStr") str: String)
object Foo extends HasGenCodec[Foo]

@name("OtherBar")
final case class Bar(str: String)
object Bar extends HasGenCodec[Bar]

final case class Complex(str: Foo)
object Complex extends HasGenCodec[Complex]

sealed trait ExampleHierarchy
case class Case123() extends ExampleHierarchy
case object CaseObj987 extends ExampleHierarchy
case class CaseOther() extends ExampleHierarchy
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.avsystem.commons
package macros.serialization

import scala.annotation.tailrec
import scala.reflect.macros.blackbox

class GenCodecUtilMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) {
import c.universe._

final def Pkg: Tree = q"_root_.com.avsystem.commons.serialization"

def codecTypeNameRaw[T: c.WeakTypeTag]: Tree =
q"$extractName"

def codecTypeName[T: c.WeakTypeTag]: Tree =
q"new $Pkg.GencodecTypeName($extractName)"

def codecFieldName[T: c.WeakTypeTag](accessor: Tree): Tree = {
@tailrec
def extract(tree: Tree): Name = tree match {
case Ident(n) => n
case Select(Select(_, _), _) => c.abort(c.enclosingPosition, s"Unsupported nested expression: $accessor")
case Select(_, n) => n
case Function(_, body) => extract(body)
case Apply(func, _) => extract(func)
case _ => c.abort(c.enclosingPosition, s"Unsupported expression: $accessor")
}

val name = extract(accessor)
val tpe = weakTypeOf[T]
val nameStr =
applyUnapplyFor(tpe).flatMap(_.apply.asMethod.paramLists.flatten.iterator.find(_.name == name))
.orElse(tpe.members.iterator.filter(m => m.isMethod && m.isPublic).find(_.name == name))
.map(m => targetName(m))
.getOrElse(c.abort(c.enclosingPosition, s"$name is not a member of $tpe"))

q"$nameStr"
}

def knownSubtypesCount[T: c.WeakTypeTag]: Tree =
q"${knownSubtypes(weakTypeOf[T]).map(_.size).getOrElse(0)}"

private def extractName[T: c.WeakTypeTag]: String = {
val tType = weakTypeOf[T]
targetName(tType.dealias.typeSymbol)
}
}

0 comments on commit 996e3b7

Please sign in to comment.