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 3 commits 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ target/
.vscode
.ammonite
.bsp
*.iml
*.iml
.DS_Store
1 change: 1 addition & 0 deletions .mill-jvm-opts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-Xss10m
12 changes: 6 additions & 6 deletions AUTOCONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Rather than implementing `ToJson` and `FromJson` by hand, you can generate them
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/userguide/SemiAuto.scala'
file='ninny/test/src/nrktkt/ninny/userguide/SemiAuto.scala'
lines='11:16'
></sauce-code>

Expand All @@ -17,8 +17,8 @@ If you like you can even skip the declaration by mixing in `AutoToJson` or
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/userguide/FullAuto.scala'
lines='9:18'
file='ninny/test/src/nrktkt/ninny/userguide/FullAuto.scala'
lines='10:19'
></sauce-code>

## Modifying field names with annotations
Expand All @@ -28,7 +28,7 @@ You can change the name of a field being read to/from JSON using the `@JsonName`
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/userguide/Annotations.scala'
file='ninny/test/src/nrktkt/ninny/userguide/Annotations.scala'
lines='10:19'
></sauce-code>

Expand All @@ -39,7 +39,7 @@ If your case class has optional parameters then you can use their default values
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/userguide/DefaultValues.scala'
file='ninny/test/src/nrktkt/ninny/userguide/DefaultValues.scala'
lines='9:17'
></sauce-code>

Expand All @@ -52,6 +52,6 @@ By providing an instance of `NullPointerBehavior` in the scope of your `ToJson`
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/example/Userguide.scala'
file='ninny/test/src/nrktkt/ninny/example/Userguide.scala'
lines='207:214'
></sauce-code>
16 changes: 8 additions & 8 deletions USERGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/userguide/Reading.scala'
file='ninny/test/src/nrktkt/ninny/userguide/Reading.scala'
lines='8:63'
></sauce-code>

# Writing values to JSON
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/userguide/Writing.scala'
file='ninny/test/src/nrktkt/ninny/userguide/Writing.scala'
lines='9:23'
></sauce-code>

Expand All @@ -38,7 +38,7 @@
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/userguide/Writing.scala'
file='ninny/test/src/nrktkt/ninny/userguide/Writing.scala'
lines='25:47'
></sauce-code>

Expand All @@ -48,7 +48,7 @@
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/userguide/Writing.scala'
file='ninny/test/src/nrktkt/ninny/userguide/Writing.scala'
lines='52:74'
></sauce-code>

Expand All @@ -60,7 +60,7 @@ You can use ninny's dynamic update syntax easly to replace values way down in th
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/userguide/Updating.scala'
file='ninny/test/src/nrktkt/ninny/userguide/Updating.scala'
lines='5:18'
></sauce-code>

Expand All @@ -69,7 +69,7 @@ You can use ninny's dynamic update syntax easly to replace values way down in th
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/userguide/DomainTo.scala'
file='ninny/test/src/nrktkt/ninny/userguide/DomainTo.scala'
lines='7:51'
></sauce-code>

Expand All @@ -78,7 +78,7 @@ You can use ninny's dynamic update syntax easly to replace values way down in th
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/userguide/DomainFrom.scala'
file='ninny/test/src/nrktkt/ninny/userguide/DomainFrom.scala'
lines='22:49'
></sauce-code>

Expand All @@ -90,7 +90,7 @@ automatically using
<sauce-code
repo='nrktkt/ninny-json'
lang='scala'
file='ninny/test/src-2/nrktkt/ninny/userguide/SemiAuto.scala'
file='ninny/test/src/nrktkt/ninny/userguide/SemiAuto.scala'
lines='11:16'
></sauce-code>

Expand Down
12 changes: 8 additions & 4 deletions 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 Expand Up @@ -47,9 +47,13 @@ class Ninny(val crossScalaVersion: String)
"-feature",
"-unchecked",
"-deprecation"
) ++ (if (crossScalaVersion != `3`)
Seq("-Ywarn-macros:after", "-Ywarn-unused")
else None)
) ++ (
if (crossScalaVersion != `3`) Seq("-Ywarn-macros:after", "-Ywarn-unused")
else None
) ++ (
if (crossScalaVersion == `2.12`) Seq("-language:higherKinds")
else None
)

def ivyDeps =
Agg(
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]])
})
}

}
Loading