-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #220 from theiterators/http4s
http4s DSL support
- Loading branch information
Showing
9 changed files
with
406 additions
and
2 deletions.
There are no files selected for viewing
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package pl.iterators.kebs | ||
|
||
import enumeratum.EnumEntry | ||
|
||
import scala.util.Try | ||
import pl.iterators.kebs.macros.CaseClass1Rep | ||
import pl.iterators.kebs.macros.enums.EnumOf | ||
import org.http4s._ | ||
import pl.iterators.kebs.instances.InstanceConverter | ||
|
||
import java.util.UUID | ||
|
||
trait Http4s { | ||
protected class PathVar[A](cast: String => Try[A]) { | ||
def unapply(str: String): Option[A] = | ||
if (str.nonEmpty) | ||
cast(str).toOption | ||
else | ||
None | ||
} | ||
|
||
object WrappedString { | ||
def apply[T](implicit rep: CaseClass1Rep[T, String]) = new PathVar[T](str => Try(rep.apply(str))) | ||
} | ||
|
||
object InstanceString { | ||
def apply[T](implicit rep: InstanceConverter[T, String]) = new PathVar[T](str => Try(rep.decode(str))) | ||
} | ||
|
||
object EnumString { | ||
def apply[T <: EnumEntry](implicit e: EnumOf[T]) = new PathVar[T](str => Try(e.`enum`.values.find(_.toString.toUpperCase == str.toUpperCase).getOrElse(throw new IllegalArgumentException(s"enum case not found: $str")))) | ||
} | ||
|
||
object WrappedInt { | ||
def apply[T](implicit rep: CaseClass1Rep[T, Int]) = new PathVar[T](str => Try(rep.apply(str.toInt))) | ||
} | ||
|
||
object InstanceInt { | ||
def apply[T](implicit rep: InstanceConverter[T, Int]) = new PathVar[T](str => Try(rep.decode(str.toInt))) | ||
} | ||
|
||
object WrappedLong { | ||
def apply[T](implicit rep: CaseClass1Rep[T, Long]) = new PathVar[T](str => Try(rep.apply(str.toLong))) | ||
} | ||
|
||
object InstanceLong { | ||
def apply[T](implicit rep: InstanceConverter[T, Long]) = new PathVar[T](str => Try(rep.decode(str.toLong))) | ||
} | ||
|
||
object WrappedUUID { | ||
def apply[T](implicit rep: CaseClass1Rep[T, UUID]) = new PathVar[T](str => Try(rep.apply(UUID.fromString(str)))) | ||
} | ||
|
||
object InstanceUUID { | ||
def apply[T](implicit rep: InstanceConverter[T, UUID]) = new PathVar[T](str => Try(rep.decode(UUID.fromString(str)))) | ||
} | ||
|
||
implicit def cc1RepQueryParamDecoder[T, U](implicit rep: CaseClass1Rep[T, U], qpd: QueryParamDecoder[U]): QueryParamDecoder[T] = qpd.emap(u => Try(rep.apply(u)).toEither.left.map(t => ParseFailure(t.getMessage, t.getMessage))) | ||
implicit def instanceConverterQueryParamDecoder[T, U](implicit rep: InstanceConverter[T, U], qpd: QueryParamDecoder[U]): QueryParamDecoder[T] = qpd.emap(u => Try(rep.decode(u)).toEither.left.map(t => ParseFailure(t.getMessage, t.getMessage))) | ||
implicit def enumQueryParamDecoder[E <: EnumEntry](implicit e: EnumOf[E]): QueryParamDecoder[E] = QueryParamDecoder[String].emap(str => Try(e.`enum`.values.find(_.toString.toUpperCase == str.toUpperCase).getOrElse(throw new IllegalArgumentException(s"enum case not found: $str"))).toEither.left.map(t => ParseFailure(t.getMessage, t.getMessage))) | ||
} |
3 changes: 3 additions & 0 deletions
3
http4s/src/main/scala-2/pl/iterators/kebs/http4s/package.scala
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package pl.iterators.kebs | ||
|
||
package object http4s extends Http4s |
57 changes: 57 additions & 0 deletions
57
http4s/src/main/scala-3/pl/iterators/kebs/http4s/package.scala
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package pl.iterators.kebs.http4s | ||
|
||
import scala.util.Try | ||
import scala.reflect.Enum | ||
import pl.iterators.kebs.macros.CaseClass1Rep | ||
import pl.iterators.kebs.macros.enums.EnumOf | ||
import org.http4s._ | ||
import pl.iterators.kebs.instances.InstanceConverter | ||
import java.util.UUID | ||
|
||
protected class PathVar[A](cast: String => Try[A]) { | ||
def unapply(str: String): Option[A] = | ||
if (str.nonEmpty) | ||
cast(str).toOption | ||
else | ||
None | ||
} | ||
|
||
object WrappedString { | ||
def apply[T](using rep: CaseClass1Rep[T, String]) = new PathVar[T](str => Try(rep.apply(str))) | ||
} | ||
|
||
object InstanceString { | ||
def apply[T](using rep: InstanceConverter[T, String]) = new PathVar[T](str => Try(rep.decode(str))) | ||
} | ||
|
||
object EnumString { | ||
def apply[T <: Enum](using e: EnumOf[T]) = new PathVar[T](str => Try(e.`enum`.values.find(_.toString.toUpperCase == str.toUpperCase).getOrElse(throw new IllegalArgumentException(s"enum case not found: $str")))) | ||
} | ||
|
||
object WrappedInt { | ||
def apply[T](using rep: CaseClass1Rep[T, Int]) = new PathVar[T](str => Try(rep.apply(str.toInt))) | ||
} | ||
|
||
object InstanceInt { | ||
def apply[T](using rep: InstanceConverter[T, Int]) = new PathVar[T](str => Try(rep.decode(str.toInt))) | ||
} | ||
|
||
object WrappedLong { | ||
def apply[T](using rep: CaseClass1Rep[T, Long]) = new PathVar[T](str => Try(rep.apply(str.toLong))) | ||
} | ||
|
||
object InstanceLong { | ||
def apply[T](using rep: InstanceConverter[T, Long]) = new PathVar[T](str => Try(rep.decode(str.toLong))) | ||
} | ||
|
||
object WrappedUUID { | ||
def apply[T](using rep: CaseClass1Rep[T, UUID]) = new PathVar[T](str => Try(rep.apply(UUID.fromString(str)))) | ||
} | ||
|
||
object InstanceUUID { | ||
def apply[T](using rep: InstanceConverter[T, UUID]) = new PathVar[T](str => Try(rep.decode(UUID.fromString(str)))) | ||
} | ||
|
||
given[T, U](using rep: CaseClass1Rep[T, U], qpd: QueryParamDecoder[U]): QueryParamDecoder[T] = qpd.emap(u => Try(rep.apply(u)).toEither.left.map(t => ParseFailure(t.getMessage, t.getMessage))) | ||
given[T, U](using rep: InstanceConverter[T, U], qpd: QueryParamDecoder[U]): QueryParamDecoder[T] = qpd.emap(u => Try(rep.decode(u)).toEither.left.map(t => ParseFailure(t.getMessage, t.getMessage))) | ||
given[E <: Enum](using e: EnumOf[E]): QueryParamDecoder[E] = QueryParamDecoder[String].emap(str => Try(e.`enum`.values.find(_.toString.toUpperCase == str.toUpperCase).getOrElse(throw new IllegalArgumentException(s"enum case not found: $str"))).toEither.left.map(t => ParseFailure(t.getMessage, t.getMessage))) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package pl.iterators.kebs | ||
|
||
import enumeratum.{Enum, EnumEntry} | ||
import pl.iterators.kebs.tag.meta.tagged | ||
import pl.iterators.kebs.tagged._ | ||
|
||
import java.util.UUID | ||
|
||
@tagged trait Domain { | ||
trait AgeTag | ||
type Age = Int @@ AgeTag | ||
object Age { | ||
def validate(value: Int): Either[String, Int] = | ||
if (value < 0) Left("No going back, sorry") else Right(value) | ||
} | ||
} | ||
|
||
object Domain extends Domain { | ||
case class UserId(id: UUID) | ||
|
||
sealed trait Color extends EnumEntry | ||
object Color extends Enum[Color] { | ||
case object Red extends Color | ||
case object Blue extends Color | ||
case object Green extends Color | ||
|
||
override def values = findValues | ||
} | ||
} |
83 changes: 83 additions & 0 deletions
83
http4s/src/test/scala-2/pl/iterators/kebs/Http4sDslTests.scala
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package pl.iterators.kebs | ||
|
||
import cats.effect.IO | ||
import cats.effect.unsafe.IORuntime | ||
import org.http4s._ | ||
import org.http4s.dsl.io._ | ||
import org.http4s.implicits._ | ||
import org.scalatest.funsuite.AnyFunSuite | ||
import org.scalatest.matchers.should.Matchers | ||
|
||
import java.time.Year | ||
import java.util.Currency | ||
import pl.iterators.kebs.instances.KebsInstances._ | ||
import pl.iterators.kebs.http4s._ | ||
|
||
class Http4sDslTests extends AnyFunSuite with Matchers { | ||
import Domain._ | ||
|
||
implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global | ||
|
||
object AgeQueryParamDecoderMatcher extends QueryParamDecoderMatcher[Age]("age") | ||
|
||
object OptionalYearParamDecoderMatcher extends OptionalQueryParamDecoderMatcher[Year]("year") | ||
|
||
object ValidatingColorQueryParamDecoderMatcher extends ValidatingQueryParamDecoderMatcher[Color]("color") | ||
|
||
// this indirection comes from: https://github.com/scala/bug/issues/884 | ||
val AgeVar = WrappedInt[Age] | ||
val CurrencyVar = InstanceString[Currency] | ||
val UserIdVar = WrappedUUID[UserId] | ||
val ColorVar = EnumString[Color] | ||
|
||
val routes = HttpRoutes.of[IO] { | ||
case GET -> Root / "WrappedInt" / AgeVar(age) => Ok(age.toString) | ||
case GET -> Root / "InstanceString" / CurrencyVar(currency) => Ok(currency.getClass.toString) | ||
case GET -> Root / "EnumString" / ColorVar(color) => Ok(color.toString) | ||
case GET -> Root / "WrappedUUID" / UserIdVar(userId) => Ok(userId.toString) | ||
case GET -> Root / "WrappedIntParam" :? AgeQueryParamDecoderMatcher(age) => Ok(age.toString) | ||
case GET -> Root / "InstanceIntParam" :? OptionalYearParamDecoderMatcher(year) => Ok(year.toString) | ||
case GET -> Root / "EnumStringParam" :? ValidatingColorQueryParamDecoderMatcher(color) => Ok(color.toString) | ||
} | ||
|
||
private def runPathGetBody(path: Uri): String = { | ||
routes.orNotFound.run(Request(method = Method.GET, uri = path)).unsafeRunSync().body.compile.fold[String]("")(_ + _.toChar).unsafeRunSync() | ||
} | ||
|
||
test("WrappedInt + Opaque") { | ||
runPathGetBody(uri"/WrappedInt/42") shouldBe "42" | ||
runPathGetBody(uri"/WrappedInt/-42") shouldBe "Not found" | ||
} | ||
|
||
test("InstanceString + Currency") { | ||
runPathGetBody(uri"/InstanceString/USD") shouldBe "class java.util.Currency" | ||
runPathGetBody(uri"/InstanceString/NOPE") shouldBe "Not found" | ||
} | ||
|
||
test("WrappedUUID") { | ||
runPathGetBody(uri"/WrappedUUID/8cc82b40-71bc-4e50-ac7e-8227013f37ea") shouldBe "UserId(8cc82b40-71bc-4e50-ac7e-8227013f37ea)" | ||
runPathGetBody(uri"/WrappedUUID/NOPE") shouldBe "Not found" | ||
} | ||
|
||
test("EnumString") { | ||
runPathGetBody(uri"/EnumString/Red") shouldBe "Red" | ||
runPathGetBody(uri"/EnumString/BlUe") shouldBe "Blue" | ||
runPathGetBody(uri"/EnumString/GrEEn") shouldBe "Green" | ||
runPathGetBody(uri"/EnumString/Yellow") shouldBe "Not found" | ||
} | ||
|
||
test("WrappedIntParam + Opaque") { | ||
runPathGetBody(uri"/WrappedIntParam?age=42") shouldBe "42" | ||
runPathGetBody(uri"/WrappedIntParam?age=-42") shouldBe "Not found" | ||
} | ||
|
||
test("InstanceIntParam + Year") { | ||
runPathGetBody(uri"/InstanceIntParam?year=2022") shouldBe "Some(2022)" | ||
runPathGetBody(uri"/InstanceIntParam?year=-2147483647") shouldBe "Not found" | ||
} | ||
|
||
test("EnumStringParam") { | ||
runPathGetBody(uri"/EnumStringParam?color=RED") shouldBe "Valid(Red)" | ||
runPathGetBody(uri"/EnumStringParam?color=YELLow") shouldBe "Invalid(NonEmptyList(org.http4s.ParseFailure: enum case not found: YELLow: enum case not found: YELLow))" | ||
} | ||
} |
Oops, something went wrong.