Skip to content

Commit

Permalink
Merge pull request #21 from mblink/formless-hlist
Browse files Browse the repository at this point in the history
Use formless with `HList`s
  • Loading branch information
mrdziuban authored Mar 27, 2024
2 parents c152fad + 1cea4bd commit e7ab73d
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 67 deletions.
52 changes: 22 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ and an optional session id.
First some imports.

```scala
import formless.tuple._
import formless.hlist._
import formless.record._
import typify.{Cursor, CursorHistory, ParseError, Typify}
```
Expand All @@ -39,7 +39,7 @@ import typify.parsedany._
case class Fail(reason: String, history: CursorHistory[_])

val tp = new Typify[Fail, Any]
// tp: Typify[Fail, Any] = typify.Typify@18b3ee39
// tp: Typify[Fail, Any] = typify.Typify@2bd9cc3e
```

We also need to define an implicit function to convert a typify.ParseError to our failure type.
Expand All @@ -52,7 +52,7 @@ case class ParseError(key: String, error: String)

```scala
implicit val parse2Error: ParseError[Any] => Fail = pe => Fail(pe.message, pe.cursor.history)
// parse2Error: Function1[ParseError[Any], Fail] = repl.MdocSession$MdocApp$$Lambda$13197/0x0000008003065000@2bbb6404
// parse2Error: Function1[ParseError[Any], Fail] = repl.MdocSession$MdocApp$$Lambda/0x000000b8024b94f8@a5b553a
```

Now we can define some validation functions.
Expand All @@ -64,29 +64,25 @@ import cats.syntax.validated._

val checkEmail = Typify.validate((_: String, s: String, c: Cursor[Any]) => s.validNel[Fail]
.ensure(NonEmptyList.of(Fail("Email is invalid", c.history)))(_.contains("@")))
// checkEmail: Function1[String, PV[Any, Fail, String]] = typify.Typify$$$Lambda$13199/0x0000008003069440@23bb1a57
// checkEmail: Function1[String, PV[Any, Fail, String]] = typify.Typify$$$Lambda/0x000000b8024c21b0@5924ffa8

val checkAge = Typify.validate((_: String, i: Int, c: Cursor[Any]) => i.validNel[Fail]
.ensure(NonEmptyList.of(Fail("Too young", c.history)))(_ > 21))
// checkAge: Function1[String, PV[Any, Fail, Int]] = typify.Typify$$$Lambda$13199/0x0000008003069440@43e05451
// checkAge: Function1[String, PV[Any, Fail, Int]] = typify.Typify$$$Lambda/0x000000b8024c21b0@1ee9de3b

val checkSessIdF = ((_: String, i: Int, c: Cursor[Any]) => i.validNel[Fail]
.ensure(NonEmptyList.of(Fail("Invalid session id", c.history)))(_ > 3000))
// checkSessIdF: Function3[String, Int, Cursor[Any], Validated[NonEmptyList[Fail], Int]] = repl.MdocSession$MdocApp$$Lambda$13201/0x000000800306c690@595d283
// checkSessIdF: Function3[String, Int, Cursor[Any], Validated[NonEmptyList[Fail], Int]] = repl.MdocSession$MdocApp$$Lambda/0x000000b8024c4460@3d77bc3f

val checkSessId = Typify.optional(checkSessIdF)
// checkSessId: Function1[String, PV[Any, Fail, Option[Int]]] = typify.Typify$$$Lambda$13202/0x0000008003069a40@757e0d71
// checkSessId: Function1[String, PV[Any, Fail, Option[Int]]] = typify.Typify$$$Lambda/0x000000b8024c27a0@6d55cc2e
```

Now we can define in which fields to look for these values under our source value as follows.

