automatically generate case object for case class
Asked Answered
J

1

5

How can I have the scala compiler automatically generate the case object?

// Pizza class
class Pizza (val crust_type: String)

// companion object
object Pizza {
    val crustType = "crust_type"
}

Desired properties for case object

  • for each attribute in the case class generate an attribute in the case object
  • set the value in of each corresponding case object to the string representation of the attribute name and change camelCase to snake_case for the object attribute name, keep snake_case for object attribute value
Judithjuditha answered 26/6, 2018 at 17:16 Comment(2)
Maybe scalameta.org is also a good tool instead of macros?Judithjuditha
#38443222 is a bit similar for macrosJudithjuditha
T
7

You can create macro annotation (generating companion object, failing if it already exists)

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

@compileTimeOnly("enable macro paradise to expand macro annotations")
class GenerateCompanion extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro GenerateCompanion.impl
}

object GenerateCompanion {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._

    annottees match {
      case (c@q"$_ class $tpname[..$_] $_(...$paramss) extends { ..$_ } with ..$_ { $_ => ..$_ }") :: Nil =>

        val vals = paramss.flatten.map(p => {
          val name = p.name.toString
          q"val ${TermName(underscoreToCamel(name))}: String = $name"
        })

        q"""
          $c
          object ${tpname.toTermName} {..$vals}
        """
    }
  }

  def underscoreToCamel(name: String): String = "_([a-z\\d])".r.replaceAllIn(name, _.group(1).toUpperCase)
}

and use it

@GenerateCompanion
class Pizza(val crust_type: String)

Pizza.crustType //crust_type

New macro (modifying companion object if it exists or generating it if it doesn't):

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

@compileTimeOnly("enable macro paradise to expand macro annotations")
class GenerateCompanion extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro GenerateCompanion.impl
}

object GenerateCompanion {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._

    def vals(paramss: Seq[Seq[ValDef]]): Seq[ValDef] =
      paramss.flatten.map(p => {
        val name = p.name.toString
        q"val ${TermName(underscoreToCamel(name))}: String = $name"
      })

    annottees match {
      case (c@q"$_ class $tpname[..$_] $_(...$paramss) extends { ..$_ } with ..$_ { $_ => ..$_ }") :: Nil =>
        q"""
          $c
          object ${tpname.toTermName} {
            ..${vals(paramss)}
          }
          """

      case (c@q"$_ class $tpname[..$_] $_(...$paramss) extends { ..$_ } with ..$_ { $_ => ..$_ }") ::
        q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }" :: Nil =>
        q"""
           $c
           $mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
            ..$body
            ..${vals(paramss)}
           }
          """
    }
  }

  def underscoreToCamel(name: String): String = "_([a-z\\d])".r.replaceAllIn(name, _.group(1).toUpperCase)
}

Usage:

@GenerateCompanion
class Pizza(val crust_type: String, val foo_foo: Int)

object Pizza {
  def bar: String = "bar"
}

Pizza.crustType //crust_type
Pizza.fooFoo //foo_foo
Pizza.bar //bar
Towandatoward answered 27/6, 2018 at 19:25 Comment(11)
If I would want to give some attributes a manually specified name or have more attributes in the object than available in the case class would this also work? I.e. passing parameters to the annotation.Judithjuditha
I guess you can. But why do you need annotation parameters for that? You can just create desired additional members in companion object manually. Then this macro should be modified. Now it doesn't expect existing companion object and create it. The macro should modify existing companion object or create it if it doesn't exist.Towandatoward
Actually this is a good suggestion. Maybe you can update the answer accordingly.Judithjuditha
Awesome. Do I understand correct that in case the companion object does not exist a new one will be created, if one exists it will only be enriched?Judithjuditha
Yes. That's exactly what the pattern matching does.Towandatoward
scala meta example github.com/lorandszakacs/field-namesToque
@GrigorievNick It's very outdated. Current Scalameta is 4.3.7. Scalameta macro annotations existed till 1.8.0.Towandatoward
@DmytroMitin Do you know some thing better? because I can't use this macros... It does not compile even in scala 2.11Toque
@GrigorievNick What doesn't compile? field-names or @GenerateCompanion in current question? The latter should work. Just use scala-macros macros and not outdated Scalameta macros. Probably you should better create new question.Towandatoward
@DmytroMitin GenerateCompanion does not work. q"val ${TermName(underscoreToCamel(name))}: String = $name" has type Seq[c.universe.Tree]. But Seq[ValDef] required.Toque
@GrigorievNick For me it compiles in 2.11.12. IntelliJ complains about Seq[ValDef]/Seq[Tree] but scalac compiles. Do sbt clean compile.Towandatoward

© 2022 - 2024 — McMap. All rights reserved.