Skip to content

Commit

Permalink
Add documentation for FlatType* functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Z1kkurat committed May 30, 2023
1 parent b1c6690 commit 7ee692d
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ 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)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ 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 _)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ 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.
*
* 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]

Expand All @@ -14,6 +38,11 @@ object FlatTypeReads:

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
Expand All @@ -22,25 +51,30 @@ object FlatTypeReads:
for {
obj <- json.validate[JsObject]
typ <- (obj \ "type").validate[String]
result <- deriveReads[A](obj, typ) match
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](
json: JsObject,
typ: String
)(using nameCodingStrategy: NameCodingStrategy): Option[Reads[A]] = {
)(using nameCodingStrategy: NameCodingStrategy): Option[Reads[A]] =
inline erasedValue[T] match
case _: EmptyTuple => None
case _: (h *: t) =>
deriveReads[h](json, typ) match
case None => deriveReadsForSum[A, t](json, typ)
deriveReads[h](typ) match
case None => deriveReadsForSum[A, t](typ)
case Some(value) => Some(value.asInstanceOf[Reads[A]])
}

private inline def deriveReads[A](json: JsObject, typ: String)(using nameCodingStrategy: NameCodingStrategy): Option[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)
Expand All @@ -50,7 +84,7 @@ object FlatTypeReads:
else None
case m: Mirror.SumOf[A] =>
// sum (trait)
deriveReadsForSum[A, m.MirroredElemTypes](json, typ)
deriveReadsForSum[A, m.MirroredElemTypes](typ)
case v: ValueOf[A] =>
// Singleton type (object without `case` modifier)
val name = singletonName[A]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ 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

Expand All @@ -17,11 +38,16 @@ object FlatTypeWrites:
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[?]] =
Expand Down

0 comments on commit 7ee692d

Please sign in to comment.