Reputation: 5685
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
Upvotes: 5
Views: 1319
Reputation: 617
Here is a version implemented with runtime reflection only. While it's less elegant than a macro based solution, it only requires the scala-reflect.jar:
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
Code:
import scala.collection.mutable
import scala.reflect.runtime.universe._
def genericParser[T: TypeTag](programName: String): OptionParser[T] = new OptionParser[T](programName) {
val StringTpe: Type = typeOf[String]
val fields: List[MethodSymbol] = typeOf[T].decls.sorted.collect {
case m: MethodSymbol if m.isCaseAccessor ⇒ m
}
val values = mutable.Map.empty[TermName, Any]
/**
* Returns an instance of a [[scopt.Read]] corresponding to the provided type
*/
def typeToRead(tpe: Type): Read[Any] = (tpe match {
case definitions.IntTpe ⇒ implicitly[Read[Int]]
case StringTpe ⇒ implicitly[Read[String]]
// Add more types if necessary...
}) map identity[Any]
for (f ← fields) {
// kind of dynamic implicit resolution
implicit val read: Read[Any] = typeToRead(f.returnType)
opt[Any](f.name.toString) required() valueName s"<${f.name}>" foreach { value ⇒
values(f.name) = value
} text s"${f.name} is required"
}
override def parse(args: Seq[String], init: T): Option[T] = {
super.parse(args, init) map { _ ⇒
val classMirror = typeTag[T].mirror.reflectClass(typeOf[T].typeSymbol.asClass)
val constructor = typeOf[T].decl(termNames.CONSTRUCTOR).asMethod
val constructorMirror = classMirror.reflectConstructor(constructor)
val constructorArgs = constructor.paramLists.flatten.map(symbol ⇒ values(symbol.asTerm.name))
constructorMirror(constructorArgs: _*).asInstanceOf[T]
}
}
}
Example usage:
case class A(f1: String, f2: Int)
println(genericParser[A]("main").parse(args, A("", -1)))
A few things to take in consideration:
copy
method is not involved).parse
method is not used at all (but it should not matter since all arguments are required).String
and Int
(See the Add more types if necessary... comment).Upvotes: 2
Reputation: 997
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!
Upvotes: 3