Last active
July 3, 2024 19:35
-
-
Save narma/9873a0569ae1543c2b39c6f35e5346be to your computer and use it in GitHub Desktop.
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.Mirror | |
import scala.quoted.* | |
import scala.compiletime.constValue | |
trait EnumWithValue: | |
def value: String | |
object EnumWithValue: | |
inline def enumMapValues[E <: EnumWithValue](using | |
m: Mirror.SumOf[E] | |
): Map[String, E] = enumValuesMacro[E].map { enumItem => | |
(enumItem.value, enumItem) | |
}.toMap | |
inline def enumValuesMacro[E]: Array[E] = ${ enumValuesImpl[E] } | |
def enumValuesImpl[E: Type](using Quotes): Expr[Array[E]] = | |
import quotes.reflect.* | |
val companion = Ref(TypeTree.of[E].symbol.companionModule) | |
Select.unique(companion, "values").asExprOf[Array[E]] | |
inline def getByValue[E <: EnumWithValue](value: String)(using | |
m: Mirror.SumOf[E] | |
): Option[E] = | |
enumMapValues[E].get(value) | |
inline def invalidValueError[E <: EnumWithValue]( | |
value: String | |
)(using m: Mirror.SumOf[E]): String = { | |
val enumName = constValue[m.MirroredLabel] | |
s"Invalid value for ${enumName}: ${value}, valid values are ${enumMapValues[E].values.map(_.value).mkString(", ")}" | |
} | |
inline def getByValueEither[E <: EnumWithValue](value: String)(using | |
m: Mirror.SumOf[E] | |
): Either[String, E] = | |
enumMapValues[E].get(value) match | |
case None => | |
Left(invalidValueError[E](value)) | |
case Some(value) => Right(value) |
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 evo.derivation.config.Config | |
enum Pet(val value: String) extends EnumWithValue: | |
case Cat extends Pet("cat") | |
case Dog extends Pet("dog") | |
case class Params( | |
pet: Option[Pet] = None | |
) derives Config, QueryParamsRead | |
object Main extends App: | |
println(Params()) |
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
//> using scala "3.5.0-RC2" | |
//> using dep "com.evolution::derivation-core:0.2.0" | |
//> using dep "org.typelevel::cats-core:2.12.0" |
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 evo.derivation.template.* | |
import evo.derivation.ValueClass | |
import evo.derivation.LazySummon.All | |
import evo.derivation.internal.Matching | |
import scala.deriving.Mirror.SumOf | |
import scala.deriving.Mirror.ProductOf | |
import evo.derivation.config.Config | |
import scala.deriving.Mirror | |
import evo.derivation.LazySummon | |
import evo.derivation.LazySummon.useEithers | |
import evo.derivation.config.ForField | |
import java.util.UUID | |
import cats.syntax.either.* | |
enum QueryError: | |
case NoValue | |
case NoKey | |
case DecodingError(error: String) | |
def repr(key: String): String = this match | |
case NoValue => s"no value for key $key" | |
case NoKey => s"parameter not found for key $key" | |
case DecodingError(error) => s"Error with key $key: $error" | |
trait QueryParamRead[T]: | |
self => | |
def decode(v: Option[List[String]] | String): Either[QueryError, T] | |
def map[R](f: T => R): QueryParamRead[R] = new QueryParamRead[R] { | |
def decode(v: Option[List[String]] | String): Either[QueryError, R] = self.decode(v).map(f) | |
} | |
object QueryParamRead: | |
def apply[T](using instance: QueryParamRead[T]): QueryParamRead[T] = instance | |
extension (v: Option[List[String]] | String) | |
private def first: Either[QueryError, String] = v match | |
case None => Left(QueryError.NoKey) | |
case Some(value) => value.headOption.toRight(QueryError.NoValue) | |
case x: String => Right(x) | |
given QueryParamRead[String] = v => v.first | |
inline given [A <: EnumWithValue](using m: Mirror.SumOf[A]): QueryParamRead[A] = | |
new QueryParamRead[A] { | |
override def decode(v: Option[List[String]] | String): Either[QueryError, A] = | |
v.first.flatMap(s => | |
EnumWithValue.getByValueEither[A](s).leftMap(err => QueryError.DecodingError(err))) | |
} | |
given [A](using read: QueryParamRead[A]): QueryParamRead[Option[A]] = | |
v => | |
v match | |
case None | "" => Right(None) | |
case _ => read.decode(v).map(Some(_)) | |
type QueryParameters = Map[String, List[String]] | |
trait QueryParamsRead[T]: | |
def extract(v: QueryParameters): Either[List[String], T] | |
extension (q: QueryParameters) | |
def decode[A](k: String)(using reads: QueryParamRead[A]): Either[QueryError, A] = | |
// filter out empty values | |
val v = q.get(k).map(_.filterNot(_.isEmpty())) | |
// filter out keys with no values at all | |
// feel free to move outside this logic (for example into ZIOController#decodeParams) | |
// if you want to operate with empty values | |
if v.exists(_.isEmpty) then reads.decode(None) | |
else reads.decode(v) | |
object QueryParamsRead | |
extends ConsistentTemplate[QueryParamRead, QueryParamsRead] | |
with SummonForProduct: | |
def extract[A](p: QueryParameters)(using | |
reads: QueryParamsRead[A] | |
): Either[List[String], A] = reads.extract(p) | |
override def sum[A](using mirror: SumOf[A])( | |
subs: All[QueryParamRead, mirror.MirroredElemTypes], | |
mkSubMap: => Map[String, QueryParamRead[A]] | |
)(using config: => Config[A], matching: Matching[A]): QueryParamsRead[A] = ??? | |
override def product[A](using mirror: ProductOf[A])( | |
fields: All[QueryParamRead, mirror.MirroredElemTypes] | |
)(using => Config[A], A <:< Product): QueryParamsRead[A] = | |
new ProductReadsMake(fields) | |
override def newtype[A](using | |
nt: ValueClass[A] | |
)(using reads: QueryParamRead[nt.Representation]): QueryParamsRead[A] = | |
v => | |
reads.decode(v.get(nt.accessorName)).map(nt.from).leftMap { err => | |
List(err.repr(nt.accessorName)) | |
} | |
class ProductReadsMake[A](using mirror: Mirror.ProductOf[A])( | |
fields: LazySummon.All[QueryParamRead, mirror.MirroredElemTypes] | |
)(using config: => Config[A]) | |
extends QueryParamsRead[A]: | |
lazy val infos = IArray(config.top.fields.map(_._2)*) | |
private def onField( | |
value: QueryParameters | |
)( | |
decoder: LazySummon.Of[QueryParamRead], | |
info: ForField[?] | |
): Either[(String, QueryError), decoder.FieldType] = | |
decoder.use(value.decode[decoder.FieldType](info.name)).leftMap(err => (info.name, err)) | |
end onField | |
override def extract(v: QueryParameters): Either[List[String], A] = | |
fields.useEithers(infos)(onField(v)) match | |
case Left(l @ head +: tail) => | |
Left(l.map { case (k, err) => err.repr(k) }.toList) | |
case Left(_) => Left(List("Unknown error")) | |
case Right(tuple) => Right(mirror.fromProduct(tuple)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment