From 996e3b74f0e6ccf7c9e462a3dfb6795085a4f0b8 Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Thu, 13 Feb 2025 13:32:42 +0100 Subject: [PATCH] Utilities for extracting GenCodec field / type names --- .../serialization/GencodecTypeName.scala | 58 +++++++++++++++++ .../serialization/GenCodecUtilsTest.scala | 63 +++++++++++++++++++ .../serialization/GenCodecUtilMacros.scala | 47 ++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 core/src/main/scala/com/avsystem/commons/serialization/GencodecTypeName.scala create mode 100644 core/src/test/scala/com/avsystem/commons/serialization/GenCodecUtilsTest.scala create mode 100644 macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecUtilMacros.scala diff --git a/core/src/main/scala/com/avsystem/commons/serialization/GencodecTypeName.scala b/core/src/main/scala/com/avsystem/commons/serialization/GencodecTypeName.scala new file mode 100644 index 000000000..a17fb1e2b --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/serialization/GencodecTypeName.scala @@ -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] +} diff --git a/core/src/test/scala/com/avsystem/commons/serialization/GenCodecUtilsTest.scala b/core/src/test/scala/com/avsystem/commons/serialization/GenCodecUtilsTest.scala new file mode 100644 index 000000000..7aa6d76e8 --- /dev/null +++ b/core/src/test/scala/com/avsystem/commons/serialization/GenCodecUtilsTest.scala @@ -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 +} diff --git a/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecUtilMacros.scala b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecUtilMacros.scala new file mode 100644 index 000000000..007ede5bc --- /dev/null +++ b/macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecUtilMacros.scala @@ -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) + } +}