-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Cross-compile generic module to Scala 3
- Loading branch information
Showing
29 changed files
with
490 additions
and
17 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
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
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
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
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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
15 changes: 15 additions & 0 deletions
15
play-json-generic/src/main/scala-3/com/evolution/playjson/generic/EnumMappings.scala
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,15 @@ | ||
package com.evolution.playjson.generic | ||
|
||
import scala.deriving.Mirror | ||
import scala.compiletime.summonAll | ||
|
||
case class EnumMappings[A](labels: Map[A, String]) | ||
|
||
object EnumMappings: | ||
inline given valueMap[E](using m: Mirror.SumOf[E]): EnumMappings[E] = | ||
// First, we make a compile-time check that all of subtypes of E are singletons | ||
// (i.e. case objects) by requiring that there's an instance of ValueOf for each subtype. | ||
val singletons = summonAll[Tuple.Map[m.MirroredElemTypes, ValueOf]] | ||
// Then, we can safely obtain a list of ValueOf instances and map each subtype to its string representation. | ||
val elems = singletons.toList.asInstanceOf[List[ValueOf[E]]] | ||
EnumMappings(elems.view.map(_.value).map(e => e -> e.toString).toMap) |
25 changes: 25 additions & 0 deletions
25
play-json-generic/src/main/scala-3/com/evolution/playjson/generic/Enumeration.scala
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,25 @@ | ||
package com.evolution.playjson.generic | ||
|
||
import play.api.libs.json._ | ||
|
||
class Enumeration[A] private(enumMappings: EnumMappings[A]): | ||
|
||
def format(using nameCodingStrategy: NameCodingStrategy): Format[A] = new Format[A]: | ||
|
||
val labelsLookup: Map[A, String] = enumMappings.labels.map { case (k, v) => (k, nameCodingStrategy(v)) } | ||
val valuesLookup: Map[String, A] = labelsLookup.map(_.swap) | ||
|
||
def writes(o: A): JsValue = JsString(labelsLookup(o)) | ||
|
||
def reads(json: JsValue): JsResult[A] = { | ||
for { | ||
s <- json.validate[JsString] | ||
v <- valuesLookup.get(s.value) match { | ||
case Some(v) => JsSuccess(v) | ||
case None => JsError(s"Cannot parse ${ s.value }") | ||
} | ||
} yield v | ||
} | ||
|
||
object Enumeration: | ||
def apply[A](using enumMappings: EnumMappings[A]) = new Enumeration[A](enumMappings) |
16 changes: 16 additions & 0 deletions
16
play-json-generic/src/main/scala-3/com/evolution/playjson/generic/FlatTypeFormat.scala
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,16 @@ | ||
package com.evolution.playjson.generic | ||
|
||
import play.api.libs.json._ | ||
|
||
/** | ||
* This is a helper class for creating a `Format` instance for a sealed trait hierarchy. | ||
* | ||
* When reading from JSON, it will look for a `type` field in the JSON object and use its value to determine which | ||
* subtype to use. The `type` field will be removed from the JSON object before the subtype's `Reads` is called. | ||
* | ||
* When writing to JSON, it will add a `type` field to the JSON object with the value of the subtype's simple name | ||
* (without package prefix). | ||
*/ | ||
object FlatTypeFormat: | ||
def apply[A](using reads: FlatTypeReads[A], writes: FlatTypeWrites[A]): OFormat[A] = | ||
OFormat(reads.reads(_), writes.writes(_)) |
99 changes: 99 additions & 0 deletions
99
play-json-generic/src/main/scala-3/com/evolution/playjson/generic/FlatTypeReads.scala
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,99 @@ | ||
package com.evolution.playjson.generic | ||
|
||
import scala.deriving.Mirror | ||
import scala.compiletime.* | ||
import play.api.libs.json.* | ||
import scala.annotation.nowarn | ||
|
||
/** | ||
* This is a helper class for creating a `Reads` instance for a sealed trait hierarchy. | ||
* It will look for a `type` field in the JSON object and use its value to determine which subtype to use. | ||
* The `type` field will be removed from the JSON object before the subtype's `Reads` is called. | ||
* | ||
* The difference between this class and `NestedTypeReads` is that this class uses the simple name of the subtype | ||
* instead of the full prefixed name. This means that you cannot use the same simple name for multiple subtypes. | ||
* | ||
* Example: | ||
* | ||
* {{{ | ||
* | ||
* sealed trait Parent | ||
* case class Child1(field1: String) extends Parent | ||
* case class Child2(field2: Int) extends Parent | ||
* | ||
* object Child1: | ||
* given Reads[Child1] = Json.reads[Child1] | ||
* object Child2: | ||
* given Reads[Child2] = Json.reads[Child2] | ||
* | ||
* val reads: FlatTypeReads[Parent] = summon[FlatTypeReads[Parent]] | ||
* | ||
* val json: JsValue = Json.parse("""{"type": "Child1", "field1": "value"}""") | ||
* val result: JsResult[Parent] = reads.reads(json) // JsSuccess(Child1(value),) | ||
* }}} | ||
*/ | ||
trait FlatTypeReads[T] extends Reads[T]: | ||
override def reads(jsValue: JsValue): JsResult[T] | ||
|
||
object FlatTypeReads: | ||
def create[A](f: JsValue => JsResult[A]): FlatTypeReads[A] = | ||
(json: JsValue) => f(json) | ||
|
||
def apply[A](using ev: FlatTypeReads[A]): FlatTypeReads[A] = ev | ||
|
||
/** | ||
* This is the first method that will be called when the compiler is looking for an instance of `FlatTypeReads`. | ||
* It will look for a `type` field in the JSON object and use its value to determine which subtype of `A` to use. | ||
* Then, it will look for an instance of `Reads` for that subtype and use it to read the JSON object. | ||
*/ | ||
inline given deriveFlatTypeReads[A](using | ||
m: Mirror.SumOf[A], | ||
nameCodingStrategy: NameCodingStrategy | ||
): FlatTypeReads[A] = | ||
create[A] { json => | ||
for { | ||
obj <- json.validate[JsObject] | ||
typ <- (obj \ "type").validate[String] | ||
result <- deriveReads[A](typ) match | ||
case Some(reads) => reads.reads(obj - "type") | ||
case None => JsError("Failed to find decoder") | ||
} yield result | ||
} | ||
|
||
/** | ||
* Recursively search the given tuple of types for one that matches the given type name and has a `Reads` instance. | ||
* | ||
* @param typ the type name to search for | ||
* @param nameCodingStrategy the naming strategy to use when comparing the type name to the names of the types in | ||
* the tuple | ||
*/ | ||
private inline def deriveReadsForSum[A, T <: Tuple]( | ||
typ: String | ||
)(using nameCodingStrategy: NameCodingStrategy): Option[Reads[A]] = | ||
inline erasedValue[T] match | ||
case _: EmptyTuple => None | ||
case _: (h *: t) => | ||
deriveReads[h](typ) match | ||
case None => deriveReadsForSum[A, t](typ) | ||
case Some(value) => Some(value.asInstanceOf[Reads[A]]) | ||
|
||
private inline def deriveReads[A](typ: String)(using nameCodingStrategy: NameCodingStrategy): Option[Reads[A]] = | ||
summonFrom { | ||
case m: Mirror.ProductOf[A] => | ||
// product (case class or case object) | ||
val name = constValue[m.MirroredLabel] | ||
if typ == nameCodingStrategy(name) | ||
then Some(summonInline[Reads[A]]) | ||
else None | ||
case m: Mirror.SumOf[A] => | ||
// sum (trait) | ||
deriveReadsForSum[A, m.MirroredElemTypes](typ) | ||
case v: ValueOf[A] => | ||
// Singleton type (object without `case` modifier) | ||
val name = singletonName[A] | ||
if typ == nameCodingStrategy(name) | ||
then Some(summonInline[Reads[A]]) | ||
else None | ||
} | ||
end deriveReads | ||
end FlatTypeReads |
79 changes: 79 additions & 0 deletions
79
play-json-generic/src/main/scala-3/com/evolution/playjson/generic/FlatTypeWrites.scala
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,79 @@ | ||
package com.evolution.playjson.generic | ||
|
||
import play.api.libs.json.* | ||
|
||
import scala.deriving.Mirror | ||
import scala.compiletime.* | ||
|
||
/** | ||
* This is a helper class for creating a `Writes` instance for a sealed trait hierarchy. | ||
* It will add a `type` field to the JSON object with the value of the subtype's simple name (without package prefix). | ||
* | ||
* Example: | ||
* | ||
* {{{ | ||
* sealed trait Parent | ||
* case class Child1(field1: String) extends Parent | ||
* case class Child2(field2: Int) extends Parent | ||
* | ||
* object Child1: | ||
* given OWrites[Child1] = Json.writes[Child1] | ||
* object Child2: | ||
* given OWrites[Child2] = Json.writes[Child2] | ||
* | ||
* val writes: FlatTypeWrites[Parent] = summon[FlatTypeWrites[Parent]] | ||
* | ||
* val json: JsValue = writes.writes(Child1("value")) // {"type": "Child1", "field1": "value"} | ||
* }}} | ||
*/ | ||
trait FlatTypeWrites[A] extends Writes[A]: | ||
override def writes(o: A): JsObject | ||
|
||
object FlatTypeWrites: | ||
def apply[A](using ev: FlatTypeWrites[A]): FlatTypeWrites[A] = ev | ||
|
||
def create[A](f: A => JsObject): FlatTypeWrites[A] = (o: A) => f(o) | ||
|
||
inline given deriveFlatTypeWrites[A](using | ||
m: Mirror.SumOf[A], | ||
nameCodingStrategy: NameCodingStrategy | ||
): FlatTypeWrites[A] = | ||
// Generate writes instances for all subtypes of A and pick | ||
// the one that matches the type of the passed value. | ||
val writes = summonWrites[m.MirroredElemTypes] | ||
create { value => | ||
writes(m.ordinal(value)).asInstanceOf[FlatTypeWrites[A]].writes(value) | ||
} | ||
|
||
/** | ||
* Recursively summon `FlatTypeWrites` instances for all types in the given tuple. | ||
*/ | ||
private inline def summonWrites[T <: Tuple](using | ||
nameCodingStrategy: NameCodingStrategy | ||
): List[FlatTypeWrites[?]] = | ||
inline erasedValue[T] match | ||
case _: EmptyTuple => Nil | ||
case _: (head *: tail) => | ||
summonWrite[head].asInstanceOf[FlatTypeWrites[?]] :: summonWrites[tail] | ||
|
||
private inline def summonWrite[A](using | ||
nameCodingStrategy: NameCodingStrategy | ||
): FlatTypeWrites[A] = | ||
summonFrom { | ||
case m: Mirror.ProductOf[A] => | ||
val name = constValue[m.MirroredLabel] | ||
val writes = summonEnrichedWrites[A](nameCodingStrategy(name)) | ||
create(value => writes.writes(value)) | ||
case m: Mirror.SumOf[A] => | ||
val allWrites = summonWrites[m.MirroredElemTypes] | ||
create { value => | ||
val idx = m.ordinal(value) | ||
allWrites(idx).asInstanceOf[FlatTypeWrites[A]].writes(value) | ||
} | ||
case valueOf: ValueOf[A] => | ||
// Singleton type (object without `case` modifier) | ||
val name = singletonName[A] | ||
val writes = summonEnrichedWrites[A](nameCodingStrategy(name)) | ||
create(value => writes.writes(value)) | ||
} | ||
end FlatTypeWrites |
28 changes: 28 additions & 0 deletions
28
play-json-generic/src/main/scala-3/com/evolution/playjson/generic/NameCodingStrategy.scala
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,28 @@ | ||
package com.evolution.playjson.generic | ||
|
||
trait NameCodingStrategy extends ((String) => String) | ||
|
||
trait LowPriority: | ||
given default: NameCodingStrategy = new NameCodingStrategy() { | ||
override def apply(s: String): String = s | ||
} | ||
|
||
object NameCodingStrategy extends LowPriority | ||
|
||
object NameCodingStrategies: | ||
|
||
private def lowerCaseSepCoding(sep: String): NameCodingStrategy = new NameCodingStrategy() { | ||
override def apply(s: String): String = s.split("(?<!^)(?=[A-Z])").map(_.toLowerCase).mkString(sep) | ||
} | ||
|
||
given kebabCase: NameCodingStrategy = lowerCaseSepCoding("-") | ||
|
||
given snakeCase: NameCodingStrategy = lowerCaseSepCoding("_") | ||
|
||
given noSepCase: NameCodingStrategy = new NameCodingStrategy() { | ||
override def apply(s: String): String = s.toLowerCase | ||
} | ||
|
||
given upperCase: NameCodingStrategy = new NameCodingStrategy() { | ||
override def apply(s: String): String = s.toUpperCase | ||
} |
7 changes: 7 additions & 0 deletions
7
play-json-generic/src/main/scala-3/com/evolution/playjson/generic/NestedTypeFormat.scala
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,7 @@ | ||
package com.evolution.playjson.generic | ||
|
||
import play.api.libs.json.OFormat | ||
|
||
object NestedTypeFormat: | ||
def apply[A](using reads: NestedTypeReads[A], writes: NestedTypeWrites[A]): OFormat[A] = | ||
OFormat(reads.reads(_), writes.writes(_)) |
Oops, something went wrong.