Skip to content

Instantly share code, notes, and snippets.

@umazalakain
Created September 29, 2022 13:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save umazalakain/fd5b8f11c3f4c99d151e1c838142a4bb to your computer and use it in GitHub Desktop.
Save umazalakain/fd5b8f11c3f4c99d151e1c838142a4bb to your computer and use it in GitHub Desktop.
Staged validation
import cats.Monad
import cats.syntax.all.*
object Validation:
enum ValidationLevel:
case Mandatory
case Desirable
case Optional
export ValidationLevel.*
/* We define the type of a field as Validated[V, D, A] where
- A is what this field contains
- D is how optional this field is
- Mandatory: has to be there
- Desirable: should be there, otherwise we will alert
- Optional: could be there, do nothing if it's not
- V is the level we are validating for.
- Mandatory: fields can only be missing if they are marked as Mandatory, Desirable or Optional
- Desirable: fields can only be missing if they are marked as Desirable or Optional
- Optional: fields can only be missing if they are marked as Optional
Note that this could (should?) be abstracted over to any preorder.
*/
enum Validated[V, D, A]:
case Some[V, D, A](a : A) extends Validated[V, D, A]
case NoneMM[A]() extends Validated[Mandatory.type, Mandatory.type, A]
case NoneMD[A]() extends Validated[Mandatory.type, Desirable.type, A]
case NoneMO[A]() extends Validated[Mandatory.type, Optional.type, A]
case NoneDD[A]() extends Validated[Desirable.type, Desirable.type, A]
case NoneDO[A]() extends Validated[Desirable.type, Optional.type, A]
case NoneOO[A]() extends Validated[Optional.type, Optional.type, A]
object Validated:
def traverse[F[_]: Monad, V, D, A, B](x: Validated[V, D, A])(f: A => F[B]): F[Validated[V, D, B]] = x match
case Validated.Some(a) => f(a).map(Validated.Some.apply)
case Validated.NoneMM() => Validated.NoneMM().pure[F]
case Validated.NoneMD() => Validated.NoneMD().pure[F]
case Validated.NoneMO() => Validated.NoneMO().pure[F]
case Validated.NoneDD() => Validated.NoneDD().pure[F]
case Validated.NoneDO() => Validated.NoneDO().pure[F]
case Validated.NoneOO() => Validated.NoneOO().pure[F]
def mandatoryToDesirable[D, A]: Validated[Mandatory.type, D, A] => Option[Validated[Desirable.type, D, A]] =
case Validated.Some(a) => Some(Validated.Some(a))
case Validated.NoneMM() => None
case Validated.NoneMD() => Some(Validated.NoneDD())
case Validated.NoneMO() => Some(Validated.NoneDO())
def desirableToOptional[D, A]: Validated[Desirable.type, D, A] => Option[Validated[Optional.type, D, A]] =
case Validated.Some(a) => Some(Validated.Some(a))
case Validated.NoneDD() => None
case Validated.NoneDO() => Some(Validated.NoneOO())
final case class Asset[V1](name: Validated[V1, Optional.type, String]):
def validate[V2](f: [D, A] => Validated[V1, D, A] => Option[Validated[V2, D, A]]): Option[Asset[V2]] =
for name2 <- f(name)
yield Asset(name2)
final case class Article[V1](title: Validated[V1, Mandatory.type, String], asset: Validated[V1, Desirable.type, Asset[V1]]):
def validate[V2](f: [D, A] => Validated[V1, D, A] => Option[Validated[V2, D, A]]): Option[Article[V2]] =
for
title2 <- f(title)
asset2 <- Validated.traverse(asset)(_.validate(f))
asset3 <- f(asset2)
yield Article(title2, asset3)
val example: Unit =
val asset: Asset[Mandatory.type] = Asset(name = Validated.NoneMO())
val stage0: Article[Mandatory.type] = Article(title = Validated.Some("title"), asset = Validated.Some(asset))
val stage1: Option[Article[Desirable.type]] = stage0.validate([D, A] => (v: Validated[Mandatory.type, D, A]) => mandatoryToDesirable[D, A](v))
val stage2: Option[Article[Optional.type]] = stage1.flatMap(_.validate([D, A] => (v: Validated[Desirable.type, D, A]) => desirableToOptional[D, A](v)))
assert(stage2.isDefined)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment