Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement auto derivation for scala 3 #68

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import $file.forProductN

val `2.12` = "2.12.15"
val `2.13` = "2.13.10"
val `3` = "3.1.0"
val `3` = "3.3.4"

val scalaTest = ivy"org.scalatest::scalatest:3.2.10"
val json4sVersion = Map(4 -> "4.0.6", 3 -> "3.6.12")
Expand Down
48 changes: 48 additions & 0 deletions ninny/src-3/nrktkt/ninny/Annotation.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package nrktkt.ninny

import scala.quoted._

object Annotation {
type Aux[A, O] = Annotation[A] { type Out = O }

transparent inline given derived[T]: Annotation[T] =
${genAnnotationImpl[T]}

def genAnnotationImpl[T: Type](using Quotes): Expr[Annotation[T]] = {
import quotes.reflect._
val classSymbol = TypeRepr.of[T].typeSymbol
val objectSymbol = classSymbol.companionModule
val tycons = TypeRepr.of[scala.*:[_, _]] match
case AppliedType(a, _) => a
val (tupleExpr, tupleType) = classSymbol.caseFields.reverse.foldLeft[(Term, TypeRepr)]((Expr(EmptyTuple).asTerm, TypeRepr.of[EmptyTuple.type])){ case ((tpleExpr, tpleTpe), symbol) =>
tpleTpe.asType match
case ('[t]) =>
val valueOpt =
symbol.annotations.collect {
case Apply(Select(New(ident),_), List(Literal(StringConstant(strVal)))) => strVal
} match
case head :: next => Some(head)
case Nil => None
valueOpt match
case Some(value) =>
val tpe = ConstantType(StringConstant(value))
tpe.asType match
case '[x] =>
(Apply(TypeApply(Select.unique(tpleExpr, "*:"), List(TypeTree.of[x], TypeTree.of[t])), List(Literal(StringConstant(value)))), AppliedType(tycons, List(tpe, tpleTpe)))
case None =>
(Apply(TypeApply(Select.unique(tpleExpr, "*:"), List(TypeTree.of[Null], TypeTree.of[t])), List('{null}.asTerm)), AppliedType(tycons, List(TypeRepr.of[Null], tpleTpe)))
}
tupleType.asType match
case '[t] =>
'{
new Annotation[T] {
type Out = t
def apply(): Out = ${tupleExpr.asExprOf[t]}
}.asInstanceOf[Annotation.Aux[T, t]]
}
}
}
trait Annotation[A] {
type Out <: Tuple
def apply(): Out
}
41 changes: 41 additions & 0 deletions ninny/src-3/nrktkt/ninny/Auto.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package nrktkt.ninny

import scala.deriving.Mirror

trait AutoToJson {
implicit inline def lgToJson[
A <: Product,
OverridingNames <: Tuple](implicit
mirror: Mirror.ProductOf[A],
names: Annotation.Aux[A, OverridingNames]): ToSomeJson[A] =
ToJsonAuto.labelledGenericToJson.toJson
}

trait AutoFromJson {
implicit inline def lgFromJson[
A <: Product,
Values <: Tuple,
Defaults <: Tuple,
OverridingNames <: Tuple,
Size <: Numlike
](implicit
mirror: Mirror.ProductOf[A],
ev: Size =:= Auto.WrappedSize[mirror.MirroredElemTypes],
defaults: Defaults.Aux[A, Defaults],
annotation: Annotation.Aux[A, OverridingNames],
from: (Sized[List[String], Size], Defaults) => FromJson[Values]
): FromJson[A] = FromJsonAuto.labelledGenericFromJson.fromJson
}

object Auto extends AutoToJson with AutoFromJson {
private[ninny] type OverrideNames[Names <: Tuple, Overriden <: Tuple] <: Tuple =
(Names, Overriden) match
case (aHead *: aTail, Null *: bTail) => aHead *: OverrideNames[aTail, bTail]
case (aHead *: aTail, str *: bTail) => str *: OverrideNames[aTail, bTail]
case (EmptyTuple, _) => EmptyTuple

private[ninny] type WrappedSize[T <: Tuple] = T match
case a *: EmptyTuple => Added[Zero]
case a *: b => Added[WrappedSize[b]]

}
39 changes: 39 additions & 0 deletions ninny/src-3/nrktkt/ninny/DefaultOptions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package nrktkt.ninny
import scala.quoted._

object DefaultOptions {
type Aux[A, O] = DefaultOptions[A] { type Out = O }

transparent inline given derived[T]: DefaultOptions[T] =
${genDefaultsImpl[T]}

def genDefaultsImpl[T: Type](using Quotes): Expr[DefaultOptions[T]] = {
import quotes.reflect._
val classSymbol = TypeRepr.of[T].typeSymbol
val objectSymbol = classSymbol.companionModule
val tycons = TypeRepr.of[scala.*:[_, _]] match
case AppliedType(a, _) => a
val (tupleExpr, tupleType) = classSymbol.caseFields.zipWithIndex.reverse.foldLeft[(Term, TypeRepr)]((Expr(EmptyTuple).asTerm, TypeRepr.of[EmptyTuple.type])){ case ((tpleExpr, tpleTpe), (symbol, idx)) =>
(tpleTpe.asType, TypeRepr.of[T].memberType(symbol).asType) match
case ('[t], '[g]) =>
if symbol.flags.is(Flags.HasDefault) then
val value = objectSymbol.declaredMethod("$lessinit$greater$default$" + (idx + 1)).head
val expr = '{Some(${Select(Ref(objectSymbol), value).asExprOf[g]})}
(Apply(TypeApply(Select.unique(tpleExpr, "*:"), List(TypeTree.of[Option[g]], TypeTree.of[t])), List(expr.asTerm)), AppliedType(tycons, List(TypeRepr.of[Option[g]], tpleTpe)))
else
(Apply(TypeApply(Select.unique(tpleExpr, "*:"), List(TypeTree.of[Option[g]], TypeTree.of[t])), List('{None}.asTerm)), AppliedType(tycons, List(TypeRepr.of[Option[g]], tpleTpe)))
}
tupleType.asType match
case '[t] =>
'{
new DefaultOptions[T] {
type Out = t
def apply(): Out = ${tupleExpr.asExprOf[t]}
}.asInstanceOf[DefaultOptions.Aux[T, t]]
}
}
}
trait DefaultOptions[A] {
type Out <: Tuple
def apply(): Out
}
58 changes: 57 additions & 1 deletion ninny/src-3/nrktkt/ninny/FromJsonAutoImpl.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,59 @@
package nrktkt.ninny

trait FromJsonAutoImpl {}
import scala.annotation.nowarn
import scala.deriving.Mirror
import nrktkt.ninny.DefaultOptions._
import scala.compiletime.ops.int
import scala.compiletime.ops.int._

trait FromJsonAutoImpl {

implicit def useDefaults[A, O <: Tuple](implicit
defaults: DefaultOptions.Aux[A, O]
): Defaults.Aux[A, O] =
new Defaults[A] {
type Out = O
def apply() = defaults()
}

implicit inline def labelledGenericFromJson[
A <: Product,
Values <: Tuple,
Defaults <: Tuple,
OverridingNames <: Tuple,
Size <: Numlike
](implicit
mirror: Mirror.ProductOf[A],
ev: Size =:= Auto.WrappedSize[mirror.MirroredElemTypes],
defaults: Defaults.Aux[A, Defaults],
annotation: Annotation.Aux[A, OverridingNames],
from: (Sized[List[String], Size], Defaults) => FromJson[Values]
): FromJsonAuto[A] = {
val names = Sized[A, OverridingNames, Size](mirror)
val fromJson = from(names, defaults()).map(mirror.fromProduct(_))
new FromJsonAuto[A](fromJson)
}

}

trait Defaults[A] {
type Out <: Tuple
def apply(): Out
}

object Defaults {
type Aux[A, O <: Tuple] = Defaults[A] { type Out = O }

implicit def ignoreDefaults[A, O <: Tuple](implicit
defaults: DefaultOptions.Aux[A, O],
): Defaults.Aux[A, O] =
new Defaults[A] {
type Out = O
def apply() =
def makeEmptyTuple(tuple: Tuple): Tuple =
tuple match
case head *: tail => None *: makeEmptyTuple(tail)
case EmptyTuple => EmptyTuple
makeEmptyTuple(defaults()).asInstanceOf[Out]
}
}
5 changes: 5 additions & 0 deletions ninny/src-3/nrktkt/ninny/JsonName.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package nrktkt.ninny

import scala.annotation.StaticAnnotation

case class JsonName(name: String) extends StaticAnnotation
16 changes: 15 additions & 1 deletion ninny/src-3/nrktkt/ninny/ToJsonAutoImpl.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
package nrktkt.ninny

import scala.deriving.Mirror
import scala.deriving.Mirror.Sum
import scala.collection.View.Zip

trait ToJsonAutoImpl {


implicit inline def labelledGenericToJson[A <: Product, OverridingNames <: Tuple](using mirror: Mirror.ProductOf[A], annotations: Annotation.Aux[A, OverridingNames]): ToJsonAuto[A] = {
new ToJsonAuto[A]((a: A) => {
val keys = scala.compiletime.constValueTuple[Auto.OverrideNames[mirror.MirroredElemLabels, OverridingNames]]
val values = Tuple.fromProductTyped(a)
val zip = keys.zip(values)

scala.compiletime.summonInline[ToSomeJsonObject[Tuple.Zip[Auto.OverrideNames[mirror.MirroredElemLabels, OverridingNames], mirror.MirroredElemTypes]]].toSome(zip.asInstanceOf[Tuple.Zip[Auto.OverrideNames[mirror.MirroredElemLabels, OverridingNames], mirror.MirroredElemTypes]])
})
}

}
72 changes: 71 additions & 1 deletion ninny/src-3/nrktkt/ninny/VersionSpecificFromJsonInstances.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,75 @@
package nrktkt.ninny

import scala.util.Success
import scala.annotation.nowarn
import scala.compiletime.ops.int.{+, -, S}

import nrktkt.ninny.ast.JsonValue
import scala.util.Failure
import scala.deriving.Mirror
import scala.util.Try

trait VersionSpecificFromJsonInstances {


implicit def recordFromJson[
Head,
DefaultHead <: Option[Head],
Tail <: Tuple,
DefaultTail <: Tuple,
TailLen <: Numlike
](implicit
headFromJson: FromJson[Head],
tailFromJson: (
Sized[List[String], TailLen],
DefaultTail
) => FromJson[Tail]
): (
Sized[List[String], Added[TailLen]],
DefaultHead *: DefaultTail
) => FromJson[Head *: Tail] = (names, defaults) => {
val defaultHeadFromJson: FromJson[Head] = maybe =>
(maybe, defaults.head) match {
case (maybe: Some[JsonValue], _) => headFromJson.from(maybe)
case (_, default: Some[Head]) => Success(default.value)
case _ => headFromJson.from(None)
}

FromJson.fromSome { json =>
for {
head <- defaultHeadFromJson
.from(json / Sized.value(names).head)
.recoverWith {
case e: JsonFieldException =>
Failure(
new JsonFieldException(
e.message,
s"${Sized.value(names).head}.${e.field}",
e
)
)
case e: Exception =>
Failure(new JsonFieldException(e.getMessage, Sized.value(names).head, e))
}
tail <- tailFromJson(names.tail, defaults.tail).from(json)
} yield head *: tail
}
}

implicit val emptyTupleFromJson: ((Sized[List[String], Zero]), EmptyTuple) => FromJson[EmptyTuple] =
(_, _) => _ => Success(EmptyTuple)
}

opaque type Sized[l <: List[String], Size] = List[String]

object Sized {
def value[L <: List[String], Size](a: Sized[L, Size]): List[String] = a
inline def apply[T <: Product, OverridingNames <: Tuple, Size <: Numlike](mirror: Mirror.ProductOf[T]): Sized[List[String], Size] =
scala.compiletime.constValueTuple[Auto.OverrideNames[mirror.MirroredElemLabels, OverridingNames]].asInstanceOf[Tuple].toList.asInstanceOf[List[String]]
}

extension [L <: List[String], Size <: Numlike] (s: Sized[L, Added[Size]]) def tail: Sized[L, Size] = s.tail


type Numlike
type Added[T] <: Numlike
type Zero <: Numlike
26 changes: 25 additions & 1 deletion ninny/src-3/nrktkt/ninny/VersionSpecificToJsonInstances.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
package nrktkt.ninny

import nrktkt.ninny.ast.JsonObject
import nrktkt.ninny.NullPointerBehavior
import nrktkt.ninny.NullPointerBehavior.Handle

trait VersionSpecificToJsonInstances {

implicit def recordToJson[V, G <: String, Tail <: Tuple](implicit
valueToJson: ToJson[V],
tailToJson: ToSomeJsonObject[Tail],
nullPointerBehavior: NullPointerBehavior
): ToSomeJsonObject[(G, V) *: Tail] = {
ToJson { case (name, value) *: tail =>
val tailJson = tailToJson.toSome(tail)
nullPointerBehavior match {
case Handle(behavior) if value == null =>
tailJson + (name -> behavior())
case _ =>
val maybeValueJson = valueToJson.to(value)
maybeValueJson match {
case Some(valueJson) => tailJson + (name -> valueJson)
case None => tailJson
}
}
}
}

implicit val hNilToJson: ToSomeJsonObject[EmptyTuple] = _ => JsonObject(Map.empty)
}
3 changes: 2 additions & 1 deletion ninny/src/nrktkt/ninny/FromJsonInstances.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.typesafe.scalalogging.LazyLogging
import java.util.Base64
import scala.collection.compat.immutable.ArraySeq
import scala.util.control.NonFatal
import scala.annotation.nowarn

trait FromJsonInstances
extends VersionSpecificFromJsonInstances
Expand Down Expand Up @@ -234,7 +235,7 @@ object FromJsonInstances extends FromJsonInstances

trait LowPriorityFromJsonInstances {
// this roundabout way to import compiler flag for higher kinded types avoids a deprecation warning when building for 2.13
protected implicit lazy val hkhack: languageFeature.higherKinds.type =
@nowarn protected implicit lazy val hkhack: languageFeature.higherKinds.type =
scala.languageFeature.higherKinds

implicit def collectionFromJson[F[_], A](implicit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ object NullPointerBehavior {
val someNull = Some(JsonNull)
() => someNull
}
implicit def default = PassThrough
implicit def default: NullPointerBehavior = PassThrough
}
Loading