```scala
val checkPerson = ("email" ->> checkEmail) *: ("age" ->> checkAge) *: ("session" ->> checkSessId) *: EmptyTuple
// checkPerson: *:[->>["email", KPV[Any, Fail, String]], *:[->>["age", KPV[Any, Fail, Int]], *:[->>["session", KPV[Any, Fail, Option[Int]]], EmptyTuple]]] = (
// typify.Typify$$$Lambda$13199/0x0000008003069440@23bb1a57,
// typify.Typify$$$Lambda$13199/0x0000008003069440@43e05451,
// typify.Typify$$$Lambda$13202/0x0000008003069a40@757e0d71
// )
val checkPerson = ("email" ->> checkEmail) :: ("age" ->> checkAge) :: ("session" ->> checkSessId) :: HNil
// checkPerson: ::[->>["email", KPV[Any, Fail, String]], ::[->>["age", KPV[Any, Fail, Int]], ::[->>["session", KPV[Any, Fail, Option[Int]]], HNil]]] = typify.Typify$$$Lambda/0x000000b8024c21b0@5924ffa8 :: typify.Typify$$$Lambda/0x000000b8024c21b0@1ee9de3b :: typify.Typify$$$Lambda/0x000000b8024c27a0@6d55cc2e :: HNil
```

From here we are able to parse a person out of Any using our Typify instance.
Expand All @@ -113,15 +109,15 @@ val failsAtValidation: Any = Map("email" -> "foo", "session" -> 77777)
// failsAtValidation: Any = Map("email" -> "foo", "session" -> 77777)

val passed = Cursor.top(passes).parse(checkPerson)
// passed: Validated[NonEmptyList[Fail], *:[->>["email", String], *:[->>["age", Int], *:[->>["session", Option[Int]], EmptyTuple]]]] = Valid(
// a = ("foo@bar", 22, None)
// passed: Validated[NonEmptyList[Fail], ::[->>["email", String], ::[->>["age", Int], ::[->>["session", Option[Int]], HNil]]]] = Valid(
// a = "foo@bar" :: 22 :: None :: HNil
// )
val passedNoSess = Cursor.top(passesNoSess).parse(checkPerson)
// passedNoSess: Validated[NonEmptyList[Fail], *:[->>["email", String], *:[->>["age", Int], *:[->>["session", Option[Int]], EmptyTuple]]]] = Valid(
// a = ("foo@bar", 22, None)
// passedNoSess: Validated[NonEmptyList[Fail], ::[->>["email", String], ::[->>["age", Int], ::[->>["session", Option[Int]], HNil]]]] = Valid(
// a = "foo@bar" :: 22 :: None :: HNil
// )
val failedAtParse = Cursor.top(failsAtParse).parse(checkPerson)
// failedAtParse: Validated[NonEmptyList[Fail], *:[->>["email", String], *:[->>["age", Int], *:[->>["session", Option[Int]], EmptyTuple]]]] = Invalid(
// failedAtParse: Validated[NonEmptyList[Fail], ::[->>["email", String], ::[->>["age", Int], ::[->>["session", Option[Int]], HNil]]]] = Invalid(
// e = NonEmptyList(
// head = Fail(
// reason = "Could not be interpreted as java.lang.String",
Expand All @@ -136,7 +132,7 @@ val failedAtParse = Cursor.top(failsAtParse).parse(checkPerson)
// )
// )
val failedAtValidation = Cursor.top(failsAtValidation).parse(checkPerson)
// failedAtValidation: Validated[NonEmptyList[Fail], *:[->>["email", String], *:[->>["age", Int], *:[->>["session", Option[Int]], EmptyTuple]]]] = Invalid(
// failedAtValidation: Validated[NonEmptyList[Fail], ::[->>["email", String], ::[->>["age", Int], ::[->>["session", Option[Int]], HNil]]]] = Invalid(
// e = NonEmptyList(
// head = Fail(
// reason = "Email is invalid",
Expand All @@ -157,20 +153,16 @@ operations to compose rules, and do partial validation.

