Skip to content

Instantly share code, notes, and snippets.

@narma
Last active July 3, 2024 19:35
Show Gist options
  • Save narma/9873a0569ae1543c2b39c6f35e5346be to your computer and use it in GitHub Desktop.
Save narma/9873a0569ae1543c2b39c6f35e5346be to your computer and use it in GitHub Desktop.
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)
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())
//> using scala "3.5.0-RC2"
//> using dep "com.evolution::derivation-core:0.2.0"
//> using dep "org.typelevel::cats-core:2.12.0"
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