My presentation for Scala Days Amsterdam.
How to make a compile time string interpolator for a language you have? Use case and step by step code examples.
11. MongoDB in Scala
There are three main drivers for MongoDB:
Casbah – synchronous, on top of Java driver.
12. MongoDB in Scala
There are three main drivers for MongoDB:
Casbah – synchronous, on top of Java driver.
ReactiveMongo – asynchronous, built on Akka actors.
13. MongoDB in Scala
There are three main drivers for MongoDB:
Casbah – synchronous, on top of Java driver.
ReactiveMongo – asynchronous, built on Akka actors.
Tepkin – reactive, on top of Akka IO and Akka Streams.
14. How Casbah API looks like
val name = "John Doe"
people.insert(MongoDBObject(
"name" -> "James Bond",
"age" -> 80,
"phone" -> List("007007"),
"address" -> MongoDBObject("country" -> "UK")))
15. How Casbah API looks like
val name = "John Doe"
people.insert(MongoDBObject(
"name" -> "James Bond",
"age" -> 80,
"phone" -> List("007007"),
"address" -> MongoDBObject("country" -> "UK")))
val a = people.findOne(MongoDBObject("name" -> name))
val b = people.find(MongoDBObject("age" ->
MongoDBObject("$lt" -> 30)))
16. How Casbah API looks like
val name = "John Doe"
people.insert(MongoDBObject(
"name" -> "James Bond",
"age" -> 80,
"phone" -> List("007007"),
"address" -> MongoDBObject("country" -> "UK")))
val a = people.findOne(MongoDBObject("name" -> name))
val b = people.find(MongoDBObject("age" ->
MongoDBObject("$lt" -> 30)))
// Using Casbah DSL
val c = people.find("age" $lt 30)
val d = people.find("phone" -> $not(_ $size 0))
17. How Casbah API looks like
val name = "John Doe"
people.insert(MongoDBObject(
"name" -> "James Bond",
"age" -> 80,
"phone" -> List("007007"),
"address" -> MongoDBObject("country" -> "UK")))
val a = people.findOne(MongoDBObject("name" -> name))
val b = people.find(MongoDBObject("age" ->
MongoDBObject("$lt" -> 30)))
// Using Casbah DSL
val c = people.find("age" $lt 30)
val d = people.find("phone" -> $not(_ $size 0))
people.update(MongoDBObject("age" -> 42),
$set("name" -> "Ford Prefect"))
18. How Casbah API looks like
val name = "John Doe"
people.insert(MongoDBObject(
"name" -> "James Bond",
"age" -> 80,
"phone" -> List("007007"),
"address" -> MongoDBObject("country" -> "UK")))
val a = people.findOne(MongoDBObject("name" -> name))
val b = people.find(MongoDBObject("age" ->
MongoDBObject("$lt" -> 30)))
// Using Casbah DSL
val c = people.find("age" $lt 30)
val d = people.find("phone" -> $not(_ $size 0))
people.update(MongoDBObject("age" -> 42),
$set("name" -> "Ford Prefect"))
val e = people.aggregate(List(
MongoDBObject("$group" ->
MongoDBObject("_id" -> "$age", "count" ->
MongoDBObject("$sum" -> 1))),
MongoDBObject("$sort" -> MongoDBObject("count" -> -1)),
MongoDBObject("$limit" -> 5)))
22. Meet MongoQuery
Using MongoQuery with Casbah:
import com.github.limansky.mongoquery.casbah._
val name = "John Doe"
val a = people.findOne(mq"{ name : $name }")
23. Meet MongoQuery
Using MongoQuery with Casbah:
import com.github.limansky.mongoquery.casbah._
val name = "John Doe"
val a = people.findOne(mq"{ name : $name }")
val b = people.find(mq"{age : { $$lt : 30 }}")
24. Meet MongoQuery
Using MongoQuery with Casbah:
import com.github.limansky.mongoquery.casbah._
val name = "John Doe"
val a = people.findOne(mq"{ name : $name }")
val b = people.find(mq"{age : { $$lt : 30 }}")
val d = people.find(
mq"{ phone : { $$not : { $$size : 0 }}}")
people.update(mq"{ age : 42 }",
mq"{ $$set { name : 'Ford Prefect' }}")
val e = people.aggregate(List(
mq"""{ $$group :
{ _id : "$$age", count : { $$sum : 1 }}}""",
mq"{ $$sort : { count : -1 }}",
mq"{ $$limit : 5}"))
25. String interpolation
implicit class MongoHelper(val sc: StringContext)
extends AnyVal {
def mq(args: Any*): DBObject = {
Parser.parseQuery(sc.parts, args) match {
case Success(v, _) =>
createObject(v)
case NoSuccess(msg, _) =>
throw new MqException(s"Invalid object: $msg")
}
}
}
26. String interpolation
implicit class MongoHelper(val sc: StringContext)
extends AnyVal {
def mq(args: Any*): DBObject = {
Parser.parseQuery(sc.parts, args) match {
case Success(v, _) =>
createObject(v)
case NoSuccess(msg, _) =>
throw new MqException(s"Invalid object: $msg")
}
}
}
mq"{ name : $name }"
sc.parts == List("{ name: ", " }")
args = List(name)
29. Wrapping it into macro
implicit class MongoHelper(val sc: StringContext) extends AnyVal {
def mq(args: Any*): DBObject = macro MongoHelper.mq_impl
}
object MongoHelper {
def mq_impl(c: Context)(args: c.Expr[Any]*):
c.Expr[DBObject] = {
import c.universe._
val q"$cn(scala.StringContext.apply(..$pTrees))"
= c.prefix.tree
val parsed = parse(c)(pTrees)
wrapObject(c)(parsed, args.map(_.tree).iterator)
}
}
30. Wrapping it into macro
object MongoHelper {
def parse(c: Context)(pTrees: List[c.Tree]) = {
import c.universe._
val parts = pTrees map {
case Literal(Constant(s: String)) => s
}
parser.parse(parts) match {
case Success(v, _) => v
case NoSuccess(msg, reader) =>
val partIndex = reader.asInstanceOf[PartReader].part
val pos = pTrees(partIndex).pos
c.abort(pos.withPoint(pos.point + reader.offset)),
s"Invalid BSON object: $msg")
}
}
}
41. mqt – typechecking interpolator
case class Phone(kind: String, number: String)
case class Person(name: String, age: Int, phone: List[Phone])
42. mqt – typechecking interpolator
case class Phone(kind: String, number: String)
case class Person(name: String, age: Int, phone: List[Phone])
// OK
persons.update(mq"{}", mqt"{ $$inc : { age : 1 } }"[Person])
persons.find(mqt"{ phone.number : '223322' }"[Person])
43. mqt – typechecking interpolator
case class Phone(kind: String, number: String)
case class Person(name: String, age: Int, phone: List[Phone])
// OK
persons.update(mq"{}", mqt"{ $$inc : { age : 1 } }"[Person])
persons.find(mqt"{ phone.number : '223322' }"[Person])
// COMPILE ERROR
persons.update(mq"{}", mqt"""{$$set : { nme : "Joe" }}"""[Person])
persons.find(mqt"{ name.1 : 'Joe' }"[Person])
persons.find(mqt"{ phone.num : '223322' }"[Person])
44. Passing type into intepolator
implicit class MongoHelper(val sc: StringContext) extends AnyVal {
def mq(args: Any*): DBObject = macro MongoHelper.mq_impl
def mqt(args: Any*) = new QueryWrapper
}
class QueryWrapper {
def apply[T]: DBObject = macro MongoHelper.mqt_impl[T]
}
object MongoHelper {
def mqt_impl[T: c.WeakTypeTag](c: Context):
c.Expr[DBObject] = ???
}
45. Inside mqt_impl
def mqt_impl[T: c.WeakTypeTag](c: Context): c.Expr[DBObject] = {
val q"$cn(scala.StringContext.apply(..$pTrees)).mqt(..$aTrees)"
= c.prefix.tree
val args = aTrees.map(c.Expr(_))
val parsed = parse(c)(pTrees)
checkObject(c)(c.weakTypeOf[T], parsed)
wrapObject(c)(parsed, args.iterator)
}
46. Verifing the type
def checkType(c: Context)(tpe: c.Type, obj: Object) = {
import c.universe._
val ctor = tpe.decl(termNames.CONSTRUCTOR).asMethod
val params = ctor.paramLists.head
val className = t.typeSymbol.name.toString
val fields = params.map(s => s.name.toString -> s).toMap
obj.members.foreach { case (m, _) =>
if (!fields.contains(m.name)) {
c.abort(c.enclosingPosition ,
s"Class $className doesn't contain field '${m.name}'")
}
}
}
47. Testing interpolator
it should "support nested objects" in {
val q = mq"""{ user : "Joe", age : {$$gt : 25}}"""
q should equal(MongoDBObject("user" -> "Joe",
"age" -> MongoDBObject("$gt" -> 25)))
}
48. Testing error scenarios
import scala.reflect.runtime.{ universe => ru }
class CompileTest extends FlatSpec {
val cl = getClass.getClassLoader.asInstanceOf[URLClassLoader]
val cp = cl.getURLs.map(_.getFile).mkString(File.pathSeparator)
val mirror = ru.runtimeMirror(cl)
val tb = mirror.mkToolBox(options = s"-cp $cp")
49. Testing error scenarios
import scala.reflect.runtime.{ universe => ru }
class CompileTest extends FlatSpec {
val cl = getClass.getClassLoader.asInstanceOf[URLClassLoader]
val cp = cl.getURLs.map(_.getFile).mkString(File.pathSeparator)
val mirror = ru.runtimeMirror(cl)
val tb = mirror.mkToolBox(options = s"-cp $cp")
def getError(q: String): String = {
val e = intercept[ToolBoxError] {
tb.eval(tb.parse(q))
}
e.message
}
50. Testing error scenarios
import scala.reflect.runtime.{ universe => ru }
class CompileTest extends FlatSpec {
val cl = getClass.getClassLoader.asInstanceOf[URLClassLoader]
val cp = cl.getURLs.map(_.getFile).mkString(File.pathSeparator)
val mirror = ru.runtimeMirror(cl)
val tb = mirror.mkToolBox(options = s"-cp $cp")
def getError(q: String): String = {
val e = intercept[ToolBoxError] {
tb.eval(tb.parse(q))
}
e.message
}
it should "fail on malformed BSON objects" in {
val e = getError("""mq"{ test 5 }" """)
e should include("`:' expected , but 5 found")
}
}
54. Summary
Cons
Not easy to implement
Not highlighted in IDE
Pros
Less limitations on language structure
55. Summary
Cons
Not easy to implement
Not highlighted in IDE
Pros
Less limitations on language structure
Can preserve existing language
56. Summary
Cons
Not easy to implement
Not highlighted in IDE
Pros
Less limitations on language structure
Can preserve existing language
Martin said that string interpolation is cool