-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Utilities for extracting GenCodec field / type names #674
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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] | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not gonna die on this hill, but I'm wondering if we even need the typeclass, the use case is pretty esoteric and the util covers it. |
||
|
||
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.* | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we change behaviour of GenCodec, but not the behaviour of this class, we may miss the discrepancy. Maybe we should write a custom output that will make it simple to compare if the name is also equal to what gencodec actually outputs. |
||
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 | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to scaladoc: We should definitely test that. |
||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think this macro could share some code with GenCodec macros for more intended coupling now that they are in the same module? |
||
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) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: something like
Typeclass holding a name used by GenCodec to serialize the T type.
would be easier to understand for first time reader imho