Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Gregg Hernandez committed Feb 13, 2017
0 parents commit 3fe37c1
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target
project/target
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
This project is an attempt at a scalameta macro for play-json that avoids the overhead of play-json's functional syntax (see: https://www.lucidchart.com/techblog/2016/08/29/speeding-up-restful-services-in-play-framework/).

In contrast with Play's `Json.format[A]` macro, json-macro has to use an annotation (scalameta does not yet support def macros):

@JsonRecord
case class Thing(a: String)
13 changes: 13 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
scalaVersion in ThisBuild := "2.11.8"

resolvers += Resolver.sonatypeRepo("releases")
resolvers += Resolver.bintrayIvyRepo("scalameta", "maven")
addCompilerPlugin("org.scalameta" % "paradise" % "3.0.0-beta4" cross CrossVersion.full)
scalacOptions in (Compile, console) := Seq()
sources in (Compile, doc) := Nil

libraryDependencies ++= Seq(
"com.typesafe.play" %% "play-json" % "2.6.0-M1",
"org.scalameta" %% "scalameta" % "1.4.0",
"org.specs2" %% "specs2-core" % "3.8.8" % Test
)
1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=0.13.13
85 changes: 85 additions & 0 deletions src/main/scala/Json.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.gregghz.json

import scala.annotation.StaticAnnotation
import scala.meta._
import scala.collection.immutable.Seq

object JsonRecord {

def generateReads(name: Type.Name, params: Seq[Term.Param]) = {

val forLines: Seq[Enumerator.Generator] = params.map { param =>
val paramTypeStr = param.decltpe.map(_.toString).getOrElse {
abort(s"${param.name.value} is missing a type annotation")
}
val tpe = t"${Type.Name(paramTypeStr)}"
val pat = Pat.Var.Term(Term.Name(param.name.value))
enumerator"$pat <- (json \ ${param.name.value}).validate[$tpe]"
}

val terms = params.map { param => Term.Name(param.name.value) }

val tt = q"${Term.Name(name.value)}.apply(..$terms)"
q"play.api.libs.json.Reads(json => for { ..$forLines } yield $tt)"
}

def generateWrites(name: Type.Name, params: Seq[Term.Param]) = {
val pairs = params.map { param =>
val term = Term.Name(param.name.value)
q"${param.name.value} -> value.$term"
}

val jsonObj = q"play.api.libs.json.Json.obj(..$pairs)"

q"play.api.libs.json.Writes(value => $jsonObj)"
}

def modifyClass(
mods: Seq[Mod],
name: Type.Name,
params: Seq[Term.Param],
body: Seq[Stat],
template: Seq[Ctor.Call],
comp: Option[Stat]
) = {
val term = Term.Name(name.value)
val readsTerm = Pat.Var.Term(Term.Name(s"${name.value}__PlayJsonReads"))
val writesTerm = Pat.Var.Term(Term.Name(s"${name.value}__PlayJsonWrites"))

val implicits = Seq(
q"implicit val $readsTerm: play.api.libs.json.Reads[$name] = ${generateReads(name, params)}",
q"implicit val $writesTerm: play.api.libs.json.Writes[$name] = ${generateWrites(name, params)}"
)

val finalComp = comp.map {
case q"..$mods object $oname { ..$body }" => q"""object $oname {
..$body
..$implicits
}"""
case _ => abort("Invalid companion object")
}.getOrElse(q"object $term { ..$implicits }")

q"""
..$mods case class $name(..$params) extends ..$template { .. $body }
$finalComp
"""
}
}

class JsonRecord extends StaticAnnotation {

inline def apply(defn: Any): Any = meta {
import JsonRecord._
defn match {
case q"""..$mods case class $name(..$params) extends ..$template { ..$body }""" =>
modifyClass(mods, name, params, body, template, None)
case q"""
..$mods case class $cname(..$cparams) extends ..$template { ..$cbody }
$comp
""" =>
modifyClass(mods, cname, cparams, cbody, template, Some(comp))
case _ =>
abort("@JsonRecord must be used with a case class")
}
}
}
45 changes: 45 additions & 0 deletions src/test/scala/JsonTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.gregghz.json

import org.specs2.mutable._
import play.api.libs.json._

class JsonTest extends Specification {

"@JsonRecord" should {
"generate Reads and Writes" in {
@JsonRecord
case class SimpleClass(
string: String,
int: Int
)

val startingJson = Json.obj("string" -> "hello", "int" -> 10)

val result = startingJson.as[SimpleClass]
result.string mustEqual "hello"
result.int mustEqual 10

val json = Json.toJson(SimpleClass("hello", 10))
json mustEqual startingJson
}

"preserve a predefined companion object" in {
@JsonRecord
case class SimpleClass(string: String, int: Int)
object SimpleClass {
def f(): String = "hello"
}

val startingJson = Json.obj("string" -> "hello", "int" -> 10)

val result = startingJson.as[SimpleClass]
result.string mustEqual "hello"
result.int mustEqual 10

val json = Json.toJson(SimpleClass("hello", 10))
json mustEqual startingJson

SimpleClass.f() mustEqual "hello"
}
}
}

0 comments on commit 3fe37c1

Please sign in to comment.