Last active
December 15, 2015 00:39
-
-
Save danieldietrich/5174348 to your computer and use it in GitHub Desktop.
Align cascaded String when using Scala 2.10's String interpolation.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
<-- | |
""" + "#") | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
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
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...