-
-
Save danieldietrich/5174348 to your computer and use it in GitHub Desktop.
| object SimpleTest extends App { | |
| import StringContextImplicits._ | |
| val list = List("Foo", "Bar", "Baz").mkString("\n") | |
| /*----------- | |
| # | |
| test --> | |
| Foo | |
| Bar | |
| Baz | |
| <-- | |
| # | |
| -----------*/ | |
| println("#" + s""" | |
| test --> | |
| $list | |
| <-- | |
| """ + "#") | |
| /*----------- | |
| #test --> | |
| Foo | |
| Bar | |
| Baz | |
| <--# | |
| -----------*/ | |
| println("#" + xs""" | |
| test --> | |
| $list | |
| <-- | |
| """ + "#") | |
| } |
| object Test extends App { | |
| val output = gen("Person", Seq("name" -> 'String, "age" -> 'Int)) | |
| println(output) | |
| // -- generator -- | |
| import StringContextImplicits._ | |
| def gen(name: String, params: Seq[(String,Symbol)]) = xs""" | |
| package model | |
| case class $name(${genParams(params)}, id: Option[Long] = None) | |
| trait ${name}Component { self: Profile => | |
| import driver.simple._ | |
| import Database.threadLocalSession | |
| object $name extends Table[$name]("${name.toUpperCase}") { | |
| def id = column[Long]("ID", O.PrimaryKey, O.AutoInc) | |
| ${genColumns(params)} | |
| def * = ${params.map(_._1).mkString(" ~ ")} ~ id.? <> ($name, $name.unapply) | |
| def delete(id: Long) = db withSession { | |
| Query(this).where(_.id is id).delete | |
| } | |
| def findById(id: Long) = db withSession { | |
| Query(this).where(_.id is id).firstOption | |
| } | |
| def save(${name.toLowerCase}: $name) = db withSession { | |
| ${name.toLowerCase}.id.fold { | |
| this.insert(${name.toLowerCase}) | |
| }{ id => | |
| Query(this).where(_.id is id).update(${name.toLowerCase}) | |
| } | |
| } | |
| } | |
| } | |
| """ | |
| def genParams(params: Seq[(String,Symbol)]) = params map { | |
| case (name, _type) => name + ": " + _type.name | |
| } mkString(", ") | |
| def genColumns(params: Seq[(String,Symbol)]) = params map { | |
| case (name, _type) => xs"""def ${name.toLowerCase} = column[${_type.name}]("${name.toUpperCase}")""" | |
| } mkString("\n") | |
| } |
| package model | |
| case class Person(name: String, age: Int, id: Option[Long] = None) | |
| trait PersonComponent { self: Profile => | |
| import driver.simple._ | |
| import Database.threadLocalSession | |
| object Person extends Table[Person]("PERSON") { | |
| def id = column[Long]("ID", O.PrimaryKey, O.AutoInc) | |
| def name = column[String]("NAME") | |
| def age = column[Int]("AGE") | |
| def * = name ~ age ~ id.? <> (Person, Person.unapply) | |
| def delete(id: Long) = db withSession { | |
| Query(this).where(_.id is id).delete | |
| } | |
| def findById(id: Long) = db withSession { | |
| Query(this).where(_.id is id).firstOption | |
| } | |
| def save(person: Person) = db withSession { | |
| person.id.fold { | |
| this.insert(person) | |
| }{ id => | |
| Query(this).where(_.id is id).update(person) | |
| } | |
| } | |
| } | |
| } |
| import scala.util.Properties.lineSeparator | |
| /** | |
| * Align cascaded Strings when using String interpolation. | |
| * xs is an extension of StringContext.s. | |
| * xraw is an extension of StringContext.raw. | |
| */ | |
| object StringContextImplicits { | |
| implicit class StringContextExtension(sc: StringContext) { | |
| def xs(args: Any*): String = align(sc.s, args) | |
| def xraw(args: Any*): String = align(sc.raw, args) | |
| /** | |
| * Indenting a rich string, removing first and last newline. | |
| * A rich string consists of arguments surrounded by text parts. | |
| */ | |
| private def align(interpolator: Seq[Any] => String, args: Seq[Any]) = { | |
| // indent embedded strings, invariant: parts.length = args.length + 1 | |
| val indentedArgs = for { | |
| (part, arg) <- sc.parts zip args.map(s => if (s == null) "" else s.toString) | |
| } yield { | |
| // get the leading space of last line of current part | |
| val space = """([ \t]*)[^\s]*$""".r.findFirstMatchIn(part).map(_.group(1)).getOrElse("") | |
| // add this leading space to each line (except the first) of current arg | |
| arg.split("\r?\n") match { | |
| case lines: Array[String] if lines.length > 0 => lines reduce (_ + lineSeparator + space + _) | |
| case whitespace => whitespace mkString "" | |
| } | |
| } | |
| // remove first and last newline and split string into separate lines | |
| // adding termination symbol \u0000 in order to preserve empty strings between last newlines when splitting | |
| val split = (interpolator(indentedArgs).replaceAll( """(^[ \t]*\r?\n)|(\r?\n[ \t]*$)""", "") + '\u0000').split("\r?\n") | |
| // find smallest indentation | |
| val prefix = split filter (!_.trim().isEmpty) map { s => | |
| """^\s+""".r.findFirstIn(s).getOrElse("") | |
| } match { | |
| case prefixes: Array[String] if prefixes.length > 0 => prefixes reduce { (s1, s2) => | |
| if (s1.length <= s2.length) s1 else s2 | |
| } | |
| case _ => "" | |
| } | |
| // align all lines | |
| val aligned = split map { s => | |
| if (s.startsWith(prefix)) s.substring(prefix.length) else s | |
| } mkString lineSeparator dropRight 1 // dropping termination character \u0000 | |
| // combine multiple newlines to two | |
| aligned.replaceAll("""[ \t]*\r?\n ([ \t]*\r?\n)+""", lineSeparator * 2) | |
| } | |
| } | |
| } |
Thank you for your feedback, I really appreciate it. I will create a git project with a sample within the next days. The code tackles a feature I know from the Xtend language, which I used to write code generators based on String templates. Scala has all the nice language features to write internal DSLs. The new String interpolation gives us a template language out-of-the-box. This snippet adds auto-aligning (using the given indentation) of cascaded Strings. Perhaps it is useful for someone else...
Added a better sample. The lines returned by genColumn() are aligned correctly.
Compared to full-blown template languages like the one of Play framework, String interpolation does not have code-blocks containing plain-text lines. Instead of mixing code and text, other functions are called which embed an interpolated String within a specific control structure. Example: genColumns()
Interesting! This is cool. The code deserves more explanation, especially some use cases so other can understand more readily how they could use this facility. If this code was made into a git project then it could have issue tracking, wiki pages, etc.
Mike