I just noticed that you do not want libraries to be formless. If this is a consolation, then this is a library that will eventually replace scala macros, so you will get about the same as pure scala without reinventing the wheel.
I think I may have something that can help with this. This is kind of a tough decision, but I think he will do what you ask.
To create a static annotation, a fantastic rock scale is used ( http://www.scalameta.org ). You annotate your case class, and this built-in macro will generate the appropriate scopt parser for your command line arguments.
In your build.sbt you will need the macro paradise plugin as well as the scalameta library. You can add them to your project with.
addCompilerPlugin("org.scalameta" % "paradise" % paradise cross CrossVersion.full) libraryDependencies ++= Seq( "org.scalameta" %% "scalameta" % meta % Provided, )
After you add these depilations to your assembly, you will have to create a separate project for your macros.
A full SBT project definition will 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 called "macros", then create a class, and here is a 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, your main module will depend on your macro module. Then you can annotate your case classes like this ...
@Opts case class Options(name: String, job: String, age: Int, netWorth: Double, job_title: String)
Then at compile time, expand your case class to include scopt definitions. Here's what the generated class looks like on top.
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 a lot of boiler plate, and for those who have more knowledge about built-in macros, feel free to tell me how I could write it better, since I'm not an expert in this.
You can find the appropriate tutorial and documentation on this at http://scalameta.org/tutorial/#Macroannotations I will also be happy to answer any questions you may have with this approach!