-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Gregg Hernandez
committed
Feb 13, 2017
0 parents
commit 3fe37c1
Showing
6 changed files
with
152 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
target | ||
project/target |
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
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) |
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
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 | ||
) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
sbt.version=0.13.13 |
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
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") | ||
} | ||
} | ||
} |
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
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" | ||
} | ||
} | ||
} |