-
Notifications
You must be signed in to change notification settings - Fork 1
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
base: master
Are you sure you want to change the base?
Changes from 7 commits
a5ef05f
d9b75c2
66c028c
9bb3f42
6bcb29e
242a7dd
35f4dac
d85fb3b
6c1138f
2e6abae
762b498
64febda
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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] | ||
|
||
|
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]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. defaultParams is |
||
) | ||
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]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. optional returns |
||
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 | ||
) |
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] | ||
} |
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]] = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 } |
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 | ||
} | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,12 +9,12 @@ trait Codec[A] { | |
defaults: Codec.Defaults, | ||
index: Int | ||
): Either[Parse.Error, A] | ||
private[toml] def optional: Boolean = false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
|
||
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] { | ||
|
There was a problem hiding this comment.
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]
?