Skip to content

Commit 28c0a9e

Browse files
committed
WIP
1 parent 2da8f0b commit 28c0a9e

File tree

7 files changed

+202
-84
lines changed

7 files changed

+202
-84
lines changed

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ lazy val supportedScalaVersions = List(scala212, scala213)
55
lazy val commonSettings = Seq(
66
name := "play-json-mapping",
77
organization := "null-vector",
8-
version := "1.0.0",
8+
version := "1.0.0-SNAPSHOT",
99
scalaVersion := scala213,
1010
crossScalaVersions := supportedScalaVersions,
1111
scalacOptions := Seq(

core/src/test/scala/org/nullvector/EventAdapterFactorySpec.scala

Lines changed: 0 additions & 50 deletions
This file was deleted.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.nullvector
2+
3+
import org.nullvector.domian._
4+
import org.scalatest.Matchers._
5+
import org.scalatest._
6+
import play.api.libs.json.{Format, Json, JsonConfiguration, Reads, Writes}
7+
8+
class JsonMapperSpec extends FlatSpec {
9+
10+
it should "create a writes for complex case classes graph" in {
11+
import JsonMapper._
12+
13+
val place = Place(
14+
"Watership Down",
15+
Location(51.235685, -1.309197),
16+
Seq(
17+
Resident("Fiver", 4, None),
18+
Resident("Bigwig", 6, Some("Owsla"))
19+
)
20+
)
21+
22+
implicit val w: Writes[Place] = writesOf[Place](snakeAndTypeNamingAndWriteNullsConfiguration)
23+
implicit val r: Reads[Place] = readsOf[Place](snakeAndTypeNamingConfiguration)
24+
25+
val jsValue = place.asJson
26+
jsValue.toString() should include("\"role\":null")
27+
(jsValue \ "center_location" \ "lat").as[Double] shouldBe 51.235685
28+
jsValue.as[Place].name shouldBe place.name
29+
}
30+
31+
it should "create a writes with a seales trait family" in {
32+
import JsonMapper._
33+
34+
val operationSchedule = OperationSchedule(Monday)
35+
36+
implicit val conf = JsonConfiguration(typeNaming = typeNaming)
37+
38+
implicit val x = mappingOf[OperationSchedule]
39+
40+
val jsValue = operationSchedule.asJson
41+
42+
(jsValue \ "availableDay" \ "_type").as[String] shouldBe "Monday"
43+
jsValue.as[OperationSchedule].availableDay shouldBe Monday
44+
}
45+
46+
}
47+

core/src/test/scala/org/nullvector/Location.scala

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.nullvector.domian
2+
3+
case class Location(lat: Double, long: Double)
4+
case class Resident(name: String, age: Int, role: Option[String])
5+
case class Place(name: String, centerLocation: Location, residents: Seq[Resident])
6+
case class OperationSchedule(availableDay: Day)
7+
8+
sealed trait Day
9+
10+
object Day {
11+
def apply(name: String): Day = name match {
12+
case "Monday" => Monday
13+
case "Sunday" => Sunday
14+
}
15+
}
16+
17+
case object Monday extends Day
18+
19+
case object Sunday extends Day

macros/src/main/scala/org/nullvector/JsonMapper.scala

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,37 @@
11
package org.nullvector
22

3-
import play.api.libs.json.{JsValue, Json, Writes}
3+
import play.api.libs.json.JsonConfiguration.Aux
4+
import play.api.libs.json.JsonNaming.SnakeCase
5+
import play.api.libs.json.OptionHandlers.WritesNull
6+
import play.api.libs.json.{Format, JsValue, Json, JsonConfiguration, JsonNaming, Reads, Writes}
7+
8+
import scala.util.matching.Regex
49

510
object JsonMapper {
611

12+
private val typeNameRegex: Regex = "^(\\$)?([^\\$\\.]*)(.*)".r
13+
14+
val typeNaming: JsonNaming = (property: String) => property.reverse match {
15+
case typeNameRegex(_, name, _) => name.reverse
16+
case _ => property
17+
}
18+
19+
val snakeAndTypeNamingConfiguration: JsonConfiguration = JsonConfiguration(SnakeCase, typeNaming = typeNaming)
20+
21+
val snakeAndTypeNamingAndWriteNullsConfiguration: JsonConfiguration = JsonConfiguration(SnakeCase, typeNaming = typeNaming, optionHandlers = WritesNull)
22+
23+
def mappingOf[T]: Format[T] = macro JsonMapperMacroFactory.mappingOf[T]
24+
25+
def mappingOf[T](jsonConfiguration: JsonConfiguration): Format[T] = macro JsonMapperMacroFactory.mappingWithConfigOf[T]
26+
27+
def readsOf[T]: Reads[T] = macro JsonMapperMacroFactory.readsOf[T]
28+
29+
def readsOf[T](jsonConfiguration: JsonConfiguration): Reads[T] = macro JsonMapperMacroFactory.readsWithConfigOf[T]
30+
731
def writesOf[T]: Writes[T] = macro JsonMapperMacroFactory.writesOf[T]
832

33+
def writesOf[T](jsonConfiguration: JsonConfiguration): Writes[T] = macro JsonMapperMacroFactory.writesWithConfigOf[T]
34+
935
implicit class WritesDsl[T](anInstance: T) {
1036

1137
def asJson(implicit w: Writes[T]): JsValue = Json.toJson(anInstance)
Lines changed: 108 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
package org.nullvector
22

3-
import play.api.libs.json.{Format, JsValue, Reads, Writes}
3+
import play.api.libs.json.{Format, JsValue, JsonConfiguration, OFormat, OWrites, Reads, Writes}
44

55
import scala.reflect.macros.blackbox
66

77
private object JsonMapperMacroFactory {
88

9-
implicit class MapOnPair[+T1, +T2](pair: (T1, T2)) {
10-
def map[A1, A2](f: (T1, T2) => (A1, A2)): (A1, A2) = f(pair._1, pair._2)
11-
}
12-
139
private val supportedClassTypes = List(
1410
"scala.Option",
1511
"scala.collection.immutable.List",
@@ -21,54 +17,139 @@ private object JsonMapperMacroFactory {
2117

2218

2319
def writesOf[E](context: blackbox.Context)
24-
(implicit domainTypeTag: context.WeakTypeTag[E]): context.Expr[Writes[E]] = {
20+
(implicit domainTypeTag: context.WeakTypeTag[E]): context.Expr[Writes[E]] = {
21+
buildExpression(context, WritesMapperFilter)(None)
22+
}
2523

26-
import context.universe._
27-
val typeOfWrites = context.typeOf[Writes[_]]
28-
val typeOfReads = context.typeOf[Reads[_]]
29-
val typeOfFormat = context.typeOf[Format[_]]
24+
def writesWithConfigOf[E](context: blackbox.Context)
25+
(jsonConfiguration: context.Expr[JsonConfiguration])
26+
(implicit domainTypeTag: context.WeakTypeTag[E]): context.Expr[Writes[E]] = {
27+
buildExpression(context, WritesMapperFilter)(Some(jsonConfiguration))
28+
}
29+
30+
def readsOf[E](context: blackbox.Context)
31+
(implicit domainTypeTag: context.WeakTypeTag[E]): context.Expr[Reads[E]] = {
32+
buildExpression(context, ReadsMapperFilter)(None)
33+
}
34+
35+
def readsWithConfigOf[E](context: blackbox.Context)
36+
(jsonConfiguration: context.Expr[JsonConfiguration])
37+
(implicit domainTypeTag: context.WeakTypeTag[E]): context.Expr[Reads[E]] = {
38+
buildExpression(context, ReadsMapperFilter)(Some(jsonConfiguration))
39+
}
40+
41+
def mappingOf[E](context: blackbox.Context)
42+
(implicit domainTypeTag: context.WeakTypeTag[E]): context.Expr[Format[E]] = {
43+
buildExpression(context, FormatMapperFilter)(None)
44+
}
45+
46+
def mappingWithConfigOf[E](context: blackbox.Context)
47+
(jsonConfiguration: context.Expr[JsonConfiguration])
48+
(implicit domainTypeTag: context.WeakTypeTag[E]): context.Expr[Format[E]] = {
49+
buildExpression(context, FormatMapperFilter)(Some(jsonConfiguration))
50+
}
3051

52+
private def buildExpression[E](context: blackbox.Context, mapperFilter: MapperFilter)
53+
(jsonConfiguration: Option[context.Expr[JsonConfiguration]])
54+
(implicit domainTypeTag: context.WeakTypeTag[E]): context.Expr[Format[E]] = {
55+
56+
import context.universe._
3157

32-
val (toBeImplicit, toBeMainWriter) = extractCaseTypes(context)(domainTypeTag.tpe).toList.reverse.distinct.partition(_ != domainTypeTag.tpe)
58+
val (toBeImplicit, toBeMainWriter) = extractTypes(context)(domainTypeTag.tpe).toList.reverse.distinct.partition(_ != domainTypeTag.tpe)
59+
val implicitWriters = mapperFilter.filterTypes(context)(toBeImplicit)
3360

34-
val implicitWriters = toBeImplicit
35-
.filter { caseType =>
36-
context.inferImplicitValue(appliedType(typeOfWrites, caseType)).isEmpty ||
37-
context.inferImplicitValue(appliedType(typeOfFormat, caseType)).isEmpty
38-
}
39-
.map(caseType => q"""private implicit val ${TermName(context.freshName())} = play.api.libs.json.Json.writes[$caseType] """)
61+
val config = jsonConfiguration
62+
.map(confExpr => q"private implicit val ${TermName(context.freshName())} = $confExpr")
63+
.getOrElse(EmptyTree)
4064

4165
val code =
4266
q"""
67+
import play.api.libs.json._
68+
import play.api.libs.json.Json._
69+
$config
4370
..$implicitWriters
44-
play.api.libs.json.Json.writes[${toBeMainWriter.head}]
71+
${mapperFilter.mapperExpression(context)(toBeMainWriter.head)}
4572
"""
46-
println(code)
47-
context.Expr[Writes[E]](code)
73+
context.Expr[Format[E]](code)
4874
}
4975

50-
private def extractCaseTypes(context: blackbox.Context)
51-
(caseType: context.universe.Type): org.nullvector.Tree[context.universe.Type] = {
76+
private def extractTypes(context: blackbox.Context)
77+
(aType: context.universe.Type): org.nullvector.Tree[context.universe.Type] = {
5278
import context.universe._
5379

80+
def isSupprtedTrait(aTypeClass: ClassSymbol) = aTypeClass.isTrait && aTypeClass.isSealed && !aTypeClass.fullName.startsWith("scala")
81+
5482
def extaracCaseClassesFromSupportedTypeClasses(classType: Type): List[Type] = {
5583
if (supportedClassTypes.contains(classType.typeSymbol.fullName)) classType.typeArgs.collect {
5684
case argType if argType.typeSymbol.asClass.isCaseClass => List(classType, argType)
5785
case t => extaracCaseClassesFromSupportedTypeClasses(t)
5886
}.flatten else Nil
5987
}
6088

61-
if (caseType.typeSymbol.asClass.isCaseClass) {
62-
Tree(caseType,
63-
caseType.decls.toList
89+
val aTypeClass: context.universe.ClassSymbol = aType.typeSymbol.asClass
90+
91+
if (aTypeClass.isCaseClass) {
92+
Tree(aType,
93+
aType.decls.toList
6494
.collect { case method: MethodSymbol if method.isCaseAccessor => method.returnType }
6595
.collect {
66-
case aType if aType.typeSymbol.asClass.isCaseClass => List(extractCaseTypes(context)(aType))
67-
case aType => extaracCaseClassesFromSupportedTypeClasses(aType).map(arg => extractCaseTypes(context)(arg))
96+
case aType if aType.typeSymbol.asClass.isCaseClass || isSupprtedTrait(aType.typeSymbol.asClass) => List(extractTypes(context)(aType))
97+
case aType => extaracCaseClassesFromSupportedTypeClasses(aType).map(arg => extractTypes(context)(arg))
6898
}.flatten
6999
)
70100
}
101+
else if (isSupprtedTrait(aTypeClass)) {
102+
Tree(aType, aTypeClass.knownDirectSubclasses.map(aType => extractTypes(context)(aType.asClass.toType)).toList)
103+
}
71104
else Tree.empty
72105
}
73106

107+
sealed trait MapperFilter {
108+
def filterTypes(context: blackbox.Context)(types: List[context.Type]): List[context.Tree]
109+
110+
def mapperExpression(context: blackbox.Context)(tpe: context.Type): context.Tree
111+
}
112+
113+
object FormatMapperFilter extends MapperFilter {
114+
115+
override def filterTypes(context: blackbox.Context)(types: List[context.Type]): List[context.Tree] = {
116+
import context.universe._
117+
val typeOfFormat = context.typeOf[Format[_]]
118+
types.filter(aType => context.inferImplicitValue(appliedType(typeOfFormat, aType)).isEmpty)
119+
.map(aType => context.parse(s"""private implicit val ${context.freshName()}: Format[$aType] = format[$aType] """))
120+
}
121+
122+
override def mapperExpression(context: blackbox.Context)(tpe: context.Type): context.Tree = {
123+
context.parse(s"format[$tpe]")
124+
}
125+
}
126+
127+
object WritesMapperFilter extends MapperFilter {
128+
129+
override def filterTypes(context: blackbox.Context)(types: List[context.Type]): List[context.Tree] = {
130+
import context.universe._
131+
val typeOfFormat = context.typeOf[Writes[_]]
132+
types.filter(aType => context.inferImplicitValue(appliedType(typeOfFormat, aType)).isEmpty)
133+
.map(aType => context.parse(s"""private implicit val ${context.freshName()}: Writes[$aType] = writes[$aType] """))
134+
}
135+
136+
override def mapperExpression(context: blackbox.Context)(tpe: context.Type): context.Tree = {
137+
context.parse(s"writes[$tpe]")
138+
}
139+
}
140+
141+
object ReadsMapperFilter extends MapperFilter {
142+
143+
override def filterTypes(context: blackbox.Context)(types: List[context.Type]): List[context.Tree] = {
144+
import context.universe._
145+
val typeOfFormat = context.typeOf[Reads[_]]
146+
types.filter(aType => context.inferImplicitValue(appliedType(typeOfFormat, aType)).isEmpty)
147+
.map(aType => context.parse(s"""private implicit val ${context.freshName()}: Reads[$aType] = reads[$aType] """))
148+
}
149+
150+
override def mapperExpression(context: blackbox.Context)(tpe: context.Type): context.Tree = {
151+
context.parse(s"reads[$tpe]")
152+
}
153+
}
154+
74155
}

0 commit comments

Comments
 (0)