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)
}
}
}
@mslinn
Copy link

mslinn 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
Copy link
Author

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