Skip to content

Commit

Permalink
Merge pull request foundweekends#126 from barnesjd/dynamic-defaults
Browse files Browse the repository at this point in the history
Dynamic defaults
  • Loading branch information
Nathan Hamblen committed Feb 16, 2014
2 parents bb590ae + b968556 commit b4b659e
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 37 deletions.
16 changes: 16 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ might be referenced in the source as:
[scalasti]: http://bmc.github.com/scalasti/
[st]: http://www.stringtemplate.org/

The template fields themselves can be utilized to define the defaults
of other fields. For instance, you could build some URLs given the
user's github id:

name = URL Builder
github_id=githubber
developer_url=https://github.com/$github_id$
project_url=https://github.com/$github_id$/$name;format="norm"$

This would yield the following in interactive mode:

name [URL Builder]: my-proj
github_id [githubber]: n8han
project_url [https://github.com/n8han/my-proj]:
developer_url [https://github.com/n8han]:

The `name` field, if defined, is treated specially by giter8. It is
assumed to be the name of a project being created, so the g8 runtime
creates a directory based off that name (with spaces and capitals
Expand Down
120 changes: 89 additions & 31 deletions library/src/main/scala/g8.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,39 @@ object G8 {
import scala.util.control.Exception.allCatch
import org.clapper.scalasti.StringTemplate

/** Properties in the order they were created/defined */
type OrderedProperties = List[(String, String)]
object OrderedProperties {
val empty = List.empty[(String, String)]
}

/** G8 template properties which have been fully resolved, i.e. defaults replaced by user input, ready for insertion into template */
type ResolvedProperties = Map[String, String]
object ResolvedProperties {
val empty = Map.empty[String, String]
}

/**
* A function which will return the resolved value of a property given the properties resolved thus far.
* This is a bit more general than was needed for resolving "dynamic defaults". I did it this way so it's
* possible to have other ValueF definitions which perform arbitrary logic given previously defined properties.
*/
type ValueF = ResolvedProperties => String

/** The ValueF implementation for handling default properties. It performs formatted substitution on any properties found. */
case class DefaultValueF(default:String) extends ValueF {
override def apply(resolved:ResolvedProperties):String = new StringTemplate(default)
.setAttributes(resolved)
.registerRenderer(renderer)
.toString
}

/** Properties which have not been resolved. I.e., ValueF() has not been evaluated */
type UnresolvedProperties = List[(String, ValueF)]
object UnresolvedProperties {
val empty = List.empty[(String, ValueF)]
}

private val renderer = new StringRenderer

def apply(fromMapping: Seq[(File,String)], toPath: File, parameters: Map[String,String]): Seq[File] =
Expand Down Expand Up @@ -84,21 +117,14 @@ case class Config(
)
object G8Helpers {
import scala.util.control.Exception.catching
import G8._

val Param = """^--(\S+)=(.+)$""".r

private def applyT(fetch: File => (Map[String, String], Stream[File], File, Option[File]), isScaffolding: Boolean = false)(tmpl: File, outputFolder: File, arguments: Seq[String] = Nil, forceOverwrite: Boolean = false) = {
private def applyT(fetch: File => (UnresolvedProperties, Stream[File], File, Option[File]), isScaffolding: Boolean = false)(tmpl: File, outputFolder: File, arguments: Seq[String] = Nil, forceOverwrite: Boolean = false) = {
val (defaults, templates, templatesRoot, scaffoldsRoot) = fetch(tmpl)

val parameters = arguments.headOption.map { _ =>
(defaults /: arguments) {
case (map, Param(key, value)) if map.contains(key) =>
map + (key -> value)
case (map, Param(key, _)) =>
println("Ignoring unrecognized parameter: " + key)
map
}
}.getOrElse { interact(defaults) }
val parameters = consoleParams(defaults, arguments).getOrElse { interact(defaults) }

val base = new File(outputFolder, parameters.get("name").map(G8.normalize).getOrElse("."))

Expand Down Expand Up @@ -138,18 +164,37 @@ object G8Helpers {

val parameters = propertiesFiles.headOption.map{ f =>
val props = readProps(new FileInputStream(f))
Ls.lookup(props).right.toOption.getOrElse(props)
}.getOrElse(Map.empty)
val lookedUp = Ls.lookup(props).right.toOption.getOrElse(props)
lookedUp.map{ case (k, v) => (k, DefaultValueF(v)) }
}.getOrElse(UnresolvedProperties.empty)

val g8templates = tmpls.filter(!_.isDirectory)

(parameters, g8templates, templatesRoot, scaffoldsRoot)
}

def interact(params: Map[String, String]) = {
def consoleParams(defaults: UnresolvedProperties, arguments: Seq[String]) = {
arguments.headOption.map { _ =>
val specified = (ResolvedProperties.empty /: arguments) {
case (map, Param(key, value)) if defaults.map(_._1).contains(key) =>
map + (key -> value)
case (map, Param(key, _)) =>
println("Ignoring unrecognized parameter: " + key)
map
}

// Add anything from defaults that wasn't picked up as an argument from the console.
defaults.foldLeft(specified) { case (resolved, (k, f)) =>
if(!resolved.contains(k)) resolved + (k -> f(resolved))
else resolved
}
}
}

def interact(params: UnresolvedProperties):ResolvedProperties = {
val (desc, others) = params partition { case (k,_) => k == "description" }

desc.values.foreach { d =>
desc.foreach { d =>
@scala.annotation.tailrec
def liner(cursor: Int, rem: Iterable[String]) {
if (!rem.isEmpty) {
Expand All @@ -164,21 +209,26 @@ object G8Helpers {
}
}
println()
liner(0, d.split(" "))
liner(0, d._2(ResolvedProperties.empty).split(" "))
println("\n")
}

val fixed = Set("verbatim")
others map { case (k,v) =>
if (fixed.contains(k))
(k, v)
else {
printf("%s [%s]: ", k,v)
Console.flush() // Gotta flush for Windows console!
val in = Console.readLine().trim
(k, if (in.isEmpty) v else in)
}
}
val renderer = new StringRenderer

others.foldLeft(ResolvedProperties.empty) { case (resolved, (k,f)) =>
resolved + (
if (fixed.contains(k))
k -> f(resolved)
else {
val default = f(resolved)
printf("%s [%s]: ", k, default)
Console.flush() // Gotta flush for Windows console!
val in = Console.readLine().trim
(k, if (in.isEmpty) default else in)
}
)
}.toMap
}

private def relativize(in: File, from: File) = from.toURI().relativize(in.toURI).getPath
Expand Down Expand Up @@ -261,18 +311,26 @@ object G8Helpers {
}
}


def readProps(stm: java.io.InputStream) = {
import scala.collection.JavaConversions._
val p = new java.util.Properties
def readProps(stm: java.io.InputStream):G8.OrderedProperties = {
val p = new LinkedListProperties
p.load(stm)
stm.close()
(Map.empty[String, String] /: p.propertyNames) { (m, k) =>
m + (k.toString -> p.getProperty(k.toString))
(OrderedProperties.empty /: p.keyList) { (l, k) =>
l :+ (k, p.getProperty(k))
}
}
}

/** Hacked override of java.util.Properties for the sake of getting the properties in the order they are specified in the file */
private [giter8] class LinkedListProperties extends java.util.Properties {
var keyList = List.empty[String]

override def put(k:Object, v:Object) = {
keyList = keyList :+ k.toString
super.put(k, v)
}
}

class StringRenderer extends org.clapper.scalasti.AttributeRenderer[String] {
import G8._
def toString(value: String): String = value
Expand Down
12 changes: 8 additions & 4 deletions library/src/main/scala/ls.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,24 @@ object Ls extends JavaTokenParsers {
def unapply(value: String) =
Some(parse(spec, value)).filter { _.successful }.map { _.get }

def lookup(rawDefaults: Map[String,String])
: Either[String, Map[String,String]] = {
def lookup(rawDefaults: G8.OrderedProperties)
: Either[String, G8.OrderedProperties] = {
val lsDefaults = rawDefaults.view.collect {
case (key, Ls(library, user, repo)) =>
ls.DefaultClient {
_.Handler.latest(library, user, repo)
}.right.map { key -> _ }
}
val initial: Either[String,Map[String,String]] = Right(rawDefaults)
val initial: Either[String,G8.OrderedProperties] = Right(rawDefaults)
(initial /: lsDefaults) { (accumEither, lsEither) =>
for {
cur <- accumEither.right
ls <- lsEither.right
} yield cur + ls
} yield {
// Find the match in the accumulator and replace it with the ls'd value
val (inits, tail) = cur.span { case (k, _) => k != ls._1 }
inits ++ (ls +: (tail.tail))
}
}.left.map { "Error retrieving ls version info: " + _ }
}
}
4 changes: 2 additions & 2 deletions plugin/src/main/scala/giterate-plugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ object Plugin extends sbt.Plugin {
outputPath in g8 <<= (target) { dir => dir / "g8" },
propertiesFile in g8 <<= (unmanagedSourceDirectories in g8) { dirs => (dirs / "default.properties").get.head },
properties in g8 <<= (propertiesFile in g8) map { f =>
Ls.lookup(GIO.readProps(new java.io.FileInputStream(f))).fold(
Ls.lookup(G8Helpers.readProps(new java.io.FileInputStream(f))).fold(
err => sys.error(err),
identity
_.toMap
)
}
)
Expand Down

0 comments on commit b4b659e

Please sign in to comment.