Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Align cascaded String when using Scala 2.10's String interpolation.
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)
}
}
}
@mslinn

This comment has been minimized.

Copy link

commented Mar 16, 2013

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

@danieldietrich

This comment has been minimized.

Copy link
Owner Author

commented Mar 16, 2013

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

@danieldietrich

This comment has been minimized.

Copy link
Owner Author

commented Mar 16, 2013

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.