Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to parse generic case class fields using scala option parser?

I have a case class includes around 20 fields , all of which are primitive types.

case class A( f1: String, f2: Int .....)

and I have to parse these fields all from command line (unfortunately). I can , but I really don't want to write this 20 times

opt[String]("f1") required() valueName "<f1>" action { (x, c) =>
    c.copy(f1 = x)
  } text "f1 is required"
//...repeat 20 times

I can obtain the field name and filed type through reflection, but I have no idea how to stuck those information to this call within a for loop

I can connect this with shapeless but I'm still not familiar with that and can this be done without shapeless ?

==

scala option parser => scopt

like image 615
zinking Avatar asked Aug 11 '17 08:08

zinking


1 Answers

I just noticed you wanted no libraries like shapeless. If it's any consolation this is a library that will replace scala reflect macros eventually, so it's about as close as pure scala you'll get without reinventing the wheel.

I think I may have something that might help with this. It's a kind of heavy solution but I think it will do what you are asking.

This uses the fantastic scalameta (http://www.scalameta.org) library in order to create a static annotation. You will annotate your case class and this inline macro will then generate the appropriate scopt parser for your command line args.

Your build.sbt is going to need the macro paradise plugin as well as the scalameta library. You can add these to your project with.

addCompilerPlugin("org.scalameta" % "paradise" % paradise cross CrossVersion.full)
libraryDependencies ++= Seq(
    "org.scalameta" %% "scalameta" % meta % Provided,
)

Once you have added those deps to your build you will have to create a separate project for you macros.

A complete SBT project definition would look like

lazy val macros = project
  .in(file("macros"))
  .settings(
    addCompilerPlugin("org.scalameta" % "paradise" % paradise cross CrossVersion.full),
    libraryDependencies ++= Seq(
      "org.scalameta" %% "scalameta" % "1.8.0" % Provided,
    )
   )

If the module itself is named "macros", then create a class and here is the static annotation.

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.meta._

@compileTimeOnly("@Opts not expanded")
class Opts extends StaticAnnotation {
  inline def apply(defn: Any): Any = meta {
    defn match {
      case q"..$mods class $tname[..$tparams] ..$ctorMods (...$paramss) extends $template" =>
        val opttpe = Type.Name(tname.value)
        val optName = Lit.String(tname.value)
        val opts = paramss.flatten.map {
          case param"..${_} $name: ${tpeopt: Option[Type]} = $expropt" =>
            val tpe = Type.Name(tpeopt.get.toString())
            val litName = Lit.String(name.toString())
            val errMsg = Lit.String(s"${litName.value} is required.")
            val tname = Term.Name(name.toString())
            val targ = Term.Arg.Named(tname, q"x")
            q"""
                opt[$tpe]($litName)
                  .required()
                  .action((x, c) => c.copy($targ))
                  .text($errMsg)
            """
        }
        val stats = template.stats.getOrElse(Nil) :+ q"def options: OptionParser[$opttpe] = new OptionParser[$opttpe]($optName){ ..$opts }"
        q"""..$mods class $tname[..$tparams] ..$ctorMods (...$paramss) {
            import scopt._
            ..$stats
        }"""
    }
  }
}

After that you will make your main module depend on your macros module. Then you can annotate your case classes like so...

@Opts
case class Options(name: String, job: String, age: Int, netWorth: Double, job_title: String)

This will then at compile time expand your case class to include the scopt definitions. Here is what a generated class looks like from above.

case class Options(name: String, job: String, age: Int, netWorth: Double, job_title: String) {
  import scopt._

  def options: OptionParser[Options] = new OptionParser[Options]("Options") {
    opt[String]("name").required().action((x, c) => c.copy(name = x)).text("name is required.")
    opt[String]("job").required().action((x, c) => c.copy(job = x)).text("job is required.")
    opt[Int]("age").required().action((x, c) => c.copy(age = x)).text("age is required.")
    opt[Double]("netWorth").required().action((x, c) => c.copy(netWorth = x)).text("netWorth is required.")
    opt[String]("job_title").required().action((x, c) => c.copy(job_title = x)).text("job_title is required.")
  }
}

This should save you a ton of boiler plate, and for anyone with more knowledge of inline macros please feel free to tell me how I could write this better, since I am not an expert on this.

You can find the appropriate tutorial and documentation about this at http://scalameta.org/tutorial/#Macroannotations I'm also happy to answer any questions you might have about this approach!

like image 50
Stephen Carman Avatar answered Oct 03 '22 06:10

Stephen Carman