-
-
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