Skip to content

Commit d672cc6

Browse files
committed
Support for scala.Enumeration
1 parent 5b916ca commit d672cc6

6 files changed

Lines changed: 103 additions & 31 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,14 @@ val place = Place(
3939

4040
val json: JsValue = place.asJson
4141
```
42+
Scala Enumerations are also automatic mapping:
43+
```scala
44+
case class Money(amount: BigDecimal, currency: Money.Currency)
45+
46+
object Money extends Enumeration {
47+
type Currency = Value
48+
val ARS, BRL, USD, MXN = Value
49+
}
50+
...
51+
implicit val moneyMapping: Format[Money] = JsonMapper.mappingOf[Money]
52+
```

build.sbt

Lines changed: 2 additions & 2 deletions
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.2",
8+
version := "1.0.3",
99
scalaVersion := scala213,
1010
crossScalaVersions := supportedScalaVersions,
1111
scalacOptions := Seq(
@@ -20,7 +20,7 @@ lazy val commonSettings = Seq(
2020
libraryDependencies += "com.typesafe.play" %% "play-json" % "2.8.1",
2121
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3",
2222
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value,
23-
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % Test,
23+
libraryDependencies += "org.scalatest" %% "scalatest" % "3.1.1" % Test,
2424
licenses += ("MIT", url("https://opensource.org/licenses/MIT")),
2525
coverageExcludedPackages := "<empty>",
2626

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

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package org.nullvector
22

3-
import org.nullvector.domian.{Location, Monday, OperationSchedule, Place, Resident}
4-
import org.scalatest.Matchers._
5-
import org.scalatest._
3+
import org.nullvector.domian._
4+
import org.scalatest.flatspec.AnyFlatSpec
5+
import org.scalatest.matchers.should.Matchers._
66
import play.api.libs.json.{Format, Json, JsonConfiguration, Reads, Writes}
77

8-
class JsonMapperSpec extends FlatSpec {
8+
class JsonMapperSpec extends AnyFlatSpec {
99

1010
it should "create a writes for complex case classes graph" in {
1111
import JsonMapper._
@@ -30,18 +30,39 @@ class JsonMapperSpec extends FlatSpec {
3030

3131
it should "create a writes with a seales trait family" in {
3232
import JsonMapper._
33-
3433
val operationSchedule = OperationSchedule(Monday)
35-
3634
implicit val conf = JsonConfiguration(typeNaming = typeNaming)
37-
3835
implicit val x = mappingOf[OperationSchedule]
39-
4036
val jsValue = operationSchedule.asJson
41-
4237
(jsValue \ "availableDay" \ "_type").as[String] shouldBe "Monday"
4338
jsValue.as[OperationSchedule].availableDay shouldBe Monday
4439
}
4540

41+
it should "create a format mapping with enum" in {
42+
import JsonMapper._
43+
implicit val m: Format[Money] = mappingOf[Money]
44+
45+
val aMoney = Money.ars(2345.678)
46+
47+
aMoney.asJson.as[Money] should be(aMoney)
48+
}
49+
50+
it should "create a writes mapping with enum" in {
51+
import JsonMapper._
52+
implicit val w: Writes[Money] = writesOf[Money]
53+
54+
val aMoney = Money(6783.3211, Money.MXN)
55+
56+
aMoney.asJson.toString() should be("""{"amount":6783.3211,"currency":"MXN"}""")
57+
}
58+
59+
it should "create a reads mapping with enum" in {
60+
import JsonMapper._
61+
implicit val r: Reads[Money] = readsOf[Money]
62+
63+
Json
64+
.parse("""{"amount":6783.3211,"currency":"USD"}""")
65+
.as[Money] shouldBe Money(6783.3211, Money.USD)
66+
}
4667
}
4768

core/src/test/scala/org/nullvector/domian/Domin.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,17 @@ object Day {
1717
case object Monday extends Day
1818

1919
case object Sunday extends Day
20+
21+
case class Money(amount: BigDecimal, currency: Money.Currency) {
22+
23+
def +(aMoney: Money): Money = copy(amount + aMoney.amount)
24+
25+
def *(factor: BigDecimal): Money = copy(amount * factor)
26+
}
27+
28+
object Money extends Enumeration {
29+
type Currency = Value
30+
val ARS, BRL, USD, MXN = Value
31+
32+
def ars(amount: BigDecimal): Money = Money(amount, ARS)
33+
}

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

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,18 @@ private object JsonMapperMacroFactory {
5959
..$implicitWriters
6060
${mapperFilter.mainExpressionFrom(context)(domainTypeTag.tpe)}
6161
"""
62-
//println(code)
62+
//context.warning(context.enclosingPosition, code.toString())
6363
context.Expr[Format[E]](code)
6464
}
6565

6666

6767
private def extractTypes(context: blackbox.Context)
6868
(rootType: context.universe.Type): org.nullvector.tree.Tree[context.universe.Type] = {
6969
import context.universe._
70+
val enumType = context.typeOf[Enumeration]
7071

7172
def extractAll(caseType: context.universe.Type): org.nullvector.tree.Tree[context.universe.Type] = {
72-
7373
def isSupprtedTrait(aTypeClass: ClassSymbol) = aTypeClass.isTrait && aTypeClass.isSealed && !aTypeClass.fullName.startsWith("scala")
74-
7574
def extaracCaseClassesFromTypeArgs(classType: Type): List[Type] = {
7675
classType.typeArgs.collect {
7776
case argType if argType.typeSymbol.asClass.isCaseClass => List(classType, argType)
@@ -80,25 +79,27 @@ private object JsonMapperMacroFactory {
8079
}
8180

8281
val caseTypeAsClass = caseType.typeSymbol.asClass
83-
caseTypeAsClass.toString
84-
//println(s"$caseTypeAsClass -- is case class --> ${caseTypeAsClass.isCaseClass}")
8582
if (caseTypeAsClass.isCaseClass) {
8683
Tree(caseType,
87-
caseType.decls.collect { case method: MethodSymbol if method.isCaseAccessor =>
88-
val returnType = method.returnType
89-
returnType.toString // This is needed to materialize the type (WTF!!)
90-
returnType
91-
}
84+
caseType.decls
85+
.collect { case method: MethodSymbol if method.isCaseAccessor =>
86+
val returnType = method.returnType
87+
returnType.toString // This is needed to materialize the type (WTF!!)
88+
returnType
89+
}
9290
.collect {
91+
case aType if aType.typeSymbol.owner.isType &&
92+
aType.typeSymbol.owner.asType.toType =:= enumType =>
93+
List(Tree(aType))
9394
case aType if aType.typeSymbol.asClass.isCaseClass || isSupprtedTrait(aType.typeSymbol.asClass) => List(extractAll(aType))
9495
case aType => extaracCaseClassesFromTypeArgs(aType).map(arg => extractAll(arg))
95-
}.flatten.toList
96+
}
97+
.flatten.toList
9698

9799
)
98100
}
99101
else if (isSupprtedTrait(caseTypeAsClass)) {
100102
val subclasses = caseTypeAsClass.knownDirectSubclasses
101-
//println(s"$caseType -- subclasses --> $subclasses")
102103
Tree(caseType, subclasses.map(aType => extractAll(aType.asClass.toType)).toList)
103104
}
104105
else Tree.empty
@@ -110,6 +111,12 @@ private object JsonMapperMacroFactory {
110111

111112
sealed trait ExpressionFactory {
112113

114+
def enumTypeName(context: blackbox.Context)(aType: context.Type): Option[String] = {
115+
val enumType = context.typeOf[Enumeration]
116+
if (aType.typeSymbol.owner.asType.toType =:= enumType) Some(aType.toString.split("\\.").dropRight(1).mkString("."))
117+
else None
118+
}
119+
113120
def typeNotImplicitDeclared(context: blackbox.Context)(aType: context.Type, ignore: context.Type): Boolean
114121

115122
def implicitExpressionsFrom(context: blackbox.Context)(types: List[context.Type]): List[context.Tree]
@@ -120,11 +127,17 @@ private object JsonMapperMacroFactory {
120127
private object FormatExpressionFactory extends ExpressionFactory {
121128

122129
override def implicitExpressionsFrom(context: blackbox.Context)(types: List[context.Type]): List[context.Tree] = {
123-
types.map(aType => context.parse(s"""private implicit val ${context.freshName()}: Format[$aType] = format[$aType] """))
130+
types.map(aType => enumTypeName(context)(aType) match {
131+
case Some(enumName) =>
132+
context.parse(s"""private implicit val ${context.freshName()}: Format[$aType] = formatEnum($enumName) """)
133+
case None =>
134+
context.parse(s"""private implicit val ${context.freshName()}: Format[$aType] = format[$aType] """)
135+
})
124136
}
125137

126138
override def mainExpressionFrom(context: blackbox.Context)(tpe: context.Type): context.Tree = {
127-
context.parse(s"format[$tpe]")
139+
import context.universe._
140+
(q"format[$tpe]")
128141
}
129142

130143
override def typeNotImplicitDeclared(context: blackbox.Context)(aType: context.Type, ignore: context.Type): Boolean = {
@@ -140,11 +153,17 @@ private object JsonMapperMacroFactory {
140153
private object WritesExpressionFactory extends ExpressionFactory {
141154

142155
override def implicitExpressionsFrom(context: blackbox.Context)(types: List[context.Type]): List[context.Tree] = {
143-
types.map(aType => context.parse(s"""private implicit val ${context.freshName()}: Writes[$aType] = writes[$aType] """))
156+
types.map(aType => enumTypeName(context)(aType) match {
157+
case Some(enumName) =>
158+
context.parse(s"""private implicit val ${context.freshName()}: Writes[$aType] = Writes.enumNameWrites[$enumName] """)
159+
case None =>
160+
context.parse(s"""private implicit val ${context.freshName()}: Writes[$aType] = writes[$aType] """)
161+
})
144162
}
145163

146164
override def mainExpressionFrom(context: blackbox.Context)(tpe: context.Type): context.Tree = {
147-
context.parse(s"writes[$tpe]")
165+
import context.universe._
166+
(q"writes[$tpe]")
148167
}
149168

150169
override def typeNotImplicitDeclared(context: blackbox.Context)(aType: context.Type, ignore: context.Type): Boolean = {
@@ -160,11 +179,17 @@ private object JsonMapperMacroFactory {
160179
private object ReadsExpressionFactory extends ExpressionFactory {
161180

162181
override def implicitExpressionsFrom(context: blackbox.Context)(types: List[context.Type]): List[context.Tree] = {
163-
types.map(aType => context.parse(s"""private implicit val ${context.freshName()}: Reads[$aType] = reads[$aType] """))
182+
types.map(aType => enumTypeName(context)(aType) match {
183+
case Some(enumName) =>
184+
context.parse(s"""private implicit val ${context.freshName()}: Reads[$aType] = Reads.enumNameReads($enumName) """)
185+
case None =>
186+
context.parse(s"""private implicit val ${context.freshName()}: Reads[$aType] = reads[$aType] """)
187+
})
164188
}
165189

166190
override def mainExpressionFrom(context: blackbox.Context)(tpe: context.Type): context.Tree = {
167-
context.parse(s"reads[$tpe]")
191+
import context.universe._
192+
(q"reads[$tpe]")
168193
}
169194

170195
override def typeNotImplicitDeclared(context: blackbox.Context)(aType: context.Type, ignore: context.Type): Boolean = {

macros/src/test/scala/org/nullvector/TreeSpec.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package org.nullvector
22

33
import org.nullvector.tree.Tree
4-
import org.scalatest.{FlatSpec, Matchers}
4+
import org.scalatest.flatspec.AnyFlatSpec
5+
import org.scalatest.matchers.should.Matchers
56

6-
class TreeSpec extends FlatSpec with Matchers {
7+
class TreeSpec extends AnyFlatSpec with Matchers {
78

89
it should """ has a root element """ in {
910
Tree("Hola").toList shouldBe (List("Hola"))

0 commit comments

Comments
 (0)