Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option for Scala case class, and easy configuration of other renderers. #98

Merged
merged 1 commit into from
Apr 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/scala/sbtbuildinfo/BuildInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ object BuildInfo {

def makeFile(file: File): File = {
val values = results(keys, options, proj, state)
val lines = renderer.header ++ renderer.renderKeys(values) ++ renderer.footer
val lines = renderer.renderKeys(values)
IO.writeLines(file, lines, IO.utf8)
file
}
Expand Down
1 change: 1 addition & 0 deletions src/main/scala/sbtbuildinfo/BuildInfoKeys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Keys._
trait BuildInfoKeys {
lazy val buildInfo = taskKey[Seq[File]]("The task that generates the build info.")
lazy val buildInfoRenderer = settingKey[BuildInfoRenderer]("The renderer to use to generate the build info.")
lazy val buildInfoRenderFactory = settingKey[BuildInfoRenderer.Factory]("The renderFactory to used to build the renderer.")
lazy val buildInfoObject = settingKey[String]("The name for the generated object.")
lazy val buildInfoPackage = settingKey[String]("The name for the generated package.")
lazy val buildInfoUsePackageAsPath = settingKey[Boolean]("If true, the generated object is placed in the folder of the package instead of \"sbt-buildinfo\".")
Expand Down
5 changes: 3 additions & 2 deletions src/main/scala/sbtbuildinfo/BuildInfoPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ object BuildInfoPlugin extends sbt.AutoPlugin {
resourceGenerators ++= {
if(buildInfoRenderer.value.isResource) Seq(buildInfo.taskValue) else Nil
},
buildInfoRenderer := ScalaClassRenderer(
buildInfoRenderer := buildInfoRenderFactory.value.apply(
buildInfoOptions.value,
buildInfoPackage.value,
buildInfoObject.value)
Expand All @@ -87,6 +87,7 @@ object BuildInfoPlugin extends sbt.AutoPlugin {
buildInfoUsePackageAsPath := false,
buildInfoKeys := Seq(name, version, scalaVersion, sbtVersion),
buildInfoBuildNumber := buildNumberTask(baseDirectory.value, 1),
buildInfoOptions := Seq()
buildInfoOptions := Seq(),
buildInfoRenderFactory := ScalaCaseObjectRenderer.apply
)
}
6 changes: 4 additions & 2 deletions src/main/scala/sbtbuildinfo/BuildInfoRenderer.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package sbtbuildinfo

object BuildInfoRenderer {
type Factory = (Seq[BuildInfoOption], String, String) => BuildInfoRenderer
}

trait BuildInfoRenderer {

def fileType: BuildInfoType
def extension: String
def header: Seq[String]
def renderKeys(infoKeysNameAndValues: Seq[BuildInfoResult]): Seq[String]
def footer: Seq[String]

def isSource = fileType == BuildInfoType.Source
def isResource = fileType == BuildInfoType.Resource
Expand Down
74 changes: 74 additions & 0 deletions src/main/scala/sbtbuildinfo/ScalaCaseClassRenderer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package sbtbuildinfo

case class ScalaCaseClassRenderer(options: Seq[BuildInfoOption], pkg: String, obj: String) extends ScalaRenderer {
override def fileType = BuildInfoType.Source
override def extension = "scala"

val traitNames = options.collect{case BuildInfoOption.Traits(ts @ _*) => ts}.flatten
val objTraits = if (traitNames.isEmpty) "" else " extends " ++ traitNames.mkString(" with ")

// It is safe to add `import scala.Predef` even though we need to keep `-Ywarn-unused-import` in mind
// because we always generate code that has a reference to `String`. If the "base" generated code were to be
// changed and no longer contain a reference to `String`, we would need to remove `import scala.Predef` and
// fully qualify every reference. Note it is NOT safe to use `import scala._` because of the possibility of
// the project using `-Ywarn-unused-import` because we do not always generated references that are part of
// `scala` such as `scala.Option`.
def header = List(
s"package $pkg",
"",
"import scala.Predef._",
"",
s"/** This file was generated by sbt-buildinfo. */"
)

override def renderKeys(buildInfoResults: Seq[BuildInfoResult]) =
header ++
caseClassDefinitionBegin ++
buildInfoResults.flatMap(caseClassParameter).mkString(",\n").split("\n") ++
caseClassDefinitionEnd ++
toMapMethod(buildInfoResults) ++
caseClassEnd ++
List("") ++
caseObjectLine(buildInfoResults)

private def caseClassDefinitionBegin = List(
s"case class $obj$objTraits("
)

private def caseClassParameter(r: BuildInfoResult): Seq[String] = {
val typeDecl = getType(r.typeExpr) getOrElse "Any"

List(
s" ${r.identifier}: $typeDecl"
)
}

private def toMapMethod(results: Seq[BuildInfoResult]) =
if (options.contains(BuildInfoOption.ToMap))
results
.map(result => " \"%s\" -> %s".format(result.identifier, result.identifier))
.mkString(" def toMap: Map[String, Any] = Map[String, Any](\n", ",\n", ")")
.split("\n")
.toList ::: List("")
else Nil

private def caseClassDefinitionEnd = List(") {", "")
private def caseClassEnd = List("}")

private def caseObjectLine(buildInfoResults: Seq[BuildInfoResult]) = List(
s"case object $obj {",
s" def apply(): $obj = new $obj(${buildInfoResults.map(_.value).map(quote).mkString(",")})",
s" val get = apply()",
s" val value = apply()",
s"}"
)

def toMapLine(results: Seq[BuildInfoResult]): Seq[String] =
if (options.contains(BuildInfoOption.ToMap) || options.contains(BuildInfoOption.ToJson))
results
.map(result => " \"%s\" -> %s".format(result.identifier, result.identifier))
.mkString(" val toMap: Map[String, Any] = Map[String, Any](\n", ",\n", ")")
.split("\n")
.toList ::: List("")
else Nil
}
68 changes: 68 additions & 0 deletions src/main/scala/sbtbuildinfo/ScalaCaseObjectRenderer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package sbtbuildinfo

private[sbtbuildinfo] case class ScalaCaseObjectRenderer(options: Seq[BuildInfoOption], pkg: String, obj: String) extends ScalaRenderer {

override def fileType = BuildInfoType.Source
override def extension = "scala"
val traitNames = options.collect{case BuildInfoOption.Traits(ts @ _*) => ts}.flatten
val objTraits = if (traitNames.isEmpty) "" else " extends " ++ traitNames.mkString(" with ")

// It is safe to add `import scala.Predef` even though we need to keep `-Ywarn-unused-import` in mind
// because we always generate code that has a reference to `String`. If the "base" generated code were to be
// changed and no longer contain a reference to `String`, we would need to remove `import scala.Predef` and
// fully qualify every reference. Note it is NOT safe to use `import scala._` because of the possibility of
// the project using `-Ywarn-unused-import` because we do not always generated references that are part of
// `scala` such as `scala.Option`.
def header = List(
s"package $pkg",
"",
"import scala.Predef._",
"",
s"/** This object was generated by sbt-buildinfo. */",
s"case object $obj$objTraits {"
)

def footer = List("}")

override def renderKeys(buildInfoResults: Seq[BuildInfoResult]) =
header ++
buildInfoResults.flatMap(line) ++ Seq(toStringLines(buildInfoResults)) ++
toMapLine(buildInfoResults) ++ toJsonLine ++
footer

private def line(result: BuildInfoResult): Seq[String] = {
import result._
val typeDecl = getType(result.typeExpr) map { ": " + _ } getOrElse ""

List(
s" /** The value is ${quote(value)}. */",
s" val $identifier$typeDecl = ${quote(value)}"
)
}

def toStringLines(results: Seq[BuildInfoResult]): String = {
val idents = results.map(_.identifier)
val fmt = idents.map("%s: %%s" format _).mkString(", ")
val vars = idents.mkString(", ")
s""" override val toString: String = {
| "$fmt" format (
| $vars
| )
| }""".stripMargin
}

def toMapLine(results: Seq[BuildInfoResult]): Seq[String] =
if (options.contains(BuildInfoOption.ToMap) || options.contains(BuildInfoOption.ToJson))
results
.map(result => " \"%s\" -> %s".format(result.identifier, result.identifier))
.mkString(" val toMap: Map[String, Any] = Map[String, Any](\n", ",\n", ")")
.split("\n")
.toList ::: List("")
else Nil

def toJsonLine: Seq[String] =
if (options contains BuildInfoOption.ToJson)
List(""" val toJson: String = toMap.map(i => "\"" + i._1 + "\":\"" + i._2 + "\"").mkString("{", ", ", "}")""")
else Nil

}
120 changes: 0 additions & 120 deletions src/main/scala/sbtbuildinfo/ScalaClassRenderer.scala

This file was deleted.

63 changes: 63 additions & 0 deletions src/main/scala/sbtbuildinfo/ScalaRenderer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package sbtbuildinfo

/**
* Created by akos on 1/6/17.
*/
abstract class ScalaRenderer extends BuildInfoRenderer {
protected def getType(typeExpr: TypeExpression): Option[String] = {
def tpeToReturnType(tpe: TypeExpression): Option[String] =
tpe match {
case TypeExpression("Any", Nil) => None
case TypeExpression("Int", Nil) => Some("scala.Int")
case TypeExpression("Long", Nil) => Some("scala.Long")
case TypeExpression("Double", Nil) => Some("scala.Double")
case TypeExpression("Boolean", Nil) => Some("scala.Boolean")
case TypeExpression("scala.Symbol", Nil) => Some("scala.Symbol")
case TypeExpression("java.lang.String", Nil) => Some("String")
case TypeExpression("java.net.URL", Nil) => Some("java.net.URL")
case TypeExpression("sbt.URL", Nil) => Some("java.net.URL")
case TypeExpression("java.io.File", Nil) => Some("java.io.File")
case TypeExpression("sbt.File", Nil) => Some("java.io.File")
case TypeExpression("scala.xml.NodeSeq", Nil) => Some("scala.xml.NodeSeq")

case TypeExpression("sbt.ModuleID", Nil) => Some("String")
case TypeExpression("sbt.Resolver", Nil) => Some("String")

case TypeExpression("scala.Option", Seq(arg)) =>
tpeToReturnType(arg) map { x => s"scala.Option[$x]" }
case TypeExpression("scala.collection.Seq", Seq(arg)) =>
tpeToReturnType(arg) map { x => s"scala.collection.Seq[$x]" }
case TypeExpression("scala.collection.immutable.Map", Seq(arg0, arg1)) =>
for {
x0 <- tpeToReturnType(arg0)
x1 <- tpeToReturnType(arg1)
} yield s"Map[$x0, $x1]"
case TypeExpression("scala.Tuple2", Seq(arg0, arg1)) =>
for {
x0 <- tpeToReturnType(arg0)
x1 <- tpeToReturnType(arg1)
} yield s"($x0, $x1)"
case _ => None
}
tpeToReturnType(typeExpr)
}

protected def quote(v: Any): String = v match {
case x @ ( _: Int | _: Double | _: Boolean | _: Symbol) => x.toString
case x: Long => x.toString + "L"
case node: scala.xml.NodeSeq if node.toString().trim.nonEmpty => node.toString()
case (k, _v) => "(%s -> %s)" format(quote(k), quote(_v))
case mp: Map[_, _] => mp.toList.map(quote(_)).mkString("Map(", ", ", ")")
case seq: Seq[_] => seq.map(quote).mkString("scala.collection.Seq(", ", ", ")")
case op: Option[_] => op map { x => "scala.Some(" + quote(x) + ")" } getOrElse {"scala.None"}
case url: java.net.URL => "new java.net.URL(%s)" format quote(url.toString)
case file: java.io.File => "new java.io.File(%s)" format quote(file.toString)
case s => "\"%s\"" format encodeStringLiteral(s.toString)
}

protected def encodeStringLiteral(str: String): String =
str.replace("\\","\\\\").replace("\n","\\n").replace("\b","\\b").replace("\r","\\r").
replace("\t","\\t").replace("\'","\\'").replace("\f","\\f").replace("\"","\\\"")


}