```scala
val checkRequiredSess = Typify.validate(checkSessIdF)
// checkRequiredSess: Function1[String, PV[Any, Fail, Int]] = typify.Typify$$$Lambda$13199/0x0000008003069440@2ae0d8a2
// checkRequiredSess: Function1[String, PV[Any, Fail, Int]] = typify.Typify$$$Lambda/0x000000b8024c21b0@50d15db7
val checkPersonWithSession = checkPerson.updateWith("session")(_ => checkRequiredSess)
// checkPersonWithSession: *:[->>["email", KPV[Any, Fail, String]], *:[->>["age", KPV[Any, Fail, Int]], *:[->>["session", Function1[String, PV[Any, Fail, Int]]], EmptyTuple]]] = (
// typify.Typify$$$Lambda$13199/0x0000008003069440@23bb1a57,
// typify.Typify$$$Lambda$13199/0x0000008003069440@43e05451,
// typify.Typify$$$Lambda$13199/0x0000008003069440@2ae0d8a2
// )
// checkPersonWithSession: ::[->>["email", KPV[Any, Fail, String]], ::[->>["age", KPV[Any, Fail, Int]], ::[->>["session", Function1[String, PV[Any, Fail, Int]]], HNil]]] = typify.Typify$$$Lambda/0x000000b8024c21b0@5924ffa8 :: typify.Typify$$$Lambda/0x000000b8024c21b0@1ee9de3b :: typify.Typify$$$Lambda/0x000000b8024c21b0@50d15db7 :: HNil

val passedWithSession = Cursor.top(passes).parse(checkPersonWithSession)
// passedWithSession: Validated[NonEmptyList[Fail], *:[->>["email", String], *:[->>["age", Int], *:[->>["session", Int], EmptyTuple]]]] = Valid(
// a = ("foo@bar", 22, 77777)
// passedWithSession: Validated[NonEmptyList[Fail], ::[->>["email", String], ::[->>["age", Int], ::[->>["session", Int], HNil]]]] = Valid(
// a = "foo@bar" :: 22 :: 77777 :: HNil
// )
val failedNoSession = Cursor.top(passesNoSess).parse(checkPersonWithSession)
// failedNoSession: Validated[NonEmptyList[Fail], *:[->>["email", String], *:[->>["age", Int], *:[->>["session", Int], EmptyTuple]]]] = Invalid(
// failedNoSession: Validated[NonEmptyList[Fail], ::[->>["email", String], ::[->>["age", Int], ::[->>["session", Int], HNil]]]] = Invalid(
// e = NonEmptyList(
// head = Fail(
// reason = "Could not be interpreted as Int",
Expand All @@ -180,7 +172,7 @@ val failedNoSession = Cursor.top(passesNoSess).parse(checkPersonWithSession)
// )
// )
val passedPartialSession = Cursor.top(passesNoSess).parse(checkPersonWithSession - "session")
// passedPartialSession: Validated[NonEmptyList[Fail], *:[->>["email", String], *:[->>["age", Int], EmptyTuple]]] = Valid(
// a = ("foo@bar", 22)
// passedPartialSession: Validated[NonEmptyList[Fail], ::[->>["email", String], ::[->>["age", Int], HNil]]] = Valid(
// a = "foo@bar" :: 22 :: HNil
// )
```
6 changes: 3 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ lazy val baseSettings = Seq(
scalaVersion := scala3,
crossScalaVersions := Seq(scala213, scala3),
organization := "typify",
version := "10.0.1",
version := "11.0.0-RC1",
resolvers += "bondlink-maven-repo" at "https://raw.githubusercontent.com/mblink/maven-repo/main",
mimaPreviousArtifacts := Set("typify" %%% name.value % "10.0.0"),
mimaPreviousArtifacts := Set(),
libraryDependencies ++= foldScalaV(scalaVersion.value)(
Seq(compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.3" cross CrossVersion.patch)),
Seq(),
Expand Down Expand Up @@ -73,7 +73,7 @@ lazy val root = project.in(file("."))

lazy val cats = Def.setting("org.typelevel" %%% "cats-core" % "2.10.0")
lazy val circe = "io.circe" %% "circe-core" % "0.14.6"
lazy val formless = Def.setting("com.bondlink" %%% "formless" % "0.2.0")
lazy val formless = Def.setting("com.bondlink" %%% "formless" % "0.3.0")
lazy val json4s = "org.json4s" %% "json4s-jackson" % "4.0.7"
lazy val playJson = "org.playframework" %% "play-json" % "3.0.2"
lazy val shapeless = Def.setting("com.chuusai" %%% "shapeless" % "2.3.10")
Expand Down
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ and an optional session id.
First some imports.

```scala mdoc:silent
import formless.tuple._
import formless.hlist._
import formless.record._
import typify.{Cursor, CursorHistory, ParseError, Typify}
```
Expand Down Expand Up @@ -75,7 +75,7 @@ val checkSessId = Typify.optional(checkSessIdF)
Now we can define in which fields to look for these values under our source value as follows.

```scala mdoc
val checkPerson = ("email" ->> checkEmail) *: ("age" ->> checkAge) *: ("session" ->> checkSessId) *: EmptyTuple
val checkPerson = ("email" ->> checkEmail) :: ("age" ->> checkAge) :: ("session" ->> checkSessId) :: HNil
```

From here we are able to parse a person out of Any using our Typify instance.
Expand Down
6 changes: 3 additions & 3 deletions typify/shared/src/main/scala/typify/Optimize.scala
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package typify

import formless.tuple.Tuple
import formless.hlist.HList

trait TWitness[T] { final type Out = T }

class Optimize[L, P](val tp: Typify[L, P]) {
def folder[G <: Tuple, R <: Tuple](@deprecated("unused", "") in: G)(
def folder[G <: HList, R <: HList](@deprecated("unused", "") in: G)(
implicit rf: PVFolder[P, L, G, R]
): PVFolder[P, L, G, R] = rf

def success[G <: Tuple, R <: Tuple](@deprecated("unused", "") in: G)(
def success[G <: HList, R <: HList](@deprecated("unused", "") in: G)(
implicit @deprecated("unused", "") rf: PVFolder[P, L, G, R]
): TWitness[R] = new TWitness[R] {}
}
34 changes: 17 additions & 17 deletions typify/shared/src/main/scala/typify/PVFolder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package typify

import cats.syntax.apply._
import cats.syntax.validated._
import formless.hlist._
import formless.record._
import formless.tuple._

sealed trait PVFolder[P, L, I <: Tuple, O <: Tuple]
extends (I => PV[P, L, EmptyTuple] => PV[P, L, O])
sealed trait PVFolder[P, L, I <: HList, O <: HList]
extends (I => PV[P, L, HNil] => PV[P, L, O])

object PVFolder {
sealed trait Case[I, O] extends (I => O)
Expand All @@ -16,16 +16,16 @@ object PVFolder {
def apply(i: I): O = f(i)
}

implicit def PV[P, L, O <: Tuple, A]: Case[PV[P, L, A], PV[P, L, A]] =
implicit def PV[P, L, O <: HList, A]: Case[PV[P, L, A], PV[P, L, A]] =
inst(identity)

implicit def labelledPV[P, L, O <: Tuple, K, A]: Case[K ->> PV[P, L, A], PV[P, L, K ->> A]] =
implicit def labelledPV[P, L, O <: HList, K, A]: Case[K ->> PV[P, L, A], PV[P, L, K ->> A]] =
inst(_.andThen(_.map(label[K][A](_))))

implicit def labelledKPVStr[P, L, O <: Tuple, K <: String, A](implicit k: ValueOf[K]): Case[K ->> KPV[P, L, A], PV[P, L, K ->> A]] =
implicit def labelledKPVStr[P, L, O <: HList, K <: String, A](implicit k: ValueOf[K]): Case[K ->> KPV[P, L, A], PV[P, L, K ->> A]] =
inst(a => a(k.value).andThen(_.map(label[K][A](_))))

implicit def listOfPVStr[P, L, O <: Tuple, K <: String, A](
implicit def listOfPVStr[P, L, O <: HList, K <: String, A](
implicit k: ValueOf[K],
e2l: E2L[L, P]
): Case[K ->> listOf[PV[P, L, A]], PV[P, L, K ->> List[A]]] =
Expand All @@ -34,33 +34,33 @@ object PVFolder {
in.run
).map(label[K][List[A]](_)))

implicit def listOfTuple[P, L, O <: Tuple, K, I <: Tuple, IR <: Tuple](
implicit def listOfHList[P, L, O <: HList, K, I <: HList, IR <: HList](
implicit rf: PVFolder[P, L, I, IR],
lpv: Case[K ->> listOf[PV[P, L, IR]], PV[P, L, K ->> List[IR]]]
): Case[K ->> listOf[I], PV[P, L, K ->> List[IR]]] =
inst { in =>
val x = label[K](listOf(rf(in.run)(pvEmptyTuple)))
val x = label[K](listOf(rf(in.run)(pvHNil)))
lpv(x)
}

implicit def nestedStr[P, L, O <: Tuple, K <: String, I <: Tuple, IR <: Tuple](
implicit def nestedStr[P, L, O <: HList, K <: String, I <: HList, IR <: HList](
implicit rf: PVFolder[P, L, I, IR],
k: ValueOf[K]
): Case[K ->> I, PV[P, L, K ->> IR]] =
inst(in => (c: Cursor[P]) => rf(in)(pvEmptyTuple)(c.downField(k.value)).map(label[K][IR](_)))
inst(in => (c: Cursor[P]) => rf(in)(pvHNil)(c.downField(k.value)).map(label[K][IR](_)))
}

private def inst[P, L, I <: Tuple, O <: Tuple](f: I => PV[P, L, EmptyTuple] => PV[P, L, O]): PVFolder[P, L, I, O] =
private def inst[P, L, I <: HList, O <: HList](f: I => PV[P, L, HNil] => PV[P, L, O]): PVFolder[P, L, I, O] =
new PVFolder[P, L, I, O] {
def apply(i: I): PV[P, L, EmptyTuple] => PV[P, L, O] = f(i)
def apply(i: I): PV[P, L, HNil] => PV[P, L, O] = f(i)
}

implicit def emptyTuple[P, L]: PVFolder[P, L, EmptyTuple, EmptyTuple] =
implicit def hnil[P, L]: PVFolder[P, L, HNil, HNil] =
inst(_ => identity)

implicit def tupleN[P, L, HI, HO, TI <: Tuple, TO <: Tuple](
implicit def hcons[P, L, HI, HO, TI <: HList, TO <: HList](
implicit ch: Case[HI, PV[P, L, HO]],
ft: PVFolder[P, L, TI, TO]
): PVFolder[P, L, HI *: TI, HO *: TO] =
inst(t => pve => (c: Cursor[P]) => (ch(t.head)(c), ft(t.tail)(pve)(c)).mapN(_ *: _))
): PVFolder[P, L, HI :: TI, HO :: TO] =
inst(t => pve => (c: Cursor[P]) => (ch(t.head)(c), ft(t.tail)(pve)(c)).mapN(_ :: _))
}
20 changes: 10 additions & 10 deletions typify/shared/src/main/scala/typify/Typify.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cats.data.ValidatedNel
import cats.instances.option._
import cats.syntax.traverse._
import cats.syntax.validated._
import formless.tuple._
import formless.hlist._
import scala.language.implicitConversions
import scala.reflect.ClassTag

Expand Down Expand Up @@ -53,10 +53,10 @@ object Typify {
def get[A](k: String)(implicit cp: CanParse[A, P], e2l: E2L[L, P]): ValidatedNel[L, A] =
cp.parse(k, c).leftMap(_.map(e2l))

def parse[I <: Tuple, R <: Tuple](in: I)(implicit rf: PVFolder[P, L, I, R]): ValidatedNel[L, R] =
rf(in)(pvEmptyTuple)(c)
def parse[I <: HList, R <: HList](in: I)(implicit rf: PVFolder[P, L, I, R]): ValidatedNel[L, R] =
rf(in)(pvHNil)(c)

def parseOption[I <: Tuple, R <: Tuple](in: I)(
def parseOption[I <: HList, R <: HList](in: I)(
implicit rf: PVFolder[P, L, I, R],
cpop: CanParse[Option[P], P],
e2l: E2L[L, P]
Expand All @@ -67,17 +67,17 @@ object Typify {
new CursorOps[L, P](c.replace(x, Some(c), CursorOp.WithFocus((_: P) => x))).parse(in)))
}

def parseList[I <: Tuple, R <: Tuple](in: I)(implicit rf: PVFolder[P, L, I, R], e2l: E2L[L, P]): ValidatedNel[L, List[R]] =
def parseList[I <: HList, R <: HList](in: I)(implicit rf: PVFolder[P, L, I, R], e2l: E2L[L, P]): ValidatedNel[L, List[R]] =
typify.parseList(c)(
f => e2l(ParseError(f, s"Could not be interpreted as List")).invalidNel[List[R]],
rf(in)(pvEmptyTuple))
rf(in)(pvHNil))
}
}

class Typify[L, P] { tp =>
final type PV[A] = typify.PV[P, L, A]
final type KPV[A] = typify.KPV[P, L, A]
final type PVFolder[I <: Tuple, O <: Tuple] = typify.PVFolder[P, L, I, O]
final type PVFolder[I <: HList, O <: HList] = typify.PVFolder[P, L, I, O]

object syntax {
implicit def cursorToCursorOps(c: Cursor[P]): Typify.CursorOps[L, P] = new Typify.CursorOps[L, P](c)
Expand All @@ -91,16 +91,16 @@ class Typify[L, P] { tp =>
final def get[A](c: Cursor[P])(k: String)(implicit cp: CanParse[A, P], e2l: E2L[L, P]): ValidatedNel[L, A] =
c.get[A](k)

final def parse[I <: Tuple, R <: Tuple](c: Cursor[P])(in: I)(implicit rf: PVFolder[I, R]): ValidatedNel[L, R] =
final def parse[I <: HList, R <: HList](c: Cursor[P])(in: I)(implicit rf: PVFolder[I, R]): ValidatedNel[L, R] =
c.parse(in)

final def parseOption[I <: Tuple, R <: Tuple](c: Cursor[P])(in: I)(
final def parseOption[I <: HList, R <: HList](c: Cursor[P])(in: I)(
implicit rf: PVFolder[I, R],
cpop: CanParse[Option[P], P],
e2l: E2L[L, P]
): ValidatedNel[L, Option[R]] =
c.parseOption(in)

final def parseList[I <: Tuple, A, R <: Tuple](c: Cursor[P])(in: I)(implicit rf: PVFolder[I, R], e2l: E2L[L, P]): ValidatedNel[L, List[R]] =
final def parseList[I <: HList, A, R <: HList](c: Cursor[P])(in: I)(implicit rf: PVFolder[I, R], e2l: E2L[L, P]): ValidatedNel[L, List[R]] =
c.parseList(in)
}
4 changes: 2 additions & 2 deletions typify/shared/src/main/scala/typify/package.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import cats.data.ValidatedNel
import cats.syntax.apply._
import cats.syntax.validated._
import formless.tuple._
import formless.hlist._
import scala.annotation.tailrec
import scala.language.implicitConversions

Expand Down Expand Up @@ -37,5 +37,5 @@ package object typify {
}
}

def pvEmptyTuple[P, L]: PV[P, L, EmptyTuple] = (_: Cursor[P]) => EmptyTuple.validNel[L]
def pvHNil[P, L]: PV[P, L, HNil] = (_: Cursor[P]) => HNil.validNel[L]
}

0 comments on commit e7ab73d

Please sign in to comment.