Skip to content

Instantly share code, notes, and snippets.

@jodersky
Created May 26, 2020 17:57
Show Gist options
  • Save jodersky/8d92e51c59128ea92d96c2de8746d3d9 to your computer and use it in GitHub Desktop.
Save jodersky/8d92e51c59128ea92d96c2de8746d3d9 to your computer and use it in GitHub Desktop.
Simple ujson-based pickler using Dotty typeclass derivation
import scala.deriving._
import scala.compiletime.{erasedValue, summonInline}
// super primitive (but composable via typeclass derivation) JSON reader
trait Reader[A] {
def read(json: ujson.Value): A
}
object Reader {
given Reader[Int] = new Reader[Int] {
def read(json: ujson.Value) = json.num.toInt
}
given Reader[Double] = new Reader[Double] {
def read(json: ujson.Value) = json.num
}
given Reader[Boolean] = new Reader[Boolean] {
def read(json: ujson.Value) = json.bool
}
given Reader[String] = new Reader[String] {
def read(json: ujson.Value) = json.str
}
given seqReader[A](using elemReader: Reader[A]) as Reader[Seq[A]] = new Reader[Seq[A]] {
def read(json: ujson.Value): Seq[A] = json.arr.toSeq.map(elem => elemReader.read(elem))
}
given optionReader[A](using elemReader: Reader[A]) as Reader[Option[A]] = new Reader[Option[A]] {
def read(json: ujson.Value): Option[A] = if (json.isNull) None else Some(elemReader.read(json))
}
given mapReader[A](using elemReader: Reader[A]) as Reader[Map[String, A]] = new Reader[Map[String, A]] {
def read(json: ujson.Value): Map[String, A] = json.obj.map{case (k, v) => k -> elemReader.read(v)}.toMap
}
inline def tupleReads[Names <: Tuple, Types <: Tuple](fields: ujson.Obj): Tuple = inline (erasedValue[Names], erasedValue[Types]) match {
case _: (Unit, Unit) => ()
case _: (*:[n,ns], *:[t, ts]) => {
val name: String = valueOf[n].toString // singleton types
val reader: Reader[t] = summonInline[Reader[t]]
val valueOrNull: ujson.Value = fields.obj.getOrElse(name, ujson.Null)
val head: t = try {
reader.read(valueOrNull)
} catch {
case ex: Exception =>
throw IllegalArgumentException(s"error parsing field: $name", ex) // we rethrow with some added debug info
}
head *: tupleReads[ns, ts](fields)
}
}
inline def derived[A](using m: Mirror.Of[A]): Reader[A] = inline m match {
case s: Mirror.SumOf[A] => ??? // TODO: implement reader for sum types
case p: Mirror.ProductOf[A] =>
new Reader[A] {
def read(json: ujson.Value): A = {
val tuple: Tuple = tupleReads[m.MirroredElemLabels, m.MirroredElemTypes](json.obj)
p.fromProduct(tuple.asInstanceOf[Product])
}
}
}
}
import scala.collection.mutable
trait Writer[A] {
def write(a: A): ujson.Value
}
object Writer {
given Writer[Int] = new Writer[Int] {
def write(a: Int) = ujson.Num(a.toDouble)
}
given Writer[Double] = new Writer[Double] {
def write(a: Double) = ujson.Num(a)
}
given Writer[Boolean] = new Writer[Boolean] {
def write(a: Boolean) = ujson.Bool(a)
}
given Writer[String] = new Writer[String] {
def write(a: String) = ujson.Str(a)
}
given seqWriter[A](using elemWriter: Writer[A]) as Writer[Seq[A]] = new Writer[Seq[A]] {
def write(a: Seq[A]) = ujson.Arr.from(a.map(e => elemWriter.write(e)))
}
given optionWriter[A](using elemWriter: Writer[A]) as Writer[Option[A]] = new Writer[Option[A]] {
def write(a: Option[A]) = a match {
case None => ujson.Null
case Some(a) => elemWriter.write(a)
}
}
given mapWriter[A](using elemWriter: Writer[A]) as Writer[Map[String, A]] = new Writer[Map[String, A]] {
def write(a: Map[String, A]) = ujson.Obj.from(a.map{case (key, value) => key -> elemWriter.write(value)})
}
inline def tupleWrites[Names <: Tuple, Types <: Tuple](out: mutable.LinkedHashMap[String, ujson.Value], valueIterator: Iterator[Any]): Unit =
inline (erasedValue[Names], erasedValue[Types]) match {
case _: (Unit, Unit) => ()
case _: (*:[n,ns], *:[t, ts]) =>
val name: String = valueOf[n].toString
val writer: Writer[t] = summonInline[Writer[t]]
val value = valueIterator.next().asInstanceOf[t]
out += name -> writer.write(value)
tupleWrites[ns, ts](out, valueIterator)
}
inline def derived[A](using m: Mirror.Of[A]): Writer[A] = inline m match {
case s: Mirror.SumOf[A] => ??? // TODO: implement sum types
case p: Mirror.ProductOf[A] =>
new Writer[A]{
def write(a: A): ujson.Value = {
val data = mutable.LinkedHashMap.empty[String, ujson.Value]
tupleWrites[m.MirroredElemLabels, m.MirroredElemTypes](data, a.asInstanceOf[Product].productIterator)
ujson.Obj(data)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment