Search code examples
scalabuilderscala-macrosscala-macro-paradisescalameta

Scala macro to auto generate fluent builders


I am interacting with an external Java API which looks like this:

val obj: SomeBigJavaObj = {
  val _obj = new SomeBigJavaObj(p1, p2)
  _obj.setFoo(p3)
  _obj.setBar(p4)
  val somethingElse = {
    val _obj2 = new SomethingElse(p5)
    _obj2.setBar(p6)
    _obj2
   }
  _obj.setSomethingElse(somethingElse)
  _obj
}

Basically the Java API exposes bunch of .setXXXX methods which returns void and sets something. I have no control over these external POJOs.

I would therefore like to write a fluent build Scala macro which inspects the object and creates a builder-pattern type .withXXXX() method for each of the void setXXXX() methods which returns this:

val obj: SomeBigJavaObj =
  build(new SomeBigJavaObj(p1, p2))
    .withFoo(p3)
    .withBar(p4)
    .withSomethingElse(
       build(new SomethingElse(p5))
         .withBar(p6)
         .result()
    )
    .result()

Is this possible? I know I cannot generate new top level objects with def macros so open to other suggestions where I would have the similar ergonomics.


Solution

  • It is not complicated to use macros; just unfriendly to IDE (like:code completion;...);

    //edit 1 : support multiple arguments

    entity:

    public class Hello {
      public int    a;
      public String b;
    
    
      public void setA(int a) {
        this.a = a;
      }
    
      public void setB(String b) {
        this.b = b;
      }
    
      public void setAB(int a , String b){
        this.a = a;
        this.b = b;
      }
    }
    

    macro code : import scala.language.experimental.macros import scala.reflect.macros.whitebox

    trait BuildWrap[T] {
      def result(): T
    }
    
    object BuildWrap {
      def build[T](t: T): Any = macro BuildWrapImpl.impl[T]
    }
    
    class BuildWrapImpl(val c: whitebox.Context) {
    
      import c.universe._
    
      def impl[T: c.WeakTypeTag](t: c.Expr[T]) = {
        val tpe = c.weakTypeOf[T]
        //get all set member
        val setMembers = tpe.members
          .filter(_.isMethod)
          .filter(_.name.toString.startsWith("set"))
          .map(_.asMethod)
          .toList
        // temp value ;
        val valueName = TermName("valueName")
    
        val buildMethods = setMembers.map { member =>
          if (member.paramLists.length > 1)
            c.abort(c.enclosingPosition,"do not support Currying")
    
          val params = member.paramLists.head
          val paramsDef = params.map(e=>q"${e.name.toTermName} : ${e.typeSignature}")
          val paramsName = params.map(_.name)
    
          val fieldName = member.name.toString.drop(3)//drop set
          val buildFuncName = TermName(s"with$fieldName")
          q"def $buildFuncName(..$paramsDef ) = {$valueName.${member.name}(..$paramsName);this} "
        }
    
    
        val result =
          q"""new BuildWrap[$tpe] {
            private val $valueName = $t
            ..${buildMethods}
            def result() = $valueName
    
           }"""
    
        // debug
        println(showCode(result))
        result
      }
    }
    

    test code :

    val hello1: Hello = BuildWrap.build(new Hello).withA(1).withB("b").result()
    assert(hello1.a == 1)
    assert(hello1.b == "b")
    
    val hello2: Hello = BuildWrap.build(new Hello).withAB(1, "b").result()
    assert(hello2.a == 1)
    assert(hello2.b == "b")