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

feat: scala 3 derivation #8

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
5 changes: 3 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ val Scala2_13 = "2.13.14"
val Scala3 = "3.3.4"
val FastParse = "3.1.1"
val Shapeless = "2.3.12"
val Shapeless3 = "3.4.3"
val ScalaCheck = "1.18.1"
val ScalaTest = "3.2.19"
val ScalaJavaTime = "2.6.0"
Expand Down Expand Up @@ -60,12 +61,12 @@ lazy val toml =
),
libraryDependencies ++= {
if(scalaVersion.value.startsWith("3."))
Seq.empty
Seq("org.typelevel" %%% "shapeless3-deriving" % Shapeless3)
else
Seq("com.chuusai" %%% "shapeless" % Shapeless)
},
libraryDependencies ++= {
if(virtualAxes.value.contains(VirtualAxis.jvm)) Seq.empty else
if(virtualAxes.value.contains(VirtualAxis.jvm)) Seq.empty else
Seq(
"io.github.cquiroz" %%% "scala-java-time" % ScalaJavaTime,
)
Expand Down
5 changes: 1 addition & 4 deletions core/src/main/scala-2/toml/TomlVersionSpecific.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ package toml

import shapeless._

trait TomlVersionSpecific {

def parse(toml: String, extensions: Set[Extension] = Set()): Either[Parse.Error, Value.Tbl]

trait TomlVersionSpecific { self: Toml.type =>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be this can be made private[toml]?

class CodecHelperGeneric[A] {
def apply[D <: HList, R <: HList](table: Value.Tbl)(implicit
generic: LabelledGeneric.Aux[A, R],
Expand Down
34 changes: 33 additions & 1 deletion core/src/main/scala-3/toml/TomlVersionSpecific.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
package toml

trait TomlVersionSpecific
import toml.derivation.DefaultParams
import shapeless3.deriving.K0.ProductGeneric
trait TomlVersionSpecific:
self:Toml.type =>
final def parseAs[T](input: Value.Tbl | String)(
using Codec[T], DefaultParams[T]
):Either[Parse.Error, T] = parseAs(input, Set.empty)

final def parseAs[T](
input: Value.Tbl | String,
extensions: Set[Extension]
)(using codec: Codec[T], D: DefaultParams[T]
):Either[Parse.Error, T] = input match
case toml: String =>
parse(toml, extensions).flatMap(codec(_,D.defaultParams,0))
case table: Value.Tbl => codec(
table,
D.defaultParams,
0
)
final class CodecHelperValue[A]:
def apply(value: Value)(using codec: Codec[A]): Either[Parse.Error, A] =
codec(value, Map(), 0)

def apply(toml: String, extensions: Set[Extension] = Set())(using
codec: Codec[A]
): Either[Parse.Error, A] =
parse(toml, extensions).right.flatMap(codec(_, Map(), 0))
end CodecHelperValue

final def parseAsValue[T]: CodecHelperValue[T] = new CodecHelperValue[T]


123 changes: 123 additions & 0 deletions core/src/main/scala-3/toml/derivation/DerivedProductCodec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package toml
package derivation
import toml.Codec.Defaults
import toml.Codec.Index
import shapeless3.deriving.*

trait DerivedProductCodec[P] extends Codec[P]
object DerivedProductCodec:
private def fieldNotFound[T](label: String): Either[Parse.Error, T] =
Left(Nil, s"Cannot resolve `${label}`")

given [P <: Product](using
labelled: Labelling[P],
inst: K0.ProductInstances[Codec, P],
d: DefaultParams[P]
): DerivedProductCodec[Option[P]] with
type Result[A] = Either[Parse.Error, Option[A]]
override def optional: Boolean = true
def apply(value: Value, __ : Defaults, ___ : Int): Result[P] =
val labels = labelled.elemLabels.iterator.zipWithIndex

val decodeField =
[t] => (codec: Codec[t]) =>
value match
case Value.Tbl(map) =>
val (witnessName, _) = labels.next()
map.get(witnessName) match
case Some(value) =>
codec(value, d.defaultParams, 0)
.map(Some(_))
.left.map((a,m) => (witnessName +: a, m))
case None =>
Right(
d.defaultParams.get(witnessName)
.map(_.asInstanceOf[t])
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultParams is Map[Strng, Any], so you need asInstanceOf

)
case value =>
Left((Nil, "Not Implemented"))
val combineFields: Ap[[a] =>> Result[a]] =
[a, b] =>
(ff: Result[a => b], fa: Result[a]) =>
(fa, ff) match
case (Left(e), Right(_)) => Left(e)
case (_, Left(e)) => Left(e)
case (Right(Some(a)), Right(Some(f))) => Right(Some(f(a)))
case (Right(_), Right(_)) => Right(None)

inst.constructA[Result](decodeField)(
pure = [a] => (x: a) => Right(Some(x)),
map = [a, b] => (fa: Result[a], f: a => b) => fa.map:
case Some(a) => Some(f(a))
case None => None,
combineFields
)


given [P <: Product](using
labelled: Labelling[P],
inst: K0.ProductInstances[Codec, P],
d: DefaultParams[P],
): DerivedProductCodec[P] with
override def apply(value: Value, __ : Defaults, ___ :Index): Either[Parse.Error, P] =
val labels = labelled.elemLabels.iterator.zipWithIndex
val labelsSet = labelled.elemLabels.toSet

def validateNoExtraField(map: Map[String, Value]) =
map.keySet.diff(labelsSet).headOption match
case None => Right(())
case Some(unknownField) =>
Left((List(unknownField), "Unknown field"))

val decodeField =
[t] => (codec: Codec[t]) =>
value match
case Value.Tbl(map) =>
for
_ <- validateNoExtraField(map)
(witnessName, _) = labels.next()
result <- map.get(witnessName) match
case Some(value) => codec.apply(value, d.defaultParams, 0)
.left.map((a,m) => (witnessName +: a, m))
case None =>
d.defaultParams.get(witnessName) match
case None if codec.optional => Right(None.asInstanceOf[t])
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional returns true if and only if codec is of type Codec[Option[?]], so it is safe to use None.asInstanceOf[t] where t is of type Option[?].

case None => fieldNotFound(witnessName)
case Some(value) => Right(value.asInstanceOf[t])
yield result
case Value.Arr(values) if values.nonEmpty =>
labels.nextOption() match
case Some((_, idx)) if idx < values.length =>
codec.apply(values(idx), d.defaultParams, idx)
.left.map((a,m) => (s"#${idx + 1}" +: a, m))
case Some((witnessName, _)) if d.defaultParams.contains(witnessName) =>
Right(d.defaultParams(witnessName).asInstanceOf[t])
case Some(_) if codec.optional => Right(None.asInstanceOf[t])
case Some((witnessName, _)) =>
fieldNotFound(witnessName)
case None => Left(Nil, "Field not available")
case Value.Arr(values) if values.isEmpty =>
val (witnessName, idx) = labels.next()
if d.defaultParams.contains(witnessName) then
Right(d.defaultParams(witnessName).asInstanceOf[t])
else
if codec.optional then Right(None.asInstanceOf[t])
else fieldNotFound(witnessName)
case _ =>
val (witnessName,_) = labels.next()
fieldNotFound(witnessName)

val combineFields: Ap[[a] =>> Either[Parse.Error, a]] =
[a, b] =>
(ff: Either[Parse.Error, a => b], fa: Either[Parse.Error, a]) =>
(fa, ff) match
case (Left(e),Right(_)) => Left(e)
case (Right(_),Left(e)) => Left(e)
case (Right(a),Right(f)) => Right(f(a))
case (Left((_, _)), Left((path, message))) => Left((path,message))

inst.constructA(decodeField)(
pure = [a] => (x: a) => Right(x),
map = [a, b] => (fa: Either[Parse.Error, a], f: a => b) => fa.map(f),
ap = combineFields
)
13 changes: 13 additions & 0 deletions core/src/main/scala-3/toml/derivation/ProductDefaults.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package toml
package derivation
import shapeless3.deriving.*

trait DefaultParams[P]:
def defaultParams: Map[String, Any]
object DefaultParams:
class DefaultParamsGen[P <: Product](f: () => Map[String,Any]) extends DefaultParams[P] {
final def defaultParams: Map[String, Any] = f()
}
inline given inst[P <: Product](using r: K0.ProductGeneric[P]): DefaultParams[P] = DefaultParamsGen {
() => macros.defaultParams[P]
}
26 changes: 26 additions & 0 deletions core/src/main/scala-3/toml/derivation/ProductDefaultsMacro.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package toml.derivation
import scala.quoted.*
private[toml] object macros:
inline def defaultParams[T]: Map[String, Any] = ${ defaultParmasImpl[T] }

def defaultParmasImpl[T](using quotes: Quotes, tpe: Type[T]): Expr[Map[String, Any]] =
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo - Parmas instead of Params

seems like the typo is copied from https://github.com/lampepfl/dotty-macro-examples/blob/main/defaultParamsInference/src/macro.scala

import quotes.reflect.*
val sym = TypeTree.of[T].symbol
val comp = sym.companionClass
val mod = Ref(sym.companionModule)
val names =
for p <- sym.caseFields if p.flags.is(Flags.HasDefault)
yield p.name
val namesExpr: Expr[List[String]] =
Expr.ofList(names.map(Expr(_)))

val body = comp.tree.asInstanceOf[ClassDef].body
val idents: List[Ref] =
for case deff @ DefDef(name, _, _, _) <- body
if name.startsWith(
"$lessinit$greater$default")
yield mod.select(deff.symbol)
val identsExpr: Expr[List[Any]] =
Expr.ofList(idents.map(_.asExpr))

'{ $namesExpr.zip($identsExpr).toMap }
17 changes: 17 additions & 0 deletions core/src/main/scala-3/toml/derivation/auto.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package toml
package derivation
import shapeless3.deriving.*
import toml.Codec.Defaults

object auto:
inline implicit def derivedProductCodec[P](using
inline codec: DerivedProductCodec[P],
): Codec[P] = codec

implicit def op[A](implicit c:Codec[A]): Codec[Option[A]] = new Codec {
def apply(value: Value, defaults: Defaults, index: Int): Either[Parse.Error, Option[A]] = c.apply(value,defaults,index).map(Some(_))
override def optional: Boolean = true
}



2 changes: 1 addition & 1 deletion core/src/main/scala/toml/Codec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ trait Codec[A] {
defaults: Codec.Defaults,
index: Int
): Either[Parse.Error, A]
private[toml] def optional: Boolean = false
Copy link
Author

@i10416 i10416 Dec 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field makes it easier to handle default value for missing field of type Option[?]. toml does not have the concept of null or nil, so we cannot simply convert null into null-value-like AST node.

}

object Codec {
type Defaults = Map[String, Any]
type Index = Int

def apply[T](
f: (Value, Defaults, Index) => Either[Parse.Error, T]
): Codec[T] = new Codec[T] {
Expand Down
Loading