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
Georg Heiler
  • 16,916
  • 36
  • 162
  • 292

1 Answers1

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
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • 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. – Georg Heiler Jun 28 '18 at 06:11
  • 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. – Dmytro Mitin Jun 28 '18 at 07:18
  • Actually this is a good suggestion. Maybe you can update the answer accordingly. – Georg Heiler Jun 28 '18 at 07:51
  • 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? – Georg Heiler Jun 28 '18 at 08:22
  • Yes. That's exactly what the pattern matching does. – Dmytro Mitin Jun 28 '18 at 08:23
  • scala meta example https://github.com/lorandszakacs/field-names – Grigoriev Nick Apr 02 '20 at 16:45
  • @GrigorievNick It's very outdated. Current Scalameta is 4.3.7. Scalameta macro annotations existed till 1.8.0. – Dmytro Mitin Apr 02 '20 at 16:49
  • @DmytroMitin Do you know some thing better? because I can't use this macros... It does not compile even in scala 2.11 – Grigoriev Nick Apr 02 '20 at 17:08
  • @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. – Dmytro Mitin Apr 02 '20 at 17:14
  • @DmytroMitin GenerateCompanion does not work. `q"val ${TermName(underscoreToCamel(name))}: String = $name"` has type `Seq[c.universe.Tree]`. But `Seq[ValDef]` required. – Grigoriev Nick Apr 02 '20 at 17:16
  • 1
    @GrigorievNick For me it compiles in 2.11.12. IntelliJ complains about `Seq[ValDef]`/`Seq[Tree]` but scalac compiles. Do `sbt clean compile.` – Dmytro Mitin Apr 02 '20 at 17:31