Skip to content

Instantly share code, notes, and snippets.

@danieldietrich
Last active December 15, 2015 00:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danieldietrich/5174348 to your computer and use it in GitHub Desktop.
Save danieldietrich/5174348 to your computer and use it in GitHub Desktop.
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)
}
}
}
@danieldietrich
Copy link
Author

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