Skip to content

Commit 1ea64ee

Browse files
author
Martin Raison
committed
first commit
0 parents  commit 1ea64ee

File tree

8 files changed

+255
-0
lines changed

8 files changed

+255
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
target/

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2014 FortyTwo Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of
6+
this software and associated documentation files (the "Software"), to deal in
7+
the Software without restriction, including without limitation the rights to
8+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9+
the Software, and to permit persons to whom the Software is furnished to do so,
10+
subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+

README.md

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
The __@json__ scala macro annotation is the quickest way to add a JSON format to your [Play](http://www.playframework.com/) project's case classes.
2+
3+
#How it works
4+
Just add ```@json``` in front of your case class definition:
5+
6+
```scala
7+
import com.kifi.macros.json
8+
9+
@json case class Person(name: String, age: Int)
10+
```
11+
12+
You can now serialize/deserialize your objects using Play's convenience methods:
13+
14+
```scala
15+
import play.api.libs.json._
16+
val person = Person("Victor Hugo", 46)
17+
val json = Json.toJson(person)
18+
Json.fromJson[Person](json)
19+
```
20+
21+
If the case class contains 2 fields or more, Play's [JSON macro inception](http://www.playframework.com/documentation/2.1.1/ScalaJsonInception) is used. If the case class has only one field (i.e. the class is just a wrapper around another type), then the JSON format is the format of the field itself:
22+
23+
```scala
24+
> @json case class City(name: String)
25+
> val city = City("San Francisco")
26+
> Json.toJson(city)
27+
"San Francisco"
28+
```
29+
30+
This is often more convenient than Play's default format ```{"name": "San Francisco"}```.
31+
32+
#Installation
33+
*This project hasn't been published yet. This message will be removed once it becomes available*
34+
35+
If you're using Play (version 2.1 or higher) with SBT, you should add the following settings to your build:
36+
37+
```scala
38+
39+
libraryDependencies += "com.kifi" %% "json-annotation" % "0.1"
40+
41+
addCompilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full)
42+
```
43+
44+
If you're not using Play, you will also need to add ```play-json``` to your dependencies:
45+
46+
```scala
47+
48+
resolvers += "Typesafe Repo" at "http://repo.typesafe.com/typesafe/releases/"
49+
50+
libraryDependencies += "com.typesafe.play" %% "play-json" % "2.2.1"
51+
```
52+
53+
This library was tested with both Scala 2.10 and 2.11.

build.sbt

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
organization := "com.kifi"
2+
3+
name := "json-annotation"
4+
5+
version := "0.1"
6+
7+
scalaVersion := "2.11.1"
8+
9+
crossScalaVersions := Seq("2.10.2", "2.10.3", "2.10.4", "2.11.0", "2.11.1")
10+
11+
resolvers += Resolver.sonatypeRepo("releases")
12+
13+
libraryDependencies <+= (scalaVersion)("org.scala-lang" % "scala-reflect" % _)
14+
15+
libraryDependencies ++= (
16+
if (scalaVersion.value.startsWith("2.10")) List("org.scalamacros" %% "quasiquotes" % "2.0.1")
17+
else Nil
18+
)
19+
20+
unmanagedSourceDirectories in Compile <+= (sourceDirectory in Compile, scalaBinaryVersion){
21+
(sourceDir, version) => sourceDir / (if (version.startsWith("2.10")) "scala_2.10" else "scala_2.11")
22+
}
23+
24+
addCompilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full)
25+
26+
scalacOptions in ThisBuild ++= Seq("-unchecked", "-deprecation")
27+
28+
publishMavenStyle := true
29+
30+
publishTo := {
31+
val nexus = "https://oss.sonatype.org/"
32+
if (isSnapshot.value)
33+
Some("snapshots" at nexus + "content/repositories/snapshots")
34+
else
35+
Some("releases" at nexus + "service/local/staging/deploy/maven2")
36+
}
37+
38+
publishArtifact in Test := false
39+
40+
pomIncludeRepository := { _ => false }
41+
42+
pomExtra := (
43+
<url>https://github.com/kifi/json-annotation</url>
44+
<licenses>
45+
<license>
46+
<name>MIT</name>
47+
<url>http://opensource.org/licenses/MIT</url>
48+
<distribution>repo</distribution>
49+
</license>
50+
</licenses>
51+
<scm>
52+
<url>git@github.com:kifi/json-annotation.git</url>
53+
<connection>scm:git:git@github.com:kifi/json-annotation.git</connection>
54+
</scm>
55+
<developers>
56+
<developer>
57+
<id>martinraison</id>
58+
<name>Martin Raison</name>
59+
<url>https://github.com/martinraison</url>
60+
</developer>
61+
</developers>)

