Skip to content

Commit

Permalink
Implement support for iron types (#3038)
Browse files Browse the repository at this point in the history
  • Loading branch information
majk-p authored Aug 10, 2023
1 parent 2f010e4 commit 5c73922
Show file tree
Hide file tree
Showing 10 changed files with 629 additions and 1 deletion.
18 changes: 17 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ lazy val rawAllAggregates = core.projectRefs ++
catsEffect.projectRefs ++
enumeratum.projectRefs ++
refined.projectRefs ++
iron.projectRefs ++
zio1.projectRefs ++
zio.projectRefs ++
newtype.projectRefs ++
Expand Down Expand Up @@ -634,7 +635,7 @@ lazy val refined: ProjectMatrix = (projectMatrix in file("integrations/refined")
)
.jvmPlatform(scalaVersions = scala2And3Versions)
.jsPlatform(
scalaVersions = scala2And3Versions,
scalaVersions = List(scala3),
settings = commonJsSettings ++ Seq(
libraryDependencies ++= Seq(
"io.github.cquiroz" %%% "scala-java-time" % Versions.jsScalaJavaTime % Test
Expand All @@ -643,6 +644,21 @@ lazy val refined: ProjectMatrix = (projectMatrix in file("integrations/refined")
)
.dependsOn(core, circeJson % Test)

lazy val iron: ProjectMatrix = (projectMatrix in file("integrations/iron"))
.settings(commonSettings)
.settings(
name := "tapir-iron",
libraryDependencies ++= Seq(
"io.github.iltotore" %% "iron" % Versions.iron,
scalaTest.value % Test
)
)
.jvmPlatform(scalaVersions = List(scala3))
.jsPlatform(
scalaVersions = List(scala3)
)
.dependsOn(core)

lazy val zio1: ProjectMatrix = (projectMatrix in file("integrations/zio1"))
.settings(commonSettings)
.settings(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package sttp.tapir.typelevel
import scala.quoted.Quotes

import scala.annotation.implicitNotFound
import scala.quoted.*
import scala.collection.View.Empty

trait IntersectionTypeMirror[A] {

type ElementTypes <: Tuple
}

// Building a class is more convenient to instantiate using macros
class IntersectionTypeMirrorImpl[A, T <: Tuple] extends IntersectionTypeMirror[A] {
override type ElementTypes = T
}

object IntersectionTypeMirror {

transparent inline given derived[A]: IntersectionTypeMirror[A] = ${ derivedImpl[A] }

private def derivedImpl[A](using Quotes, Type[A]): Expr[IntersectionTypeMirror[A]] = {
import quotes.reflect.*

val tplPrependType = TypeRepr.of[? *: ?]
val tplConcatType = TypeRepr.of[Tuple.Concat]

def prependTypes(head: TypeRepr, tail: TypeRepr): TypeRepr =
AppliedType(tplPrependType, List(head, tail))

def concatTypes(left: TypeRepr, right: TypeRepr): TypeRepr =
AppliedType(tplConcatType, List(left, right))

def rec(tpe: TypeRepr): TypeRepr = {
tpe.dealias match
case AndType(left, right) => concatTypes(rec(left), rec(right))
case t => prependTypes(t, TypeRepr.of[EmptyTuple])
}
val tupled =
TypeRepr.of[A].dealias match {
case and: AndType => rec(and).asType.asInstanceOf[Type[Elems]]
case tpe => report.errorAndAbort(s"${tpe.show} is not an intersection type")
}
type Elems

given Type[Elems] = tupled

Apply( // Passing the type using quotations causes the type to not be inlined
TypeApply(
Select.unique(
New(
Applied(
TypeTree.of[IntersectionTypeMirrorImpl],
List(
TypeTree.of[A],
TypeTree.of[Elems]
)
)
),
"<init>"
),
List(
TypeTree.of[A],
TypeTree.of[Elems]
)
),
Nil
).asExprOf[IntersectionTypeMirror[A]]
}
}
72 changes: 72 additions & 0 deletions core/src/main/scala-3/sttp/tapir/typelevel/UnionTypeMirror.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package sttp.tapir.typelevel

import scala.quoted.Quotes

import scala.annotation.implicitNotFound
import scala.quoted.*
import scala.collection.View.Empty

trait UnionTypeMirror[A] {

type ElementTypes <: Tuple
}

// Building a class is more convenient to instantiate using macros
class UnionTypeMirrorImpl[A, T <: Tuple] extends UnionTypeMirror[A] {

override type ElementTypes = T
}

object UnionTypeMirror {
transparent inline given derived[A]: UnionTypeMirror[A] = ${ derivedImpl[A] }

private def derivedImpl[A](using Quotes, Type[A]): Expr[UnionTypeMirror[A]] = {
import quotes.reflect.*

val tplPrependType = TypeRepr.of[? *: ?]
val tplConcatType = TypeRepr.of[Tuple.Concat]

def prependTypes(head: TypeRepr, tail: TypeRepr): TypeRepr =
AppliedType(tplPrependType, List(head, tail))

def concatTypes(left: TypeRepr, right: TypeRepr): TypeRepr =
AppliedType(tplConcatType, List(left, right))

def rec(tpe: TypeRepr): TypeRepr =
tpe.dealias match {
case OrType(left, right) => concatTypes(rec(left), rec(right))
case t => prependTypes(t, TypeRepr.of[EmptyTuple])
}
val tupled =
TypeRepr.of[A].dealias match {
case or: OrType => rec(or).asType.asInstanceOf[Type[Elems]]
case tpe => report.errorAndAbort(s"${tpe.show} is not a union type")
}

type Elems

given Type[Elems] = tupled

Apply( // Passing the type using quotations causes the type to not be inlined
TypeApply(
Select.unique(
New(
Applied(
TypeTree.of[UnionTypeMirrorImpl],
List(
TypeTree.of[A],
TypeTree.of[Elems]
)
)
),
"<init>"
),
List(
TypeTree.of[A],
TypeTree.of[Elems]
)
),
Nil
).asExprOf[UnionTypeMirror[A]]
}
}
2 changes: 2 additions & 0 deletions doc/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ Tuple-concatenating code is copied from [akka-http](https://github.com/akka/akka

Parts of generic derivation configuration is copied from [circe](https://github.com/circe/circe/blob/master/modules/generic-extras/src/main/scala/io/circe/generic/extras/Configuration.scala)

Implementation of mirror for union and intersection types are originally implemented by [Iltotore](https://github.com/Iltotore) in [this gist](https://gist.github.com/Iltotore/eece20188d383f7aee16a0b89eeb887f)

Tapir logo & stickers have been drawn by [impurepics](https://twitter.com/impurepics).
18 changes: 18 additions & 0 deletions doc/endpoint/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ If you are not satisfied with the validator generated by `tapir-refined`, you ca
`ValidatorForPredicate[T, P]` in scope using `ValidatorForPredicate.fromPrimitiveValidator` to build it (do not
hesitate to contribute your work!).

## Iron integration

If you use [iron](https://github.com/Iltotore/iron), the `tapir-iron` module will provide implicit codecs and
validators for `T :| P` as long as a codec for `T` already exists:

```scala
"com.softwaremill.sttp.tapir" %% "tapir-iron" % "@VERSION@"
```

The module is only available for Scala 3 since iron is not designed to work with Scala 2.

You'll need to extend the `sttp.tapir.codec.refined.TapirCodecIron`
trait or `import sttp.tapir.codec.iron._` to bring the implicit values into scope.

The iron codecs contain a validator which apply the constraint to validated value.

Similarly to `tapir-refined`, you can find the predicate logic in `integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala` and provide your own given `ValidatorForPredicate[T, P]` in scope using `ValidatorForPredicate.fromPrimitiveValidator`

## Enumeratum integration

The `tapir-enumeratum` module provides schemas, validators and codecs for [Enumeratum](https://github.com/lloydmeta/enumeratum)
Expand Down
1 change: 1 addition & 0 deletions doc/stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ The modules are categorised using the following levels:
| zio | experimental |
| zio1 | stabilising |
| zio-prelude | experimental |
| iron | experimental |

## JSON modules

Expand Down
Loading

0 comments on commit 5c73922

Please sign in to comment.