Permalink
Browse files

Usability improvements

- Arguments now have a descriptive name used when asking the user for
  the value to use (required changes to .json. Much simpler and DRY now)
- Messages printed to the SBT shell  are generally prettier and nicer
  • Loading branch information...
mads-hartmann committed Dec 7, 2011
1 parent c44d7a8 commit 771939683401cfac4896978198a68229356a0130
View
@@ -20,8 +20,6 @@ libraryDependencies += "org.scalaz" %% "scalaz-core" % "6.0.3"
libraryDependencies += "jline" % "jline" % "0.9.94"
libraryDependencies += "org.fusesource.scalate" % "scalate-core" % "1.5.3"
pomPostProcess := {
import xml._
Rewrite.rewriter {
@@ -1,17 +1,5 @@
package org.lifty.engine
/*
The following are the data classes that are used internally
*/
trait Command { val keyword: String }
object HelpCommand extends Command { val keyword = "help"}
object CreateCommand extends Command { val keyword = "create"}
object TemplatesCommand extends Command { val keyword = "templates"}
object UpdateTemplatesCommand extends Command { val keyword = "update"}
object LearnCommand extends Command { val keyword = "learn" }
object RecipesCommand extends Command { val keyword = "recipes" }
object DeleteCommand extends Command { val keyword = "delete" }
case class Error(message: String)
@@ -26,17 +14,21 @@ case class Description(
origin: String,
version: Int,
templates: List[Template],
arguments: List[Argument],
sources: List[Source]) {
def templateNamed(name: String): Option[Template] = {
templates.find( _.name == name)
}
def argumentNamed(name: String): Option[Argument] = {
arguments.find( _.name == name)
}
def allArguments(template: Template): List[Argument] = {
val directArguments = template.arguments
val transitive = dependenciesOfTemplate(template).flatMap( _.arguments)
(directArguments ::: transitive).distinct
(directArguments ::: transitive).distinct.flatMap( (arg: String) => arguments.find( _.name == arg ) )
}
def allFolders(template: Template): List[String] = {
@@ -63,7 +55,7 @@ case class Template(
description: String,
notice: Option[String],
folders: Option[List[String]],
arguments: List[Argument],
arguments: List[String],
files: List[TemplateFile],
injections: List[TemplateInjection],
dependencies: List[String])
@@ -79,9 +71,9 @@ case class TemplateFile(
)
case class Argument(
name: String,
default: Option[String],
repeatable: Option[Boolean])
name: String,
descriptiveName: String,
default: Option[String])
case class TemplateInjection(
file: String,
@@ -5,7 +5,6 @@ import java.net.{ URL, URI }
import scalaz._
import scalaz.effects._
import Scalaz._
import org.fusesource.scalate.{ TemplateEngine }
object LiftyEngine {
@@ -105,11 +104,10 @@ object LiftyEngine {
}
def learnMsg(name: String, url: String, recipe: Recipe) = {
"Lifty successfully installed recipe with name '%s'. \n".format(name) +
"Lifty successfully installed recipe with name: '%s'\n".format(name) +
"\n"+
"Run 'lifty templates %s' for information about the newly installed templates. \n".format(name) +
"\n" +
"Happy hacking."
"Run 'lifty templates %s' for information about\n".format(name) +
"the newly installed templates. Happy hacking."
}
/**
@@ -133,12 +131,19 @@ object LiftyEngine {
}.getOrElse("")
val value = InputReaderComponent
.requestInput("Enter value for %s%s: ".format(argument.name, argument.default.map(_=>"["+default+"]").getOrElse("")),default)
.requestInput("%s%s: ".format(argument.descriptiveName, argument.default.map(_=>" ["+default+"]").getOrElse("")),default)
.unsafePerformIO
(argument.name, value)
}
val kvs = description.allArguments(template).foldLeft(Nil: List[(String,String)]) { (xs,c) =>
val allArgs = description.allArguments(template)
if (!allArgs.isEmpty) {
println("\nHi, I just need some more information so I can\n" +
"create the templates just the way you want them\n")
}
val kvs = allArgs.foldLeft(Nil: List[(String,String)]) { (xs,c) =>
requestInputForMissingArgument(Map(xs: _*), c) :: xs
}
@@ -1,5 +1,7 @@
package org.lifty.engine
import scala.util.matching.Regex._
import org.lifty.engine.io.{ FileUtil, Storage }
import org.lifty.engine.io.FileUtil.{ readToString, writeToFile, file}
import Functional._
@@ -27,86 +29,121 @@ object TemplateRenderer {
} f.mkdirs()
// render and copy templates (in parallel, yeah!)
toRender.foreach { f => renderTemplate(f,env,description) }
toCopy.par.foreach { f => copyTemplate(f,env) }
val (rendered, copied) = (
toRender.par.map { f => renderTemplate(f,env,description) },
toCopy.par.map { f => copyTemplate(f,env) }
)
// for any injections that wasn't possible, tell to user how to continue
invalidInjections(env.template, description).foreach { templateInjection =>
for {
txtFile <- Storage.template(env.recipe, templateInjection.file).unsafePerformIO
contents <- FileUtil.readToString(txtFile).unsafePerformIO
renderedStr <- Some(renderString(contents, None, env, description))
} {
println("\nWasn't able to inject\n\n%s\n\ninto %s at %s ".format(
renderedStr,
templateInjection.into,
templateInjection.point
))
val failedRendered = rendered.filter( _.isFailure )
val failedCopied = copied.filter( _.isFailure )
if (!failedRendered.isEmpty || !failedCopied.isEmpty) {
Error((failedRendered.map( _.fold(e => e.message , s => s) ) ++
failedCopied.map( _.fold(e => e.message , s => s ))).mkString("\n")).fail
} else { // no failures, we can continue.
val invalid = invalidInjections(env.template, description)
if (!invalid.isEmpty) {
val injectionMsg: List[String] = (for {
injection <- invalid
} yield for {
txtFile <- Storage.template(env.recipe, injection.file).unsafePerformIO
contents <- FileUtil.readToString(txtFile).unsafePerformIO
renderedStr <- renderString(contents, None, env, description).toOption
} yield {
"\nWasn't able to inject the following into %s at %s\n\n%s\n\n".format(
injection.into,
injection.point,
renderedStr
)
}).flatten
(injectionMsg.mkString("\n") +
"I successfully finished, however, some of the templates I\n" +
"rendered wanted to inject code into existing files and I'm\n" +
"not allowed to do that so you have to do it manually, sorry.\n" +
"Read above to see what I wanted to inject into each file.").success
} else {
("I successfully finished." + env.template.notice.map(s => " I was asked to tell you to: \n\n" + s).getOrElse("")).success
}
}
/* TODO: MUCH BETTER ERROR HANDLING */
"Done.".success
}
private def renderString(str: String, templateOpt: Option[TemplateFile], env: Environment, description: Description): String = {
def processLine(line: String): String = {
println("processLine")
private def renderString(str: String, templateOpt: Option[TemplateFile], env: Environment, description: Description): Validation[Error,String] = {
def processLine(line: String): Validation[Error,String] = {
val variable = """\$\{(\w*)\}""".r //example: ${argumentName}
val injection = """\/{2}\#inject\spoint\:\s(\w*)""".r //example: //#inject point: dependencies
val variablesReplaced = variable.replaceAllIn(line, m => env.values(m.group(m.groupCount)))
(for { template <- templateOpt } yield {
(for {
template <- templateOpt
} yield {
val injectionsReplaced = injection.replaceAllIn(variablesReplaced, m => {
def grp(m: Match) = {
val point = m.group(m.groupCount)
injectionsForPointInFile(point, template, description, env.template).map { injection =>
Storage.template(env.recipe, injection.file).unsafePerformIO.flatMap { injectionFile =>
(for {
read <- readToString(injectionFile).unsafePerformIO
} yield processLine(read))
processed <- processLine(read).toOption
} yield processed )
}.getOrElse {
println("Tried to load " + injection.file + " but failed") // Propagate to Validation
""
throw new Exception("Tried to load " + injection.file + " but failed")
}
}.mkString("\n")
})
injectionsReplaced
}) getOrElse(variablesReplaced)
}
try {
injection.replaceAllIn(variablesReplaced, grp(_)).success
} catch {
case e: Exception => Error(e.getMessage).fail
}
}).getOrElse(variablesReplaced.success)
}
str.split("\n").map(processLine).mkString("\n")
val processed = str.split("\n").map(processLine)
val processedFailed = processed.filter(_.isFailure)
if (processedFailed.isEmpty) {
processed.flatMap(_.toOption).mkString("\n").success
} else {
Error(processedFailed.map( _.fold(e => e.message , s => s)).mkString("\n")).fail
}
}
private def renderTemplate(template: TemplateFile, env: Environment, description: Description): String = {
private def renderTemplate(template: TemplateFile, env: Environment, description: Description): Validation[Error,String] = {
val destination = replaceVariables(template.destination, env) |> properPath |> file
Storage.template(env.recipe, template.file).unsafePerformIO.flatMap { templateFile =>
for {
str <- readToString(templateFile).unsafePerformIO
rendered = renderString(str, Some(template), env, description)
rendered <- renderString(str, Some(template), env, description).toOption
result <- writeToFile(rendered, destination)
} yield "Rendered file to " + destination
} getOrElse "Wasn't able to render a file to " + destination
} yield ("Rendered file to " + destination).success
} getOrElse Error("Wasn't able to render a file to " + destination).fail
}
private def copyTemplate(template: TemplateFile, env: Environment): String = {
private def copyTemplate(template: TemplateFile, env: Environment): Validation[Error,String] = {
val destination = replaceVariables(template.destination, env) |> properPath |> file
Storage.template(env.recipe, template.file).unsafePerformIO.flatMap { templateFile =>
for {
templateStr <- readToString(templateFile).unsafePerformIO
result <- writeToFile(templateStr, destination)
} yield "Copied file " + template.file
} getOrElse "Wasn't able to copy a file to " + destination
} yield ("Copied file " + template.file).success
} getOrElse Error("Wasn't able to copy a file to " + destination).fail
}
//
@@ -60,7 +60,7 @@ object Storage {
} yield {
val templates = folder.listFiles.filter( f => f.isFile && f.getName.endsWith(".ssp"))
Recipe(name, descriptor, templates).success
}).getOrElse(Error("No recipe named %s in the storage.".format(name)).fail)
}).getOrElse(Error("Sorry, no recipe named %s in the storage.".format(name)).fail)
}
/**
@@ -114,13 +114,13 @@ object Storage {
*/
def deleteRecipe(name: String): IO[Validation[Error, String]] = io {
recipe(name).unsafePerformIO.fold(
(e) => Error("No recipe named %s installed.".format(name)).fail,
(e) => Error("Sorry, no recipe named '%s' installed.".format(name)).fail,
(s) => {
storage.listFiles
.filter( f => f.isDirectory && f.getName == name )
.headOption
.foreach( recursiveDelete )
"Removed %s from the storage".format(name).success
"Successfully removed %s from the storage".format(name).success
})
}
View
@@ -12,8 +12,6 @@ publishTo := Some("Scala Tools Nexus" at "http://nexus.scala-tools.org/content/r
credentials += Credentials(Path.userHome / "dev" / ".nexus_credentials")
libraryDependencies += "org.fusesource.scalate" % "scalate-core" % "1.5.3"
pomPostProcess := {
import xml._
Rewrite.rewriter {
@@ -36,12 +36,12 @@ object Lifty extends Plugin {
def listTuple(t: (String,String)): List[String] = List(t._1,t._2)
// data
val keywords = List("create", "templates", "learn", "delete", "upgrade", "recipes", "help")
val recipes: List[String] = Storage.allRecipes.unsafePerformIO map (_.name)
val templates: Map[String,List[String]] = (recipes map ( r => (r,Storage.templateNames(r).unsafePerformIO.toOption.get))).toMap
def keywords = List("create", "templates", "learn", "delete", "upgrade", "recipes", "help")
def recipes: List[String] = Storage.allRecipes.unsafePerformIO map (_.name)
def templates: Map[String,List[String]] = (recipes map ( r => (r,Storage.templateNames(r).unsafePerformIO.toOption.get))).toMap
// parsers
val recipe: Parser[String] = token(NotSpace.examples(recipes : _ *))
lazy val recipe: Parser[String] = token(NotSpace.examples(recipes : _ *))
def template(recipe: String): Parser[(String,String)] = {
if (templates.contains(recipe)) {
@@ -51,17 +51,17 @@ object Lifty extends Plugin {
}
}
val name: Parser[String] = NotSpace
lazy val name: Parser[String] = NotSpace
val url: String => Parser[(String,String)] = (n) => NotSpace map ( u => (n,u) )
lazy val url: String => Parser[(String,String)] = (n) => NotSpace map ( u => (n,u) )
val createParser : Parser[(String,String)] = recipe flatMap ( t => Space ~> template(t) )
val templatesParser: Parser[String] = recipe
val learnParser : Parser[(String,String)] = name flatMap ( t => Space ~> url(t) )
val deleteParser : Parser[String] = recipe
val upgradeParser : Parser[String] = recipe
lazy val createParser : Parser[(String,String)] = recipe flatMap ( t => Space ~> template(t) )
lazy val templatesParser: Parser[String] = recipe
lazy val learnParser : Parser[(String,String)] = name flatMap ( t => Space ~> url(t) )
lazy val deleteParser : Parser[String] = recipe
lazy val upgradeParser : Parser[String] = recipe
val liftyParser: Parser[(String,List[String])] = {
lazy val liftyParser: Parser[(String,List[String])] = {
Space ~>
token(NotSpace.examples(keywords : _ *)) flatMap { cmd => cmd match {
case "create" => Space ~> createParser map { p => (cmd,listTuple(p)) }
Oops, something went wrong.

0 comments on commit 7719396

Please sign in to comment.