aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohannes Rudolph <johannes_rudolph@gmx.de>2012-10-10 12:29:01 +0200
committerJohannes Rudolph <johannes_rudolph@gmx.de>2012-10-10 12:29:01 +0200
commit3fb580276af6bc12861de42c0115c04549c98177 (patch)
treeac358d8d558474c965020572196897e3a3e37b98
parent7ccabd803ca26cde46c970c7fea45d59b7c54b3d (diff)
downloadsbt-boilerplate-3fb580276af6bc12861de42c0115c04549c98177.tar.gz
sbt-boilerplate-3fb580276af6bc12861de42c0115c04549c98177.tar.bz2
sbt-boilerplate-3fb580276af6bc12861de42c0115c04549c98177.zip
simplified syntax and provide a proper parser
-rw-r--r--README.md87
-rw-r--r--src/main/scala/cc/spray/boilerplate/BoilerplatePlugin.scala27
-rw-r--r--src/main/scala/cc/spray/boilerplate/Generator.scala89
-rw-r--r--src/main/scala/cc/spray/boilerplate/TemplateParser.scala50
4 files changed, 183 insertions, 70 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3a669b9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,87 @@
+# sbt-boilerplate
+
+Boilerplate is an sbt-plugin that generates stubs for code which has to be expanded
+for all numbers of arguments from 1 to 22. This is sometimes necessary to support
+all of the `TupleX` or `FunctionX` generically.
+
+The plugin defines a simple template language for just this purpose.
+
+## The template language
+
+The template file contains mostly literal code with some control characters guiding the
+expansion. Expansion follows these rules:
+
+ - There's a current number of arguments `i` which is initialized to 22.
+ - Code embraced in `[#` and `#]` is copied `i` times and the expansion is applied
+ recursively with `i` being set accordingly. It is possible to define a custom separator
+ between the instances by putting the separator text between the `#` and the `]` of the closing
+ bracket. If no separator is supplied `", "` is assumed.
+ - Digit `1` is replaced by `i` and digit `0` is replaced by `i - 1` unless the digit is
+ prefixed with `##`
+
+## Examples
+
+### Apply function to tuple
+
+Consider the task is to provide overloads for a function which can apply a function to
+a tuple for all numbers of arguments from 1 to 22.
+
+Start by writing out the function for only one argument:
+
+ def applyFunc[P1, R](input: Tuple1[P1], func: (P1) => R): R =
+ func(input._1)
+
+For the function to be copied for each possible number of arguments, enclose it in `[#`
+and `#]` (the newline between the closing `#` and `]` defines that instances should be
+separated by newline and not by the default `, `):
+
+ [#def applyFunc[P1, R](input: Tuple1[P1], func: (P1) => R): R =
+ func(input._1)#
+ ]
+
+This will already expand to:
+
+ def applyFunc[P1, R](input: Tuple1[P1], func: (P1) => R): R =
+ func(input._1)
+ def applyFunc[P2, R](input: Tuple2[P2], func: (P2) => R): R =
+ func(input._2)
+ def applyFunc[P3, R](input: Tuple3[P3], func: (P3) => R): R =
+ func(input._3)
+ def applyFunc[P4, R](input: Tuple4[P4], func: (P4) => R): R =
+ func(input._4)
+ // ...
+
+This is not yet what we want, because we `P1` to expand to
+`P1, P2, ..., Pi` and `input._1` to `input._1, input._2, ..., input._i`. So we embrace the
+parts to expand another time:
+
+ [#def applyFunc[[#P1#], R](input: Tuple1[[#P1#]], func: ([#P1#]) => R): R =
+ func([#input._1#])#
+ ]
+
+This now expands correctly to
+
+ def applyFunc[P1, R](input: Tuple1[P1], func: (P1) => R): R =
+ func(input._1)
+ def applyFunc[P1, P2, R](input: Tuple2[P1, P2], func: (P1, P2) => R): R =
+ func(input._1, input._2)
+ def applyFunc[P1, P2, P3, R](input: Tuple3[P1, P2, P3], func: (P1, P2, P3) => R): R =
+ func(input._1, input._2, input._3)
+ def applyFunc[P1, P2, P3, P4, R](input: Tuple4[P1, P2, P3, P4], func: (P1, P2, P3, P4) => R): R =
+ func(input._1, input._2, input._3, input._4)
+
+## Usage
+
+Put
+
+ addSbtPlugin("cc.spray" % "sbt-boilerplate" % "0.5.0-SNAPSHOT")
+
+into your `plugins.sbt` and add
+
+ Boilerplate.settings
+
+to your `build.sbt`.
+
+The templates have to be put into the `src/main/boilerplate` directory and the file name
+must end with `.template`. The generated files will be put into the same hierarchy as they
+appear in `src/main/boilerplate` with the `.template` extension stripped off. \ No newline at end of file
diff --git a/src/main/scala/cc/spray/boilerplate/BoilerplatePlugin.scala b/src/main/scala/cc/spray/boilerplate/BoilerplatePlugin.scala
index 0d8b9c3..df50691 100644
--- a/src/main/scala/cc/spray/boilerplate/BoilerplatePlugin.scala
+++ b/src/main/scala/cc/spray/boilerplate/BoilerplatePlugin.scala
@@ -12,7 +12,7 @@ object BoilerplatePlugin extends Plugin {
target in boilerplateGenerate <<= (sourceManaged in Compile),
- boilerplateGenerate <<= (streams, sourceDirectory in boilerplateGenerate, target in boilerplateGenerate) map Generator.generateFromTemplates,
+ boilerplateGenerate <<= (streams, sourceDirectory in boilerplateGenerate, target in boilerplateGenerate) map generateFromTemplates,
(sourceGenerators in Compile) <+= boilerplateGenerate,
(managedSourceDirectories in Compile) <+= target in boilerplateGenerate,
@@ -33,5 +33,30 @@ object BoilerplatePlugin extends Plugin {
watchSources <++= (sourceDirKey, filterKey, excludeKey) map descendents
def descendents(sourceDir: File, filt: FileFilter, excl: FileFilter) =
sourceDir.descendantsExcept(filt, excl).get
+
+ def generateFromTemplates(streams: TaskStreams, sourceDir: File, targetDir: File): Seq[File] = {
+ val files = sourceDir ** "*.template"
+
+ def changeExtension(f: File): File = {
+ val (ext, name) = f.getName.reverse.span(_ != '.')
+ new File(f.getParent, name.drop(1).reverse.toString)
+ }
+
+ val mapping = (files x rebase(sourceDir, targetDir)).map {
+ case (orig, target) => (orig, changeExtension(target))
+ }
+
+ mapping foreach {
+ case (templateFile, target) =>
+ if (templateFile.lastModified > target.lastModified) {
+ streams.log.info("Generating '%s'" format target.getName)
+ val template = IO.read(templateFile)
+ IO.write(target, Generator.generateFromTemplate(template, 22))
+ } else
+ streams.log.debug("Template '%s' older than target. Ignoring." format templateFile.getName)
+ }
+
+ mapping.map(_._2)
+ }
}
}
diff --git a/src/main/scala/cc/spray/boilerplate/Generator.scala b/src/main/scala/cc/spray/boilerplate/Generator.scala
index d171d7b..9a9b5ab 100644
--- a/src/main/scala/cc/spray/boilerplate/Generator.scala
+++ b/src/main/scala/cc/spray/boilerplate/Generator.scala
@@ -1,75 +1,26 @@
package cc.spray.boilerplate
-import sbt._
-import sbt.Keys._
+import util.parsing.combinator.RegexParsers
object Generator {
- def generateFromTemplates(streams: TaskStreams, sourceDir: File, targetDir: File): Seq[File] = {
- val files = sourceDir ** "*.template"
-
- def changeExtension(f: File): File = {
- val (ext, name) = f.getName.reverse.span(_ != '.')
- new File(f.getParent, name.reverse.toString+"scala")
- }
-
- val mapping = (files x rebase(sourceDir, targetDir)).map {
- case (orig, target) => (orig, changeExtension(target))
- }
-
- mapping foreach {
- case (templateFile, target) =>
- if (templateFile.lastModified > target.lastModified) {
- streams.log.info("Generating '%s'" format target.getName)
- val template = IO.read(templateFile)
- IO.write(target, generateFromTemplate(template))
- } else
- streams.log.debug("Template '%s' older than target. Ignoring." format templateFile.getName)
- }
-
- mapping.map(_._2)
- }
-
- val ExpandReplacement = """(?s:\*\{(.*)\}\*)""".r
- def generateFromTemplate(template: String): String =
- ExpandReplacement.replaceAllIn(template, { m =>
- val format = m.group(1)
- (1 to 22).map(generate(format)).mkString
- })
-
- val SimpleReplacement = """\[\{(.*)\}\]""".r
- val EnumerateReplacement = """\{\{([^}]*)\}([^}]+)?\}""".r
-
- def generate(format: String)(idx: Int): String = {
- val numChars = idx.toString.length
- def formatNum0(num: Int) = num.formatted("%0"+numChars+"d")
- def formatNumSpace(num: Int) = num.formatted("%"+numChars+"d")
-
- def replaceSimple(pattern: String) =
- SimpleReplacement.replaceAllIn(pattern, { m =>
- val pattern = m.group(1)
- replaceInPattern(pattern)(idx)
- })
-
- def replaceExpand(pattern: String) =
- EnumerateReplacement.replaceAllIn(pattern, { m =>
- val pattern = m.group(1)
- val separator = m.group(2) match {
- case null =>
- if (pattern.endsWith(", ") || pattern.contains("\n")) "" else ", "
- case sep => sep
- }
-
- (1 to idx).map(replaceInPattern(pattern)).mkString(separator)
- })
- def replaceInPattern(pattern: String)(idx: Int): String =
- // in likely identifiers replace by '04' etc.
- pattern.replaceAll("(?<!\\d)1(?!\\d)", formatNum0(idx))
- .replaceAll("(?<=\\w)(?<!\\d)0(?!\\d)", formatNum0(idx - 1))
-
- // in other places replace by ' 4' etc.
- .replaceAll("(?<!\\w)(?<!\\d)1(?!\\d)", formatNumSpace(idx))
- .replaceAll("(?<!\\w)(?<!\\d)0(?!\\d)", formatNumSpace(idx - 1))
-
- replaceExpand(replaceSimple(format))
+ def generateFromTemplate(template: String, expandTo: Int): String =
+ generate(TemplateParser.parse(template))(expandTo)
+
+ def formatNum0(num: Int) = num.formatted("%d")
+ def formatNumSpace(num: Int) = num.formatted("%d")
+ def replaceInPattern(pattern: String)(idx: Int): String =
+ // in likely identifiers replace by '04' etc.
+ pattern.replaceAll("(?<=\\w)(?<!\\d)(?<!##)1(?!\\d)", formatNum0(idx))
+ .replaceAll("(?<=\\w)(?<!\\d)(?<!##)0(?!\\d)", formatNum0(idx - 1))
+
+ // in other places replace by ' 4' etc.
+ .replaceAll("(?<!\\w)(?<!\\d)(?<!##)1(?!\\d)", formatNumSpace(idx))
+ .replaceAll("(?<!\\w)(?<!\\d)(?<!##)0(?!\\d)", formatNumSpace(idx - 1))
+
+ def generate(format: TemplateElement)(idx: Int): String = format match {
+ case Sequence(els) => els.map(e => generate(e)(idx)).mkString
+ case Expand(inner, sep) => (1 to idx).map(generate(inner)).mkString(sep)
+ case LiteralString(lit) => replaceInPattern(lit)(idx)
+ case FixedString(lit) => lit
}
}
diff --git a/src/main/scala/cc/spray/boilerplate/TemplateParser.scala b/src/main/scala/cc/spray/boilerplate/TemplateParser.scala
new file mode 100644
index 0000000..4e280a4
--- /dev/null
+++ b/src/main/scala/cc/spray/boilerplate/TemplateParser.scala
@@ -0,0 +1,50 @@
+package cc.spray.boilerplate
+
+import util.parsing.combinator.RegexParsers
+import java.lang.RuntimeException
+
+sealed trait TemplateElement
+case class Sequence(elements: Seq[TemplateElement]) extends TemplateElement
+case class LiteralString(literal: String) extends TemplateElement
+case class FixedString(literal: String) extends TemplateElement
+case class Expand(inner: TemplateElement, separator: String) extends TemplateElement
+
+object TemplateParser extends RegexParsers {
+ override type Elem = Char
+ type Tokens = TemplateElement
+ override val skipWhitespace = false
+
+ def elements: Parser[TemplateElement] = rep1(element) ^^ {
+ case one :: Nil => one
+ case several => Sequence(several)
+ }
+
+ def element: Parser[TemplateElement] = literal | fixed | expand
+
+ def literalChar: Parser[String] = """(?s:(?!\[#)(?!#[^\]]*\]).)""".r
+ def literalChars: Parser[String] = rep1(literalChar) ^^ { _.reduceLeft(_ + _) }
+
+ def literal: Parser[LiteralString] = literalChars ^^ LiteralString
+
+ def fixed: Parser[FixedString] = "##" ~> ".".r ^^ (new String(_)) ^^ FixedString
+
+ def expand: Parser[Expand] = "[#" ~> elements ~ "#" ~ separatorChars <~ "]" ^^ {
+ case els ~ x ~ sep => Expand(els, sep.getOrElse(", "))
+ }
+
+ def separatorChars: Parser[Option[String]] = rep("""[^\]]""".r) ^^ (_.reduceLeftOption(_ + _))
+
+ def parse(input:String): TemplateElement =
+ phrase(elements)(new scala.util.parsing.input.CharArrayReader(input.toCharArray)) match {
+ case Success(res,_) => res
+ case x:NoSuccess => throw new RuntimeException(x.msg)
+ }
+}
+
+object TestParser extends App {
+ def check(format: String) {
+ println(TemplateParser.parse(format))
+ }
+
+ check("[#abc ##1 # ++ ]")
+} \ No newline at end of file