project/build.properties

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sbt.version=0.13.5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.kifi.macros
2+
3+
import scala.reflect.macros._
4+
import scala.language.experimental.macros
5+
import scala.annotation.StaticAnnotation
6+
7+
import CrossVersionDefs._
8+
9+
/**
10+
* "@json" macro annotation for case classes
11+
*
12+
* This macro annotation automatically creates a JSON serializer for the annotated case class.
13+
* The companion object will be automatically created if it does not already exist.
14+
*
15+
* If the case class has more than one field, the default Play formatter is used.
16+
* If the case class has only one field, the field is directly serialized. For example, if A
17+
* is defined as:
18+
*
19+
* case class A(value: Int)
20+
*
21+
* then A(4) will be serialized as '4' instead of '{"value": 4}'.
22+
*/
23+
class json extends StaticAnnotation {
24+
def macroTransform(annottees: Any*): Any = macro jsonMacro.impl
25+
}
26+
27+
object jsonMacro {
28+
def impl(c: CrossVersionContext)(annottees: c.Expr[Any]*): c.Expr[Any] = {
29+
import c.universe._
30+
31+
def extractClassNameAndFields(classDecl: ClassDef) = {
32+
try {
33+
val q"case class $className(..$fields) extends ..$bases { ..$body }" = classDecl
34+
(className, fields)
35+
} catch {
36+
case _: MatchError => c.abort(c.enclosingPosition, "Annotation is only supported on case class")
37+
}
38+
}
39+
40+
def jsonFormatter(className: TypeName, fields: List[ValDef]) = {
41+
fields.length match {
42+
case 0 => c.abort(c.enclosingPosition, "Cannot create json formatter for case class with no fields")
43+
case 1 =>
44+
// Only one field, use the serializer for the field
45+
q"""
46+
implicit val jsonAnnotationFormat = {
47+
import play.api.libs.json._
48+
Format(
49+
__.read[${fields.head.tpt}].map(s => ${className.toTermName}(s)),
50+
new Writes[$className] { def writes(o: $className) = Json.toJson(o.${fields.head.name}) }
51+
)
52+
}
53+
"""
54+
case _ =>
55+
// More than one field, use Play's macro
56+
q"implicit val jsonAnnotationFormat = play.api.libs.json.Json.format[$className]"
57+
}
58+
}
59+
60+
def modifiedCompanion(compDeclOpt: Option[ModuleDef], format: ValDef, className: TypeName) = {
61+
compDeclOpt map { compDecl =>
62+
// Add the formatter to the existing companion object
63+
val q"object $obj extends ..$bases { ..$body }" = compDecl
64+
q"""
65+
object $obj extends ..$bases {
66+
..$body
67+
$format
68+
}
69+
"""
70+
} getOrElse {
71+
// Create a companion object with the formatter
72+
q"object ${className.toTermName} { $format }"
73+
}
74+
}
75+
76+
def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None) = {
77+
val (className, fields) = extractClassNameAndFields(classDecl)
78+
val format = jsonFormatter(className, fields)
79+
val compDecl = modifiedCompanion(compDeclOpt, format, className)
80+
81+
// Return both the class and companion object declarations
82+
c.Expr(q"""
83+
$classDecl
84+
$compDecl
85+
""")
86+
}
87+
88+
annottees.map(_.tree) match {
89+
case (classDecl: ClassDef) :: Nil => modifiedDeclaration(classDecl)
90+
case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => modifiedDeclaration(classDecl, Some(compDecl))
91+
case _ => c.abort(c.enclosingPosition, "Invalid annottee")
92+
}
93+
}
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.kifi.macros
2+
3+
import scala.reflect.macros._
4+
import scala.language.experimental.macros
5+
import scala.annotation.StaticAnnotation
6+
7+
/**
8+
* Scala 2.10 uses Context (doesn't know about blackbox and whitebox)
9+
*/
10+
object CrossVersionDefs {
11+
type CrossVersionContext = Context
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.kifi.macros
2+
3+
import scala.reflect.macros._
4+
import scala.language.experimental.macros
5+
import scala.annotation.StaticAnnotation
6+
7+
/**
8+
* Context has been deprecated in Scala 2.11, blackbox.Context is used instead
9+
*/
10+
object CrossVersionDefs {
11+
type CrossVersionContext = blackbox.Context
12+
}

0 commit comments

Comments
 (0)