Created
June 27, 2019 14:32
-
-
Save harpocrates/e5b5baf072551572203231f126f0f2f6 to your computer and use it in GitHub Desktop.
Quick example demonstrating custom type-safe serialization with typeclasses and generic programming
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
import scala.reflect.ClassTag | |
import shapeless._ | |
import shapeless.labelled._ | |
/* | |
* Simple Json type | |
*/ | |
sealed trait Json | |
object Json { | |
def apply[T](implicit ser: JsSerialization[T]) = ser | |
def serialize[T](t: T)(implicit ser: JsSerialization[T]) = ser.toJson(t) | |
def deserialize[T](j: Json)(implicit ser: JsSerialization[T]) = ser.fromJson(j) | |
} | |
case class JsObject(map: Map[Symbol, Json]) extends Json { | |
override def toString = | |
map.toList | |
.map { case (k,v) => " \"" + k.name + "\": " + v } | |
.mkString("{", ",", " }") | |
} | |
case class JsNumber(num: Double) extends Json { | |
override def toString = num.toString | |
} | |
case class JsString(str: String) extends Json { | |
override def toString = "\"" + str + "\"" | |
} | |
case class JsArray(arr: Array[Json]) extends Json { | |
override def toString = arr.toList.mkString("[ ", ", ", " ]") | |
} | |
case object JsNull extends Json { | |
override def toString = "null" | |
} | |
/* | |
* JSON serialization interface | |
*/ | |
trait JsSerialization[T] { | |
def toJson(t: T): Json | |
def fromJson(json: Json): T | |
} | |
object JsSerialization extends SimpleImpls with GenericImpls | |
case class JsError(msg: String) extends Exception(msg) | |
/* | |
* Serializing basic JSON types | |
*/ | |
trait SimpleImpls { | |
// Serialize doubles | |
implicit val doubleImpl = new JsSerialization[Double] { | |
def toJson(d: Double) = JsNumber(d) | |
def fromJson(json: Json) = json match { | |
case JsNumber(double) => double | |
case other => throw JsError(s"Cannot read Double from $other") | |
} | |
} | |
// Serialialize strings | |
implicit val stringImpl = new JsSerialization[String] { | |
def toJson(s: String) = JsString(s) | |
def fromJson(json: Json) = json match { | |
case JsString(string) => string | |
case other => throw JsError(s"Cannot read String from $other") | |
} | |
} | |
// Serialize arrays | |
implicit def arrayImpl[T](implicit | |
tImpl: Lazy[JsSerialization[T]], | |
classTag: ClassTag[T] | |
) = new JsSerialization[Array[T]] { | |
def toJson(a: Array[T]) = JsArray(a.map(t => tImpl.value.toJson(t))) | |
def fromJson(json: Json): Array[T] = json match { | |
case JsArray(array) => array.map(tImpl.value.fromJson) | |
case other => throw JsError(s"Cannot read Array from $other") | |
} | |
} | |
// Serialize options | |
implicit def optionImpl[T](implicit | |
tImpl: Lazy[JsSerialization[T]] | |
) = new JsSerialization[Option[T]] { | |
def toJson(o: Option[T]) = o.fold[Json](JsNull)(tImpl.value.toJson) | |
def fromJson(json: Json): Option[T] = json match { | |
case JsNull => None | |
case other => Some(tImpl.value.fromJson(other)) | |
} | |
} | |
} | |
/* | |
* Generic JSON impls | |
*/ | |
trait GenericImpls { | |
implicit def genericImpl[T, Repr <: HList](implicit | |
lgen: LabelledGeneric.Aux[T, Repr], | |
gimpl: GenericJsSerialization[Repr] | |
) = new JsSerialization[T] { | |
def toJson(t: T): Json = JsObject(gimpl.toJsonMap(lgen.to(t))) | |
def fromJson(json: Json): T = json match { | |
case JsObject(map) => lgen.from(gimpl.fromJsonMap(map)) | |
case other => throw JsError(s"Cannot read generic object from $other") | |
} | |
} | |
sealed trait GenericJsSerialization[T <: HList] { | |
def toJsonMap(t: T): Map[Symbol, Json] | |
def fromJsonMap(obj: Map[Symbol, Json]): T | |
} | |
implicit val hnilImpl = new GenericJsSerialization[HNil] { | |
def toJsonMap(h: HNil) = Map.empty | |
def fromJsonMap(obj: Map[Symbol, Json]) = if (obj.nonEmpty) { | |
throw JsError(s"Leftover fields ${JsObject(obj)}") | |
} else { | |
HNil | |
} | |
} | |
implicit def hconsImpl[Name <: Symbol, Value, T <: HList](implicit | |
keyName: Witness.Aux[Name], | |
headImpl: Lazy[JsSerialization[Value]], | |
tailImpl: Lazy[GenericJsSerialization[T]] | |
) = new GenericJsSerialization[FieldType[Name, Value] :: T] { | |
def toJsonMap(h: FieldType[Name, Value] :: T) = { | |
val newEntry = (keyName.value -> headImpl.value.toJson(h.head)) | |
tailImpl.value.toJsonMap(h.tail) + newEntry | |
} | |
def fromJsonMap(obj: Map[Symbol, Json]) = { | |
val fieldJson = obj.getOrElse( | |
keyName.value, | |
throw JsError(s"Cannot find property ${keyName.value} in object $obj") | |
) | |
val head = headImpl.value.fromJson(fieldJson) | |
val tail = tailImpl.value.fromJsonMap(obj - keyName.value) | |
field[Name](head) :: tail | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment