Created
May 26, 2020 17:57
-
-
Save jodersky/8d92e51c59128ea92d96c2de8746d3d9 to your computer and use it in GitHub Desktop.
Simple ujson-based pickler using Dotty typeclass derivation
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.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