英文:
Scala | How can this code be put into a macro annotation?
问题
我有许多包装类,它们的作用只是添加类型安全性和格式化。除了 ClassName 之外,每个包装类本质上都是相同的。
import com.helpers.buildFormatter
import spray.json.JsonFormat
final case class ClassName(value: String)
object ClassName {
implicit val jsonFormatter: JsonFormat[ClassName] =
buildFormatter[ClassName]("ClassName", _.value, this.apply)
}
有没有办法进一步缩短这个代码?我想知道是否可以使用注解、宏或继承来实现。
编辑:buildFormatter
提供了自定义的 JSON 解析,这就是为什么我希望在结果中保留它。
def buildStringFormatter[A](className: String, serializer: A => String, deserializer: String => A): JsonFormat[A]
从评论中:
问:包装 case 类是否始终只有一个参数,参数类型是否始终为 String
,参数名称是否始终为 value
?
答:你是对的,只会有一个参数,参数名会是 value
。我对限制这些有没有问题。
英文:
I have many wrapper classes which all they do is add type safety plus formatting. Besides the ClassName, each wrapper class is essentially the same.
import com.helpers.buildFormatter
import spray.json.JsonFormat
final case class ClassName(value: String)
object ClassName {
implicit val jsonFormatter: JsonFormat[ClassName] =
buildFormatter[ClassName]("ClassName", _.value, this.apply)
}
Is there a way to shorten this further? I'm wondering if an annotation, macro, or inheritance might work.
EDIT: buildFormatter
provides custom json parsing which is why I'd like to keep it in the resulting answer
def buildStringFormatter[A](className: String, serializer: A => String, deserializer: String => A): JsonFormat[A]
From comments:
Q: Will wrapper case classes always be single-parameter, will parameter type always be String
, will parameter name always be value
?
A: You are correct, there would only be 1 parameter and the parameter name be value
. I'm fine with restricting those restrictions
答案1
得分: 3
你可以谷歌搜索 spray json derivation
https://github.com/driver-oss/spray-json-derivation
https://github.com/milessabin/spray-json-shapeless
https://github.com/zackangelo/spray-json-macros
https://github.com/ExNexu/spray-json-annotation
等等。
由于 buildFormatter
的签名是
def buildFormatter[T](str: String, value: T => String, apply: String => T): JsonFormat[T] = ???
(你说包装的 case class 总是单参数的,参数类型总是 String
,参数名称总是 value
),你可以尝试使用 Shapeless
import shapeless.{::, Generic, HList, HNil, Typeable}
object caseClassJsonFormats {
implicit def caseClassJsonFormat[A <: Product, L <: HList](implicit
gen: Generic.Aux[A, String :: HNil],
typeable: Typeable[A]
): JsonFormat[A] =
buildFormatter[A](typeable.describe, gen.to(_).head, s => gen.from(s :: HNil))
}
这样你为所有的 case class 定义了一个通用的隐式(而不是每个 case class 都有一个隐式)。
测试:
final case class ClassName(value: String)
import caseClassJsonFormats._
implicitly[JsonFormat[ClassName]] // 编译通过
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
@compileTimeOnly("enable macro annotations")
class jsonFormat extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro JsonFormatMacro.impl
}
object JsonFormatMacro {
def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
import c.universe._
def jsonFormatImplicit(tpname: TypeName) =
q"""implicit val jsonFormatter: _root_.spray.json.JsonFormat[$tpname] =
buildFormatter[$tpname](${tpname.toString}, _.value, this.apply)"""
annottees match {
// if there is companion object, modify it
case (clazz@q"$_ class $tpname[..$_] $_(...$_) extends { ..$_ } with ..$_ { $_ => ..$_ }") ::
q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }" :: Nil =>
q"""
$clazz
$mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
..$body
${jsonFormatImplicit(tpname)}
}"""
// if there is no companion object, create it
case (clazz@q"$_ class $tpname[..$_] $_(...$_) extends { ..$_ } with ..$_ { $_ => ..$_ }") :: Nil =>
q"""
$clazz
object ${tpname.toTermName} {
${jsonFormatImplicit(tpname)}
}"""
}
}
}
这样你为每个带注解的 case class 的伴生对象中都定义了一个隐式。
测试:
@jsonFormat
final case class ClassName(value: String)
implicitly[JsonFormat[ClassName]] // 编译通过
英文:
You can google spray json derivation
https://github.com/driver-oss/spray-json-derivation
https://github.com/milessabin/spray-json-shapeless
https://github.com/zackangelo/spray-json-macros
https://github.com/ExNexu/spray-json-annotation
etc.
Since the signature of buildFormatter
is
def buildFormatter[T](str: String, value: T => String, apply: String => T): JsonFormat[T] = ???
(and you said that wrapper case classes are always single-parameter, parameter type is always String
, parameter name is always value
) you can try Shapeless
import shapeless.{::, Generic, HList, HNil, Typeable}
object caseClassJsonFormats {
implicit def caseClassJsonFormat[A <: Product, L <: HList](implicit
gen: Generic.Aux[A, String :: HNil],
typeable: Typeable[A]
): JsonFormat[A] =
buildFormatter[A](typeable.describe, gen.to(_).head, s => gen.from(s :: HNil))
}
So you define a single implicit for all case classes (instead of an implicit per each case class).
Testing:
final case class ClassName(value: String)
import caseClassJsonFormats._
implicitly[JsonFormat[ClassName]] // compiles
Alternative approach is a macro annotation (sbt settings for macro projects)
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
@compileTimeOnly("enable macro annotations")
class jsonFormat extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro JsonFormatMacro.impl
}
object JsonFormatMacro {
def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
import c.universe._
def jsonFormatImplicit(tpname: TypeName) =
q"""implicit val jsonFormatter: _root_.spray.json.JsonFormat[$tpname] =
buildFormatter[$tpname](${tpname.toString}, _.value, this.apply)"""
annottees match {
// if there is companion object, modify it
case (clazz@q"$_ class $tpname[..$_] $_(...$_) extends { ..$_ } with ..$_ { $_ => ..$_ }") ::
q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }" :: Nil =>
q"""
$clazz
$mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
..$body
${jsonFormatImplicit(tpname)}
}"""
// if there is no companion object, create it
case (clazz@q"$_ class $tpname[..$_] $_(...$_) extends { ..$_ } with ..$_ { $_ => ..$_ }") :: Nil =>
q"""
$clazz
object ${tpname.toTermName} {
${jsonFormatImplicit(tpname)}
}"""
}
}
}
So you define an implicit in companion object per each case class annotated.
Testing:
@jsonFormat
final case class ClassName(value: String)
implicitly[JsonFormat[ClassName]] // compiles
// scalacOptions += "-Ymacro-debug-lite"
//scalac: {
// final case class ClassName extends scala.Product with scala.Serializable {
// <caseaccessor> <paramaccessor> val value: String = _;
// def <init>(value: String) = {
// super.<init>();
// ()
// }
// };
// object ClassName extends scala.AnyRef {
// def <init>() = {
// super.<init>();
// ()
// };
// implicit val jsonFormatter: _root_.spray.json.JsonFormat[ClassName] = buildFormatter[ClassName]("ClassName", ((x$1) => x$1.value), this.apply)
// };
// ()
//}
答案2
得分: 0
可以使用这个选项:
import spray.json.DefaultJsonProtocol._
implicit val format = jsonFormat1(ClassName.apply)
注意:这段代码是Scala语言的代码片段,用于使用Spray JSON库来定义一个类的JSON序列化格式。
英文:
You can use this option:
import spray.json.DefaultJsonProtocol._
implicit val format = jsonFormat1(ClassName.apply)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论